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
320 changed files with 16519 additions and 42713 deletions

8
.gitattributes vendored
View File

@@ -2,14 +2,6 @@
# ensures this even if the user has not set core.autocrlf. # ensures this even if the user has not set core.autocrlf.
* text=auto * 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 # This directory contains email messages verbatim, and changing CRLF to
# LF will corrupt them. # LF will corrupt them.
test-data/** text=false test-data/** text=false

View File

@@ -5,5 +5,5 @@ updates:
schedule: schedule:
interval: "monthly" interval: "monthly"
commit-message: commit-message:
prefix: "chore(cargo)" prefix: "cargo"
open-pull-requests-limit: 50 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 name: Rust CI
# Cancel previously started workflow runs # Cancel previously started workflow runs
@@ -14,21 +10,19 @@ on:
pull_request: pull_request:
push: push:
branches: branches:
- main - master
env: env:
RUSTFLAGS: -Dwarnings RUSTFLAGS: -Dwarnings
jobs: jobs:
lint_rust: lint:
name: Lint Rust name: Rustfmt and Clippy
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
RUSTUP_TOOLCHAIN: 1.78.0 RUSTUP_TOOLCHAIN: 1.68.0
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with:
show-progress: false
- name: Install rustfmt and clippy - name: Install rustfmt and clippy
run: rustup toolchain install $RUSTUP_TOOLCHAIN --profile minimal --component rustfmt --component clippy run: rustup toolchain install $RUSTUP_TOOLCHAIN --profile minimal --component rustfmt --component clippy
- name: Cache rust cargo artifacts - name: Cache rust cargo artifacts
@@ -37,28 +31,12 @@ jobs:
run: cargo fmt --all -- --check run: cargo fmt --all -- --check
- name: Run clippy - name: Run clippy
run: scripts/clippy.sh run: scripts/clippy.sh
- name: Check
run: cargo check --workspace --all-targets --all-features
npm_constants:
name: Check if node constants are up to date
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Rebuild constants
run: npm run build:core:constants
- name: Check that constants are not changed
run: git diff --exit-code
cargo_deny: cargo_deny:
name: cargo deny name: cargo deny
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with:
show-progress: false
- uses: EmbarkStudios/cargo-deny-action@v1 - uses: EmbarkStudios/cargo-deny-action@v1
with: with:
arguments: --all-features --workspace arguments: --all-features --workspace
@@ -69,9 +47,7 @@ jobs:
name: Check provider database name: Check provider database
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with:
show-progress: false
- name: Check provider database - name: Check provider database
run: scripts/update-provider-database.sh run: scripts/update-provider-database.sh
@@ -81,34 +57,38 @@ jobs:
env: env:
RUSTDOCFLAGS: -Dwarnings RUSTDOCFLAGS: -Dwarnings
steps: steps:
- uses: actions/checkout@v4 - name: Checkout sources
with: uses: actions/checkout@v3
show-progress: false
- name: Cache rust cargo artifacts - name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2 uses: swatinem/rust-cache@v2
- name: Rustdoc - name: Rustdoc
run: cargo doc --document-private-items --no-deps run: cargo doc --document-private-items --no-deps
rust_tests: build_and_test:
name: Rust tests name: Build and test
strategy: strategy:
fail-fast: false
matrix: matrix:
include: include:
# Currently used Rust version.
- os: ubuntu-latest - os: ubuntu-latest
rust: 1.78.0 rust: 1.68.0
python: 3.9
- os: windows-latest - os: windows-latest
rust: 1.78.0 rust: 1.68.0
- os: macos-latest python: false # Python bindings compilation on Windows is not supported.
rust: 1.78.0
# Minimum Supported Rust Version = 1.77.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 - os: ubuntu-latest
rust: 1.77.0 rust: 1.64.0
python: 3.7
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@master
with:
show-progress: false
- name: Install Rust ${{ matrix.rust }} - name: Install Rust ${{ matrix.rust }}
run: rustup toolchain install --profile minimal ${{ matrix.rust }} run: rustup toolchain install --profile minimal ${{ matrix.rust }}
@@ -117,206 +97,64 @@ jobs:
- name: Cache rust cargo artifacts - name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2 uses: swatinem/rust-cache@v2
- name: Install nextest - name: Check
uses: taiki-e/install-action@v2 run: cargo check --workspace --bins --examples --tests --benches
with:
tool: nextest
- name: Tests - name: Tests
env: run: cargo test --workspace
RUST_BACKTRACE: 1
# Workaround for <https://github.com/nextest-rs/nextest/issues/1493>.
RUSTUP_WINDOWS_PATH_ADD_BIN: 1
run: cargo nextest run --workspace
- name: Doc-Tests
env:
RUST_BACKTRACE: 1
run: cargo test --workspace --doc
- name: Test cargo vendor - name: Test cargo vendor
run: 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@v4
with:
show-progress: false
- 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@v4
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, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- 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@v4
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
retention-days: 1
python_lint:
name: Python lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- 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
cffi_python_tests:
name: CFFI Python tests
needs: ["c_library", "python_lint"]
strategy:
fail-fast: false
matrix:
include:
# Currently used Rust version.
- os: ubuntu-latest
python: 3.12
- os: macos-latest
python: 3.12
# PyPy tests
- os: ubuntu-latest
python: pypy3.10
- os: macos-latest
python: pypy3.10
# 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@v4
with:
show-progress: false
- name: Download libdeltachat.a
uses: actions/download-artifact@v4
with:
name: ${{ matrix.os }}-libdeltachat.a
path: target/debug
- name: Install python - name: Install python
uses: actions/setup-python@v5 if: ${{ matrix.python }}
uses: actions/setup-python@v4
with: with:
python-version: ${{ matrix.python }} python-version: ${{ matrix.python }}
- name: Install tox - name: Install tox
if: ${{ matrix.python }}
run: pip install tox run: pip install tox
- name: Build C library
if: ${{ matrix.python }}
run: cargo build -p deltachat_ffi --features jsonrpc
- name: Run python tests - name: Run python tests
if: ${{ matrix.python }}
env: env:
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }} DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
DCC_RS_TARGET: debug DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }} DCC_RS_DEV: ${{ github.workspace }}
working-directory: python working-directory: python
run: tox -e mypy,doc,py run: tox -e lint,mypy,doc,py3
rpc_python_tests: - name: Build deltachat-rpc-server
name: JSON-RPC Python tests if: ${{ matrix.python }}
needs: ["python_lint", "rpc_server"] run: cargo build -p deltachat-rpc-server
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
python: 3.12
- os: macos-latest
python: 3.12
- os: windows-latest
python: 3.12
# PyPy tests
- os: ubuntu-latest
python: pypy3.10
- os: macos-latest
python: pypy3.10
# Minimum Supported Python Version = 3.7
- os: ubuntu-latest
python: 3.7
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install python
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python }}
- name: Install tox
run: pip install tox
- name: Download deltachat-rpc-server
uses: actions/download-artifact@v4
with:
name: ${{ matrix.os }}-deltachat-rpc-server
path: target/debug
- name: Make deltachat-rpc-server executable
if: ${{ matrix.os != 'windows-latest' }}
run: chmod +x target/debug/deltachat-rpc-server
- name: Add deltachat-rpc-server to path - name: Add deltachat-rpc-server to path
if: ${{ matrix.os != 'windows-latest' }} if: ${{ matrix.python }}
run: echo ${{ github.workspace }}/target/debug >> $GITHUB_PATH run: echo ${{ github.workspace }}/target/debug >> $GITHUB_PATH
- name: Add deltachat-rpc-server to path
if: ${{ matrix.os == 'windows-latest' }}
run: |
"${{ github.workspace }}/target/debug" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append
- name: Run deltachat-rpc-client tests - name: Run deltachat-rpc-client tests
if: ${{ matrix.python }}
env: env:
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }} DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
working-directory: deltachat-rpc-client 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 # Manually triggered action to build deltachat-rpc-server binaries.
# 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.
name: Build deltachat-rpc-server binaries name: Build deltachat-rpc-server binaries
@@ -21,386 +15,112 @@ jobs:
# Build a version statically linked against musl libc # Build a version statically linked against musl libc
# to avoid problems with glibc version incompatibility. # to avoid problems with glibc version incompatibility.
build_linux: build_linux:
name: Linux name: Cross-compile deltachat-rpc-server for x86_64, aarch64 and armv7 Linux
strategy: runs-on: ubuntu-22.04
fail-fast: false
matrix:
arch: [aarch64, armv7l, armv6l, i686, x86_64]
runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build deltachat-rpc-server binaries - name: Build
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux run: sh scripts/zig-rpc-server.sh
- name: Upload binary - name: Upload x86_64 binary
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: deltachat-rpc-server-${{ matrix.arch }}-linux name: deltachat-rpc-server-x86_64
path: result/bin/deltachat-rpc-server path: target/x86_64-unknown-linux-musl/release/deltachat-rpc-server
if-no-files-found: error
- name: Upload aarch64 binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-aarch64
path: target/aarch64-unknown-linux-musl/release/deltachat-rpc-server
if-no-files-found: error
- name: Upload armv7 binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-armv7
path: target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server
if-no-files-found: error if-no-files-found: error
build_windows: build_windows:
name: Windows name: Build deltachat-rpc-server for Windows
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
arch: [win32, win64] include:
runs-on: ubuntu-latest - os: windows-latest
artifact: win32.exe
path: deltachat-rpc-server.exe
target: i686-pc-windows-msvc
- os: windows-latest
artifact: win64.exe
path: deltachat-rpc-server.exe
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }}
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: deltachat-rpc-server-${{ matrix.arch }}
path: result/bin/deltachat-rpc-server.exe
if-no-files-found: error
build_macos:
name: macOS
strategy:
fail-fast: false
matrix:
arch: [x86_64, aarch64]
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Setup rust target - name: Setup rust target
run: rustup target add ${{ matrix.arch }}-apple-darwin run: rustup target add ${{ matrix.target }}
- name: Build - name: Build
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.target }} --features vendored
- name: Upload binary - name: Upload binary
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: deltachat-rpc-server-${{ matrix.arch }}-macos name: deltachat-rpc-server-${{ matrix.artifact }}
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server path: target/${{ matrix.target}}/release/${{ matrix.path }}
if-no-files-found: error
build_android:
name: Android
strategy:
fail-fast: false
matrix:
arch: [arm64-v8a, armeabi-v7a]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build deltachat-rpc-server binaries
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
- name: Upload binary
uses: actions/upload-artifact@v4
with:
name: deltachat-rpc-server-${{ matrix.arch }}-android
path: result/bin/deltachat-rpc-server
if-no-files-found: error if-no-files-found: error
publish: publish:
name: Build wheels and upload binaries to the release name: Upload binaries to the release
needs: ["build_linux", "build_windows", "build_macos"] needs: ["build_linux", "build_windows"]
environment:
name: pypi
url: https://pypi.org/p/deltachat-rpc-server
permissions: permissions:
id-token: write
contents: write contents: write
runs-on: "ubuntu-latest" runs-on: "ubuntu-latest"
steps: steps:
- uses: actions/checkout@v4 - name: Download deltachat-rpc-server-x86_64
uses: "actions/download-artifact@v3"
with: with:
show-progress: false name: "deltachat-rpc-server-x86_64"
- uses: DeterminateSystems/nix-installer-action@main path: "dist/deltachat-rpc-server-x86_64"
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Download Linux aarch64 binary - name: Download deltachat-rpc-server-aarch64
uses: actions/download-artifact@v4 uses: "actions/download-artifact@v3"
with: with:
name: deltachat-rpc-server-aarch64-linux name: "deltachat-rpc-server-aarch64"
path: deltachat-rpc-server-aarch64-linux.d path: "dist/deltachat-rpc-server-aarch64"
- name: Download Linux armv7l binary - name: Download deltachat-rpc-server-armv7
uses: actions/download-artifact@v4 uses: "actions/download-artifact@v3"
with: with:
name: deltachat-rpc-server-armv7l-linux name: "deltachat-rpc-server-armv7"
path: deltachat-rpc-server-armv7l-linux.d path: "dist/deltachat-rpc-server-armv7"
- name: Download Linux armv6l binary - name: Download deltachat-rpc-server-win32.exe
uses: actions/download-artifact@v4 uses: "actions/download-artifact@v3"
with: with:
name: deltachat-rpc-server-armv6l-linux name: "deltachat-rpc-server-win32.exe"
path: deltachat-rpc-server-armv6l-linux.d path: "dist/deltachat-rpc-server-win32.exe"
- name: Download Linux i686 binary - name: Download deltachat-rpc-server-win64.exe
uses: actions/download-artifact@v4 uses: "actions/download-artifact@v3"
with: with:
name: deltachat-rpc-server-i686-linux name: "deltachat-rpc-server-win64.exe"
path: deltachat-rpc-server-i686-linux.d path: "dist/deltachat-rpc-server-win64.exe"
- name: Download Linux x86_64 binary - name: List downloaded artifacts
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Win32 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win64 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Download Android binary for arm64-v8a
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
- name: Create bin/ directory
run: |
mkdir -p bin
mv deltachat-rpc-server-aarch64-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-linux
mv deltachat-rpc-server-armv7l-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-armv7l-linux
mv deltachat-rpc-server-armv6l-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-armv6l-linux
mv deltachat-rpc-server-i686-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-i686-linux
mv deltachat-rpc-server-x86_64-linux.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-linux
mv deltachat-rpc-server-win32.d/deltachat-rpc-server.exe bin/deltachat-rpc-server-win32.exe
mv deltachat-rpc-server-win64.d/deltachat-rpc-server.exe bin/deltachat-rpc-server-win64.exe
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-x86_64-macos
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server bin/deltachat-rpc-server-aarch64-macos
mv deltachat-rpc-server-arm64-v8a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-arm64-v8a-android
mv deltachat-rpc-server-armeabi-v7a-android.d/deltachat-rpc-server bin/deltachat-rpc-server-armeabi-v7a-android
- name: List binaries
run: ls -l bin/
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
- name: Install python 3.12
uses: actions/setup-python@v5
with:
python-version: 3.12
- name: Install wheel
run: pip install wheel
- name: Build deltachat-rpc-server Python wheels and source package
run: |
mkdir -p dist
nix build .#deltachat-rpc-server-x86_64-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-armv7l-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-armv6l-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-aarch64-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-i686-linux-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-win64-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-win32-wheel
cp result/*.whl dist/
nix build .#deltachat-rpc-server-source
cp result/*.tar.gz dist/
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
python3 scripts/wheel-rpc-server.py aarch64-darwin bin/deltachat-rpc-server-aarch64-macos
mv *.whl dist/
- name: List artifacts
run: ls -l dist/ run: ls -l dist/
- name: Upload binaries to the GitHub release - name: Upload binaries to the GitHub release
if: github.event_name == 'release'
env: env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
run: | run: |
gh release upload ${{ github.ref_name }} \ gh release upload ${{ github.ref_name }} \
--repo ${{ github.repository }} \ --repo ${{ github.repository }} \
bin/* dist/* dist/deltachat-rpc-server-*
- name: Publish deltachat-rpc-client to PyPI
if: github.event_name == 'release'
uses: pypa/gh-action-pypi-publish@release/v1
publish_npm_package:
name: Build & Publish npm prebuilds and deltachat-rpc-server
needs: ["build_linux", "build_windows", "build_macos"]
runs-on: "ubuntu-latest"
permissions:
id-token: write
# Needed to publish the binaries to the release.
contents: write
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Download Linux aarch64 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-aarch64-linux
path: deltachat-rpc-server-aarch64-linux.d
- name: Download Linux armv7l binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armv7l-linux
path: deltachat-rpc-server-armv7l-linux.d
- name: Download Linux armv6l binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armv6l-linux
path: deltachat-rpc-server-armv6l-linux.d
- name: Download Linux i686 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-i686-linux
path: deltachat-rpc-server-i686-linux.d
- name: Download Linux x86_64 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-x86_64-linux
path: deltachat-rpc-server-x86_64-linux.d
- name: Download Win32 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win32
path: deltachat-rpc-server-win32.d
- name: Download Win64 binary
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-win64
path: deltachat-rpc-server-win64.d
- name: Download macOS binary for x86_64
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-x86_64-macos
path: deltachat-rpc-server-x86_64-macos.d
- name: Download macOS binary for aarch64
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-aarch64-macos
path: deltachat-rpc-server-aarch64-macos.d
- name: Download Android binary for arm64-v8a
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-arm64-v8a-android
path: deltachat-rpc-server-arm64-v8a-android.d
- name: Download Android binary for armeabi-v7a
uses: actions/download-artifact@v4
with:
name: deltachat-rpc-server-armeabi-v7a-android
path: deltachat-rpc-server-armeabi-v7a-android.d
- name: make npm packets for prebuilds and `@deltachat/stdio-rpc-server`
run: |
cd deltachat-rpc-server/npm-package
python --version
python scripts/pack_binary_for_platform.py aarch64-unknown-linux-musl ../../deltachat-rpc-server-aarch64-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py armv7-unknown-linux-musleabihf ../../deltachat-rpc-server-armv7l-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py arm-unknown-linux-musleabihf ../../deltachat-rpc-server-armv6l-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py i686-unknown-linux-musl ../../deltachat-rpc-server-i686-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py x86_64-unknown-linux-musl ../../deltachat-rpc-server-x86_64-linux.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py i686-pc-windows-gnu ../../deltachat-rpc-server-win32.d/deltachat-rpc-server.exe
python scripts/pack_binary_for_platform.py x86_64-pc-windows-gnu ../../deltachat-rpc-server-win64.d/deltachat-rpc-server.exe
python scripts/pack_binary_for_platform.py x86_64-apple-darwin ../../deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py aarch64-apple-darwin ../../deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py aarch64-linux-android ../../deltachat-rpc-server-arm64-v8a-android.d/deltachat-rpc-server
python scripts/pack_binary_for_platform.py armv7-linux-androideabi ../../deltachat-rpc-server-armeabi-v7a-android.d/deltachat-rpc-server
ls -lah platform_package
for platform in ./platform_package/*; do npm pack "$platform"; done
npm pack
ls -lah
- name: Upload to artifacts
uses: actions/upload-artifact@v4
with:
name: deltachat-rpc-server-npm-package
path: deltachat-rpc-server/npm-package/*.tgz
if-no-files-found: error
- name: Upload npm packets to the GitHub release
if: github.event_name == 'release'
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
run: |
gh release upload ${{ github.ref_name }} \
--repo ${{ github.repository }} \
deltachat-rpc-server/npm-package/*.tgz
# Configure Node.js for publishing.
- uses: actions/setup-node@v4
with:
node-version: 20
registry-url: "https://registry.npmjs.org"
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
if: github.event_name == 'release'
working-directory: deltachat-rpc-server/npm-package
run: |
ls -lah platform_package
for platform in *.tgz; do npm publish --provenance "$platform"; done
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

View File

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

View File

@@ -1,38 +1,82 @@
name: "Publish @deltachat/jsonrpc-client" name: "jsonrpc js client build"
on: on:
workflow_dispatch: pull_request:
release: push:
types: [published] tags:
- "*"
- "!py-*"
jobs: jobs:
pack-module: pack-module:
name: "Publish @deltachat/jsonrpc-client" name: "Package @deltachat/jsonrpc-client and upload to download.delta.chat"
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
permissions:
id-token: write
contents: read
steps: steps:
- uses: actions/checkout@v4 - name: Install tree
run: sudo apt install tree
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with: with:
show-progress: false node-version: "16"
- name: Get tag
- uses: actions/setup-node@v4 id: tag
with: uses: dawidd6/action-get-tag@v1
node-version: 20 continue-on-error: true
registry-url: "https://registry.npmjs.org" - name: Get Pull Request ID
id: prepare
- name: Install dependencies without running scripts
working-directory: deltachat-jsonrpc/typescript
run: npm install --ignore-scripts
- name: Package
working-directory: deltachat-jsonrpc/typescript
run: | run: |
tag=${{ steps.tag.outputs.tag }}
if [ -z "$tag" ]; then
node -e "console.log('DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-' + '${{ github.ref }}'.split('/')[2] + '.tar.gz')" >> $GITHUB_ENV
else
echo "DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-${{ steps.tag.outputs.tag }}.tar.gz" >> $GITHUB_ENV
echo "No preview will be uploaded this time, but the $tag release"
fi
- name: System info
run: |
npm --version
node --version
echo $DELTACHAT_JSONRPC_TAR_GZ
- name: Install dependencies without running scripts
run: |
cd deltachat-jsonrpc/typescript
npm install --ignore-scripts
- name: Package
shell: bash
run: |
cd deltachat-jsonrpc/typescript
npm run build npm run build
npm pack . npm pack .
ls -lah
- name: Publish mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
working-directory: deltachat-jsonrpc/typescript - name: Upload Prebuild
run: npm publish --provenance deltachat-jsonrpc-client-* uses: actions/upload-artifact@v3
with:
name: deltachat-jsonrpc-client.tgz
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
# Upload to download.delta.chat/node/preview/
- name: Upload deltachat-jsonrpc-client preview to download.delta.chat/node/preview/
if: ${{ ! steps.tag.outputs.tag }}
id: upload-preview
shell: bash
run: |
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
chmod 600 __TEMP_INPUT_KEY_FILE
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
continue-on-error: true
- name: Post links to details
if: steps.upload-preview.outcome == 'success'
run: node ./node/scripts/postLinksToDetails.js
env: env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} URL: preview/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MSG_CONTEXT: Download the deltachat-jsonrpc-client.tgz
# Upload to download.delta.chat/node/
- name: Upload deltachat-jsonrpc-client build to download.delta.chat/node/
if: ${{ steps.tag.outputs.tag }}
id: upload
shell: bash
run: |
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
chmod 600 __TEMP_INPUT_KEY_FILE
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/"

View File

@@ -2,9 +2,9 @@ name: JSON-RPC API Test
on: on:
push: push:
branches: [main] branches: [master]
pull_request: pull_request:
branches: [main] branches: [master]
env: env:
CARGO_TERM_COLOR: always CARGO_TERM_COLOR: always
@@ -14,29 +14,32 @@ jobs:
build_and_test: build_and_test:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with: with:
show-progress: false node-version: 16.x
- name: Use Node.js 18.x
uses: actions/setup-node@v4
with:
node-version: 18.x
- name: Add Rust cache - name: Add Rust cache
uses: Swatinem/rust-cache@v2 uses: Swatinem/rust-cache@v2
- name: npm install - name: npm install
working-directory: deltachat-jsonrpc/typescript run: |
run: npm install cd deltachat-jsonrpc/typescript
npm install
- name: Build TypeScript, run Rust tests, generate bindings - name: Build TypeScript, run Rust tests, generate bindings
working-directory: deltachat-jsonrpc/typescript run: |
run: npm run build cd deltachat-jsonrpc/typescript
npm run build
- name: Run integration tests - name: Run integration tests
working-directory: deltachat-jsonrpc/typescript run: |
run: npm run test cd deltachat-jsonrpc/typescript
npm run test
env: env:
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }} DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
- name: make sure websocket server version still builds - name: make sure websocket server version still builds
working-directory: deltachat-jsonrpc run: |
run: cargo build --bin deltachat-jsonrpc-server --features webserver cd deltachat-jsonrpc
cargo build --bin deltachat-jsonrpc-server --features webserver
- name: Run linter - name: Run linter
working-directory: deltachat-jsonrpc/typescript run: |
run: npm run prettier:check 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,31 +1,24 @@
# 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 name: Generate & upload node.js documentation
on: on:
push: push:
branches: branches:
- main - master
jobs: jobs:
generate: generate:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with:
show-progress: false
- name: Use Node.js 18.x - name: Use Node.js 16.x
uses: actions/setup-node@v4 uses: actions/setup-node@v3
with: with:
node-version: 18.x node-version: 16.x
- name: npm install and generate documentation - name: npm install and generate documentation
working-directory: node
run: | run: |
cd node
npm i --ignore-scripts npm i --ignore-scripts
npx typedoc npx typedoc
mv docs js mv docs js

View File

@@ -12,14 +12,13 @@ jobs:
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
strategy: strategy:
matrix: matrix:
os: [macos-latest, windows-latest] os: [ubuntu-20.04, macos-latest, windows-latest]
steps: steps:
- uses: actions/checkout@v4 - name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with: with:
show-progress: false node-version: "16"
- uses: actions/setup-node@v4
with:
node-version: "18"
- name: System info - name: System info
run: | run: |
rustc -vV rustc -vV
@@ -29,7 +28,7 @@ jobs:
node --version node --version
- name: Cache node modules - name: Cache node modules
uses: actions/cache@v4 uses: actions/cache@v3
with: with:
path: | path: |
${{ env.APPDATA }}/npm-cache ${{ env.APPDATA }}/npm-cache
@@ -37,7 +36,7 @@ jobs:
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }} key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index - name: Cache cargo index
uses: actions/cache@v4 uses: actions/cache@v3
with: with:
path: | path: |
~/.cargo/registry/ ~/.cargo/registry/
@@ -47,104 +46,34 @@ jobs:
- name: Install dependencies & build - name: Install dependencies & build
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
working-directory: node run: |
run: npm install --verbose cd node
npm install --verbose
- name: Build Prebuild - name: Build Prebuild
working-directory: node
run: | run: |
cd node
npm run prebuildify npm run prebuildify
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds . tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
- name: Upload Prebuild - name: Upload Prebuild
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: ${{ matrix.os }} name: ${{ matrix.os }}
path: node/${{ matrix.os }}.tar.gz 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: https://packages.debian.org/buster/libc6
container: debian:10
steps:
# Working directory is owned by 1001:1001 by default.
# Change it to our user.
- name: Change working directory owner
run: chown root:root .
- uses: actions/checkout@v4
with:
show-progress: false
- uses: actions/setup-node@v4
with:
node-version: "18"
- 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@v4
with:
path: |
${{ env.APPDATA }}/npm-cache
~/.npm
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index
uses: actions/cache@v4
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@v4
with:
name: linux
path: node/linux.tar.gz
pack-module: pack-module:
needs: [prebuild, prebuild-linux] needs: prebuild
name: Package deltachat-node and upload to download.delta.chat name: Package deltachat-node and upload to download.delta.chat
runs-on: ubuntu-latest runs-on: ubuntu-20.04
steps: steps:
- name: Install tree - name: Install tree
run: sudo apt install tree run: sudo apt install tree
- uses: actions/checkout@v4 - name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v2
with: with:
show-progress: false node-version: "16"
- uses: actions/setup-node@v4
with:
node-version: "18"
- name: Get tag - name: Get tag
id: tag id: tag
uses: dawidd6/action-get-tag@v1 uses: dawidd6/action-get-tag@v1
@@ -167,26 +96,26 @@ jobs:
npm --version npm --version
node --version node --version
echo $DELTACHAT_NODE_TAR_GZ echo $DELTACHAT_NODE_TAR_GZ
- name: Download Linux prebuild - name: Download Ubuntu prebuild
uses: actions/download-artifact@v4 uses: actions/download-artifact@v1
with: with:
name: linux name: ubuntu-20.04
- name: Download macOS prebuild - name: Download macOS prebuild
uses: actions/download-artifact@v4 uses: actions/download-artifact@v1
with: with:
name: macos-latest name: macos-latest
- name: Download Windows prebuild - name: Download Windows prebuild
uses: actions/download-artifact@v4 uses: actions/download-artifact@v1
with: with:
name: windows-latest name: windows-latest
- shell: bash - shell: bash
run: | run: |
mkdir node/prebuilds mkdir node/prebuilds
tar -xvzf linux.tar.gz -C node/prebuilds tar -xvzf ubuntu-20.04/ubuntu-20.04.tar.gz -C node/prebuilds
tar -xvzf macos-latest.tar.gz -C node/prebuilds tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
tar -xvzf windows-latest.tar.gz -C node/prebuilds tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
tree node/prebuilds tree node/prebuilds
rm -f linux.tar.gz macos-latest.tar.gz windows-latest.tar.gz rm -rf ubuntu-20.04 macos-latest windows-latest
- name: Install dependencies without running scripts - name: Install dependencies without running scripts
run: | run: |
npm install --ignore-scripts npm install --ignore-scripts
@@ -204,7 +133,7 @@ jobs:
ls -lah ls -lah
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
- name: Upload prebuild - name: Upload prebuild
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: deltachat-node.tgz name: deltachat-node.tgz
path: ${{ env.DELTACHAT_NODE_TAR_GZ }} path: ${{ env.DELTACHAT_NODE_TAR_GZ }}

View File

@@ -1,6 +1,3 @@
# GitHub Actions workflow
# to test Node.js bindings.
name: "node.js tests" name: "node.js tests"
# Cancel previously started workflow runs # Cancel previously started workflow runs
@@ -13,7 +10,7 @@ on:
pull_request: pull_request:
push: push:
branches: branches:
- main - master
jobs: jobs:
tests: tests:
@@ -23,12 +20,11 @@ jobs:
matrix: matrix:
os: [ubuntu-latest, macos-latest, windows-latest] os: [ubuntu-latest, macos-latest, windows-latest]
steps: steps:
- uses: actions/checkout@v4 - name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with: with:
show-progress: false node-version: "16"
- uses: actions/setup-node@v4
with:
node-version: "18"
- name: System info - name: System info
run: | run: |
rustc -vV rustc -vV
@@ -38,7 +34,7 @@ jobs:
node --version node --version
- name: Cache node modules - name: Cache node modules
uses: actions/cache@v4 uses: actions/cache@v3
with: with:
path: | path: |
${{ env.APPDATA }}/npm-cache ${{ env.APPDATA }}/npm-cache
@@ -46,7 +42,7 @@ jobs:
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }} key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
- name: Cache cargo index - name: Cache cargo index
uses: actions/cache@v4 uses: actions/cache@v3
with: with:
path: | path: |
~/.cargo/registry/ ~/.cargo/registry/
@@ -56,13 +52,25 @@ jobs:
- name: Install dependencies & build - name: Install dependencies & build
if: steps.cache.outputs.cache-hit != 'true' if: steps.cache.outputs.cache-hit != 'true'
working-directory: node run: |
run: npm install --verbose cd node
npm install --verbose
- name: Test - name: Test
timeout-minutes: 10 timeout-minutes: 10
working-directory: node if: runner.os != 'Windows'
run: npm run test run: |
cd node
npm run test
env: env:
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }} 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" NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"

View File

@@ -1,47 +0,0 @@
name: Publish deltachat-rpc-client to PyPI
on:
workflow_dispatch:
release:
types: [published]
jobs:
build:
name: Build distribution
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
- name: Install pypa/build
run: python3 -m pip install build
- name: Build a binary wheel and a source tarball
working-directory: deltachat-rpc-client
run: python3 -m build
- name: Store the distribution packages
uses: actions/upload-artifact@v4
with:
name: python-package-distributions
path: deltachat-rpc-client/dist/
publish-to-pypi:
name: Publish Python distribution to PyPI
if: github.event_name == 'release'
needs:
- build
runs-on: ubuntu-latest
environment:
name: pypi
url: https://pypi.org/p/deltachat-rpc-client
permissions:
id-token: write
steps:
- name: Download all the dists
uses: actions/download-artifact@v4
with:
name: python-package-distributions
path: dist/
- name: Publish deltachat-rpc-client to PyPI
uses: pypa/gh-action-pypi-publish@release/v1

View File

@@ -1,5 +1,4 @@
# Manually triggered GitHub Actions workflow # Manually triggered action to build a Windows repl.exe which users can
# to build a Windows repl.exe which users can
# download to debug complex bugs. # download to debug complex bugs.
name: Build Windows REPL .exe name: Build Windows REPL .exe
@@ -10,17 +9,15 @@ on:
jobs: jobs:
build_repl: build_repl:
name: Build REPL example name: Build REPL example
runs-on: ubuntu-latest runs-on: windows-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with:
show-progress: false
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build - name: Build
run: nix build .#deltachat-repl-win64 run: cargo build -p deltachat-repl --features vendored
- name: Upload binary - name: Upload binary
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v3
with: with:
name: repl.exe name: repl.exe
path: "result/bin/deltachat-repl.exe" path: "target/debug/deltachat-repl.exe"

View File

@@ -1,91 +1,25 @@
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, and py.delta.chat name: Build & Deploy Documentation on rs.delta.chat
on: on:
push: push:
branches: branches:
- main - master
- build_jsonrpc_docs_ci - docs-gh-action
jobs: jobs:
build-rs: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with:
show-progress: false
- name: Build the documentation with cargo - name: Build the documentation with cargo
run: | run: |
cargo doc --package deltachat --no-deps --document-private-items cargo doc --package deltachat --no-deps --document-private-items
- name: Upload to rs.delta.chat - name: Upload to rs.delta.chat
run: | uses: up9cloud/action-rsync@v1.3
mkdir -p "$HOME/.ssh" env:
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key" USER: ${{ secrets.USERNAME }}
chmod 600 "$HOME/.ssh/key" KEY: ${{ secrets.KEY }}
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.USERNAME }}@rs.delta.chat:/var/www/html/rs/" HOST: "delta.chat"
SOURCE: "target/doc"
build-python: TARGET: "/var/www/html/rs/"
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build Python documentation
run: nix build .#python-docs
- name: Upload to py.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@py.delta.chat:/home/delta/build/master"
build-c:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
show-progress: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Build C documentation
run: nix build .#docs
- name: Upload to c.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@c.delta.chat:/home/delta/build-c/master"
build-ts:
runs-on: ubuntu-latest
defaults:
run:
working-directory: ./deltachat-jsonrpc/typescript
steps:
- uses: actions/checkout@v4
with:
show-progress: false
fetch-depth: 0 # Fetch history to calculate VCS version number.
- name: Use Node.js
uses: actions/setup-node@v2
with:
node-version: '18'
- name: npm install
run: npm install
- name: npm run build
run: npm run build
- name: Run docs script
run: npm run docs
- name: Upload to js.jsonrpc.delta.chat
run: |
mkdir -p "$HOME/.ssh"
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
chmod 600 "$HOME/.ssh/key"
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.USERNAME }}@js.jsonrpc.delta.chat:/var/www/html/js-jsonrpc/"

View File

@@ -1,28 +1,25 @@
# GitHub Actions workflow
# to build `deltachat_ffi` crate documentation
# and upload it to <https://cffi.delta.chat/>
name: Build & Deploy Documentation on cffi.delta.chat name: Build & Deploy Documentation on cffi.delta.chat
on: on:
push: push:
branches: branches:
- main - master
- docs-gh-action
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v3
with:
show-progress: false
- name: Build the documentation with cargo - name: Build the documentation with cargo
run: | run: |
cargo doc --package deltachat_ffi --no-deps cargo doc --package deltachat_ffi --no-deps
- name: Upload to cffi.delta.chat - name: Upload to cffi.delta.chat
run: | uses: up9cloud/action-rsync@v1.3
mkdir -p "$HOME/.ssh" env:
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key" USER: ${{ secrets.USERNAME }}
chmod 600 "$HOME/.ssh/key" KEY: ${{ secrets.KEY }}
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc/ "${{ secrets.USERNAME }}@delta.chat:/var/www/html/cffi/" HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/cffi/"

13
.gitignore vendored
View File

@@ -1,7 +1,6 @@
/target /target
**/*.rs.bk **/*.rs.bk
/build /build
/dist
# ignore vi temporaries # ignore vi temporaries
*~ *~
@@ -19,9 +18,6 @@ python/.eggs
__pycache__ __pycache__
python/src/deltachat/capi*.so python/src/deltachat/capi*.so
python/.venv/ python/.venv/
python/venv/
venv/
env/
python/liveconfig* python/liveconfig*
@@ -33,7 +29,7 @@ deltachat-ffi/xml
coverage/ coverage/
.DS_Store .DS_Store
.vscode .vscode/launch.json
python/accounts.txt python/accounts.txt
python/all-testaccounts.txt python/all-testaccounts.txt
tmp/ tmp/
@@ -44,10 +40,3 @@ node/build/
node/dist/ node/dist/
node/prebuilds/ node/prebuilds/
node/.nyc_output/ node/.nyc_output/
# Nix symlink.
result
# direnv
.envrc
.direnv

File diff suppressed because it is too large Load Diff

View File

@@ -12,22 +12,26 @@ else()
set(DYNAMIC_EXT "dll") set(DYNAMIC_EXT "dll")
endif() endif()
if(DEFINED ENV{CARGO_BUILD_TARGET})
set(ARCH_DIR "$ENV{CARGO_BUILD_TARGET}")
else()
set(ARCH_DIR "./")
endif()
add_custom_command( add_custom_command(
OUTPUT OUTPUT
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a" "target/release/libdeltachat.a"
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}" "target/release/libdeltachat.${DYNAMIC_EXT}"
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc" "target/release/pkgconfig/deltachat.pc"
COMMAND COMMAND
PREFIX=${CMAKE_INSTALL_PREFIX} PREFIX=${CMAKE_INSTALL_PREFIX}
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR} LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR} INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --no-default-features --features jsonrpc ${CARGO} build --release --no-default-features --features jsonrpc
# Build in `deltachat-ffi` directory instead of using
# `--package deltachat_ffi` to avoid feature resolver version
# "1" bug which makes `--no-default-features` affect only
# `deltachat`, but not `deltachat-ffi` package.
#
# We can't enable version "2" resolver [1] because it is not
# stable yet on rust 1.50.0.
#
# [1] https://doc.rust-lang.org/nightly/cargo/reference/features.html#resolver-version-2-command-line-flags
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
) )
@@ -35,12 +39,12 @@ add_custom_target(
lib_deltachat lib_deltachat
ALL ALL
DEPENDS DEPENDS
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a" "target/release/libdeltachat.a"
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}" "target/release/libdeltachat.${DYNAMIC_EXT}"
"${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc" "target/release/pkgconfig/deltachat.pc"
) )
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR}) install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR}) install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR}) install(FILES "target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig) install(FILES "target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)

View File

@@ -1,126 +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/
### Errors
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
When using [`Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html),
capitalize it but do not add a full stop as the contexts will be separated by `:`.
For example:
```
.with_context(|| format!("Unable to trash message {msg_id}"))
```
All errors should be handled in one of these ways:
- With `if let Err() =` (incl. logging them into `warn!()`/`err!()`).
- With `.log_err().ok()`.
- Bubbled up with `?`.
`backtrace` feature is enabled for `anyhow` crate
and `debug = 1` option is set in the test profile.
This allows to run `RUST_BACKTRACE=1 cargo test`
and get a backtrace with line numbers in resultified tests
which return `anyhow::Result`.
### Logging
For logging, use `info!`, `warn!` and `error!` macros.
Log messages should be capitalized and have a full stop in the end. For example:
```
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
```
Format anyhow errors with `{:#}` to print all the contexts like this:
```
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
```
### Reviewing
Once a PR has an approval and passes CI, it can be merged.
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).

5087
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,9 @@
[package] [package]
name = "deltachat" name = "deltachat"
version = "1.138.5" version = "1.111.0"
edition = "2021" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
rust-version = "1.77" rust-version = "1.64"
repository = "https://github.com/deltachat/deltachat-core-rust"
[profile.dev] [profile.dev]
debug = 0 debug = 0
@@ -12,10 +11,6 @@ panic = 'abort'
opt-level = 1 opt-level = 1
[profile.test] [profile.test]
# Make anyhow `backtrace` feature useful.
# With `debug = 0` there are no line numbers in the backtrace
# produced with RUST_BACKTRACE=1.
debug = 1
opt-level = 0 opt-level = 0
# Always optimize dependencies. # Always optimize dependencies.
@@ -28,106 +23,87 @@ opt-level = "z"
lto = true lto = true
panic = 'abort' panic = 'abort'
opt-level = "z" opt-level = "z"
codegen-units = 1
strip = true [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" }
[dependencies] [dependencies]
deltachat_derive = { path = "./deltachat_derive" } deltachat_derive = { path = "./deltachat_derive" }
deltachat-time = { path = "./deltachat-time" }
deltachat-contact-tools = { path = "./deltachat-contact-tools" }
format-flowed = { path = "./format-flowed" } format-flowed = { path = "./format-flowed" }
ratelimit = { path = "./deltachat-ratelimit" } ratelimit = { path = "./deltachat-ratelimit" }
anyhow = { workspace = true } anyhow = "1"
async-broadcast = "0.7.0" async-channel = "1.8.0"
async-channel = "2.2.1" async-imap = { git = "https://github.com/async-email/async-imap", branch = "master", default-features = false, features = ["runtime-tokio"] }
async-imap = { version = "0.9.7", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.5", 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-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" backtrace = "0.3"
base64 = "0.22" base64 = "0.21"
brotli = { version = "6", default-features=false, features = ["std"] } bitflags = "1.3"
chrono = { workspace = true } chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" } email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" } encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
escaper = "0.1" escaper = "0.1"
fast-socks5 = "0.9" fast-socks5 = "0.8"
fd-lock = "4"
futures = "0.3" futures = "0.3"
futures-lite = "2.3.0" futures-lite = "1.12.0"
hex = "0.4.0" hex = "0.4.0"
hickory-resolver = "0.24"
humansize = "2" humansize = "2"
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] } image = { version = "0.24.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
iroh_old = { version = "0.4.2", default-features = false, package = "iroh"} # iroh = { version = "0.3.0", default-features = false }
iroh-net = { git = "https://github.com/link2xt/iroh", branch="link2xt/keep-connection" } iroh = { git = 'https://github.com/n0-computer/iroh', branch = "flub/ticket-multiple-addrs" }
iroh-gossip = { git = "https://github.com/link2xt/iroh", branch="link2xt/keep-connection", features = ["net"] } kamadak-exif = "0.5"
quinn = "0.10.0"
kamadak-exif = "0.5.3"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" } lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2" libc = "0.2"
mailparse = "0.15" mailparse = "0.14"
mime = "0.3.17" num_cpus = "1.15"
num_cpus = "1.16" num-derive = "0.3"
num-derive = "0.4"
num-traits = "0.2" num-traits = "0.2"
once_cell = { workspace = true } once_cell = "1.17.0"
percent-encoding = "2.3" percent-encoding = "2.2"
parking_lot = "0.12" parking_lot = "0.12"
pgp = { version = "0.11", default-features = false } pgp = { version = "0.9", default-features = false }
pretty_env_logger = { version = "0.5", optional = true } pretty_env_logger = { version = "0.4", optional = true }
qrcodegen = "1.7.0" qrcodegen = "1.7.0"
quick-xml = "0.31" quick-xml = "0.27"
quoted_printable = "0.5"
rand = "0.8" rand = "0.8"
regex = { workspace = true } regex = "1.7"
reqwest = { version = "0.11.27", features = ["json"] } reqwest = { version = "0.11.14", features = ["json"] }
rusqlite = { workspace = true, features = ["sqlcipher"] } rusqlite = { version = "0.28", features = ["sqlcipher"] }
rust-hsluv = "0.1" rust-hsluv = "0.1"
sanitize-filename = "0.5" sanitize-filename = "0.4"
serde_json = "1" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.10" sha-1 = "0.10"
sha2 = "0.10" sha2 = "0.10"
smallvec = "1.13.2" smallvec = "1"
strum = "0.26" strum = "0.24"
strum_macros = "0.26" strum_macros = "0.24"
tagger = "4.3.4" tagger = "4.3.4"
textwrap = "0.16.1" textwrap = "0.16.0"
thiserror = "1" thiserror = "1"
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] }
tokio-io-timeout = "1.2.0" tokio-io-timeout = "1.2.0"
tokio-stream = { version = "0.1.15", features = ["fs"] } tokio-stream = { version = "0.1.11", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio-util = "0.7.9" tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.8" toml = "0.7"
trust-dns-resolver = "0.22"
url = "2" url = "2"
uuid = { version = "1", features = ["serde", "v4"] } uuid = { version = "1", features = ["serde", "v4"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
# Pin OpenSSL to 3.1 releases.
# OpenSSL 3.2 has a regression tracked at <https://github.com/openssl/openssl/issues/23376>
# which results in broken `deltachat-rpc-server` binaries when cross-compiled using Zig toolchain.
# See <https://github.com/deltachat/deltachat-core-rust/issues/5206> for Delta Chat issue.
# According to <https://www.openssl.org/policies/releasestrat.html>
# 3.1 branch will be supported until 2025-03-14.
openssl-src = "~300.1"
tracing = "0.1.40"
[dev-dependencies] [dev-dependencies]
ansi_term = "0.12.0" ansi_term = "0.12.0"
anyhow = { version = "1", features = ["backtrace"] } # Enable `backtrace` feature in tests. criterion = { version = "0.4.0", features = ["async_tokio"] }
criterion = { version = "0.5.1", features = ["async_tokio"] } futures-lite = "1.12"
futures-lite = "2.3.0"
log = "0.4" log = "0.4"
pretty_env_logger = "0.5" pretty_env_logger = "0.4"
proptest = { version = "1", default-features = false, features = ["std"] } proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = "3" tempfile = "3"
testdir = "0.9.0" testdir = "0.7.2"
tokio = { version = "1.37.0", features = ["parking_lot", "rt-multi-thread", "macros"] } tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
pretty_assertions = "1.3.0"
[workspace] [workspace]
members = [ members = [
@@ -137,11 +113,14 @@ members = [
"deltachat-rpc-server", "deltachat-rpc-server",
"deltachat-ratelimit", "deltachat-ratelimit",
"deltachat-repl", "deltachat-repl",
"deltachat-time",
"format-flowed", "format-flowed",
"deltachat-contact-tools",
] ]
[[example]]
name = "simple"
path = "examples/simple.rs"
[[bench]] [[bench]]
name = "create_account" name = "create_account"
harness = false harness = false
@@ -170,13 +149,6 @@ harness = false
name = "send_events" name = "send_events"
harness = false harness = false
[workspace.dependencies]
anyhow = "1"
once_cell = "1.18.0"
regex = "1.10"
rusqlite = "0.31"
chrono = { version = "0.4.38", default-features=false, features = ["alloc", "clock", "std"] }
[features] [features]
default = ["vendored"] default = ["vendored"]
internals = [] internals = []

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 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 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 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, then You may include the notice in a location (such as a LICENSE

View File

@@ -1,19 +1,8 @@
<p align="center"> # Delta Chat Rust
<img alt="Delta Chat Logo" height="200px" src="https://raw.githubusercontent.com/deltachat/deltachat-pages/master/assets/blog/rust-delta.png">
</p>
<p align="center"> > Deltachat-core written in Rust
<a href="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml">
<img alt="Rust CI" src="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg">
</a>
<a href="https://deps.rs/repo/github/deltachat/deltachat-core-rust">
<img alt="dependency status" src="https://deps.rs/repo/github/deltachat/deltachat-core-rust/status.svg">
</a>
</p>
<p align="center"> [![Rust CI](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg)](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml)
The core library for Delta Chat, written in Rust
</p>
## Installing Rust and Cargo ## Installing Rust and Cargo
@@ -30,7 +19,7 @@ $ curl https://sh.rustup.rs -sSf | sh
Compile and run Delta Chat Core command line utility, using `cargo`: Compile and run Delta Chat Core command line utility, using `cargo`:
``` ```
$ cargo run -p deltachat-repl -- ~/deltachat-db $ RUST_LOG=deltachat_repl=info cargo run -p deltachat-repl -- ~/deltachat-db
``` ```
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist. where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
@@ -124,7 +113,7 @@ $ cargo build -p deltachat_ffi --release
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed - `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
- `RUST_LOG=async_imap=trace,async_smtp=trace`: enable IMAP and - `RUST_LOG=deltachat_repl=info,async_imap=trace,async_smtp=trace`: enable IMAP and
SMTP tracing in addition to info messages. SMTP tracing in addition to info messages.
### Expensive tests ### Expensive tests
@@ -178,8 +167,8 @@ Language bindings are available for:
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\] - **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
- **Node.js** - **Node.js**
- over cffi: \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\] - over cffi (legacy): \[[📂 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 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)\] - **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
- **Go** - **Go**
- over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\] - over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
@@ -195,7 +184,6 @@ or its language bindings:
- [Desktop](https://github.com/deltachat/deltachat-desktop) - [Desktop](https://github.com/deltachat/deltachat-desktop)
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/) - [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/) - [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
- [Ubuntu Touch](https://codeberg.org/lk108/deltatouch)
- several **Bots** - several **Bots**
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues. [^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.

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. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
3. add a link to compare previous with current version to the end of CHANGELOG.md:
`[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.2...v1.116.0`
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 ''`.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 121 KiB

After

Width:  |  Height:  |  Size: 112 KiB

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{black_box, criterion_group, criterion_main, Criterion};
use deltachat::contact::Contact; use deltachat::contact::Contact;
use deltachat::context::Context; use deltachat::context::Context;

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::path::PathBuf; use std::path::PathBuf;
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{black_box, criterion_group, criterion_main, Criterion};
@@ -9,8 +8,7 @@ async fn create_accounts(n: u32) {
let dir = tempdir().unwrap(); let dir = tempdir().unwrap();
let p: PathBuf = dir.path().join("accounts"); let p: PathBuf = dir.path().join("accounts");
let writable = true; let mut accounts = Accounts::new(p.clone()).await.unwrap();
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
for expected_id in 2..n { for expected_id in 2..n {
let id = accounts.add_account().await.unwrap(); let id = accounts.add_account().await.unwrap();

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::path::Path; use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::path::Path; use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::path::PathBuf; use std::path::PathBuf;
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::path::Path; use std::path::Path;
use criterion::{black_box, criterion_group, criterion_main, Criterion}; use criterion::{black_box, criterion_group, criterion_main, Criterion};

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use criterion::{criterion_group, criterion_main, Criterion}; use criterion::{criterion_group, criterion_main, Criterion};
use deltachat::context::Context; use deltachat::context::Context;

View File

@@ -1,93 +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://keats.github.io/tera/docs/#templates
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
footer = """
{% for release in releases -%}
{% if release.version -%}
{% if release.previous.version -%}
[{{ release.version | trim_start_matches(pat="v") }}]: \
https://github.com/deltachat/deltachat-core-rust\
/compare/{{ release.previous.version }}..{{ release.version }}
{% endif -%}
{% else -%}
[unreleased]: https://github.com/deltachat/deltachat-core-rust\
/compare/{{ release.previous.version }}..HEAD
{% endif -%}
{% endfor %}
"""

View File

@@ -1,18 +0,0 @@
[package]
name = "deltachat-contact-tools"
version = "0.0.0" # No semver-stable versioning
edition = "2021"
description = "Contact-related tools, like parsing vcards and sanitizing name and address. Meant for internal use in the deltachat crate."
license = "MPL-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
anyhow = { workspace = true }
once_cell = { workspace = true }
regex = { workspace = true }
rusqlite = { workspace = true } # Needed in order to `impl rusqlite::types::ToSql for EmailAddress`. Could easily be put behind a feature.
chrono = { workspace = true }
[dev-dependencies]
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.

View File

@@ -1,668 +0,0 @@
//! Contact-related tools, like parsing vcards and sanitizing name and address
#![forbid(unsafe_code)]
#![warn(
unused,
clippy::correctness,
missing_debug_implementations,
missing_docs,
clippy::all,
clippy::wildcard_imports,
clippy::needless_borrow,
clippy::cast_lossless,
clippy::unused_async,
clippy::explicit_iter_loop,
clippy::explicit_into_iter_loop,
clippy::cloned_instead_of_copied
)]
#![cfg_attr(not(test), warn(clippy::indexing_slicing))]
#![allow(
clippy::match_bool,
clippy::mixed_read_write_in_expression,
clippy::bool_assert_comparison,
clippy::manual_split_once,
clippy::format_push_string,
clippy::bool_to_int_with_if
)]
use std::fmt;
use std::ops::Deref;
use anyhow::bail;
use anyhow::Context as _;
use anyhow::Result;
use chrono::{DateTime, NaiveDateTime};
use once_cell::sync::Lazy;
use regex::Regex;
// TODOs to clean up:
// - Check if sanitizing is done correctly everywhere
// - Apply lints everywhere (https://doc.rust-lang.org/cargo/reference/workspaces.html#the-lints-table)
#[derive(Debug)]
/// A Contact, as represented in a VCard.
pub struct VcardContact {
/// The email address, vcard property `email`
pub addr: String,
/// This must be the name authorized by the contact itself, not a locally given name. Vcard
/// property `fn`. Can be empty, one should use `display_name()` to obtain the display name.
pub authname: String,
/// The contact's public PGP key in Base64, vcard property `key`
pub key: Option<String>,
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
pub profile_image: Option<String>,
/// The timestamp when the vcard was created / last updated, vcard property `rev`
pub timestamp: Result<i64>,
}
impl VcardContact {
/// Returns the contact's display name.
pub fn display_name(&self) -> &str {
match self.authname.is_empty() {
false => &self.authname,
true => &self.addr,
}
}
}
/// Returns a vCard containing given contacts.
///
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
pub fn make_vcard(contacts: &[VcardContact]) -> String {
fn format_timestamp(c: &VcardContact) -> Option<String> {
let timestamp = *c.timestamp.as_ref().ok()?;
let datetime = DateTime::from_timestamp(timestamp, 0)?;
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
}
let mut res = "".to_string();
for c in contacts {
let addr = &c.addr;
let display_name = c.display_name();
res += &format!(
"BEGIN:VCARD\n\
VERSION:4.0\n\
EMAIL:{addr}\n\
FN:{display_name}\n"
);
if let Some(key) = &c.key {
res += &format!("KEY:data:application/pgp-keys;base64,{key}\n");
}
if let Some(profile_image) = &c.profile_image {
res += &format!("PHOTO:data:image/jpeg;base64,{profile_image}\n");
}
if let Some(timestamp) = format_timestamp(c) {
res += &format!("REV:{timestamp}\n");
}
res += "END:VCARD\n";
}
res
}
/// Parses `VcardContact`s from a given `&str`.
pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
fn remove_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
let start_of_s = s.get(..prefix.len())?;
if start_of_s.eq_ignore_ascii_case(prefix) {
s.get(prefix.len()..)
} else {
None
}
}
fn vcard_property<'a>(s: &'a str, property: &str) -> Option<&'a str> {
let remainder = remove_prefix(s, property)?;
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
// then `remainder` is now `;TYPE=work:alice@example.com`
// TODO this doesn't handle the case where there are quotes around a colon
let (params, value) = remainder.split_once(':')?;
// In the example from above, `params` is now `;TYPE=work`
// and `value` is now `alice@example.com`
if params
.chars()
.next()
.filter(|c| !c.is_ascii_punctuation() || *c == '_')
.is_some()
{
// `s` started with `property`, but the next character after it was not punctuation,
// so this line's property is actually something else
return None;
}
Some(value)
}
fn parse_datetime(datetime: &str) -> Result<i64> {
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
// is in ISO.8601.2004 format. DateTime::parse_from_rfc3339() apparently parses
// ISO.8601, but fails to parse any of the examples given.
// So, instead just parse using a format string.
// Parses 19961022T140000Z, 19961022T140000-05, or 19961022T140000-0500.
let timestamp = match DateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S%#z") {
Ok(datetime) => datetime.timestamp(),
// Parses 19961022T140000.
Err(e) => match NaiveDateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S") {
Ok(datetime) => datetime
.and_local_timezone(chrono::offset::Local)
.single()
.context("Could not apply local timezone to parsed date and time")?
.timestamp(),
Err(_) => return Err(e.into()),
},
};
Ok(timestamp)
}
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
static NEWLINE_AND_SPACE_OR_TAB: Lazy<Regex> = Lazy::new(|| Regex::new("\n[\t ]").unwrap());
let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, "");
let mut lines = unfolded_lines.lines().peekable();
let mut contacts = Vec::new();
while lines.peek().is_some() {
// Skip to the start of the vcard:
for line in lines.by_ref() {
if line.eq_ignore_ascii_case("BEGIN:VCARD") {
break;
}
}
let mut display_name = None;
let mut addr = None;
let mut key = None;
let mut photo = None;
let mut datetime = None;
for line in lines.by_ref() {
if let Some(email) = vcard_property(line, "email") {
addr.get_or_insert(email);
} else if let Some(name) = vcard_property(line, "fn") {
display_name.get_or_insert(name);
} else if let Some(k) = remove_prefix(line, "KEY;PGP;ENCODING=BASE64:")
.or_else(|| remove_prefix(line, "KEY;TYPE=PGP;ENCODING=b:"))
.or_else(|| remove_prefix(line, "KEY:data:application/pgp-keys;base64,"))
{
key.get_or_insert(k);
} else if let Some(p) = remove_prefix(line, "PHOTO;JPEG;ENCODING=BASE64:")
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;JPEG:"))
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=b:"))
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=b;TYPE=JPEG:"))
.or_else(|| remove_prefix(line, "PHOTO;ENCODING=BASE64;TYPE=JPEG:"))
.or_else(|| remove_prefix(line, "PHOTO;TYPE=JPEG;ENCODING=BASE64:"))
.or_else(|| remove_prefix(line, "PHOTO:data:image/jpeg;base64,"))
{
photo.get_or_insert(p);
} else if let Some(rev) = vcard_property(line, "rev") {
datetime.get_or_insert(rev);
} else if line.eq_ignore_ascii_case("END:VCARD") {
break;
}
}
let (authname, addr) =
sanitize_name_and_addr(display_name.unwrap_or(""), addr.unwrap_or(""));
contacts.push(VcardContact {
authname,
addr,
key: key.map(|s| s.to_string()),
profile_image: photo.map(|s| s.to_string()),
timestamp: datetime
.context("No timestamp in vcard")
.and_then(parse_datetime),
});
}
contacts
}
/// Valid contact address.
#[derive(Debug, Clone)]
pub struct ContactAddress(String);
impl Deref for ContactAddress {
type Target = str;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl AsRef<str> for ContactAddress {
fn as_ref(&self) -> &str {
&self.0
}
}
impl fmt::Display for ContactAddress {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl ContactAddress {
/// Constructs a new contact address from string,
/// normalizing and validating it.
pub fn new(s: &str) -> Result<Self> {
let addr = addr_normalize(s);
if !may_be_valid_addr(&addr) {
bail!("invalid address {:?}", s);
}
Ok(Self(addr.to_string()))
}
}
/// Allow converting [`ContactAddress`] to an SQLite type.
impl rusqlite::types::ToSql for ContactAddress {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Text(self.0.to_string());
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
/// Make the name and address
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
static ADDR_WITH_NAME_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new("(.*)<(.*)>").unwrap());
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
(
if name.is_empty() {
strip_rtlo_characters(captures.get(1).map_or("", |m| m.as_str()))
} else {
strip_rtlo_characters(name)
},
captures
.get(2)
.map_or("".to_string(), |m| m.as_str().to_string()),
)
} else {
(
strip_rtlo_characters(&normalize_name(name)),
addr.to_string(),
)
};
let mut name = normalize_name(&name);
// If the 'display name' is just the address, remove it:
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)" (see `get_name_n_addr()`).
// If the display name is empty, DC will just show the address when it needs a display name.
if name == addr {
name = "".to_string();
}
(name, addr)
}
/// Normalize a name.
///
/// - Remove quotes (come from some bad MUA implementations)
/// - Trims the resulting string
///
/// Typically, this function is not needed as it is called implicitly by `Contact::add_address_book`.
pub fn normalize_name(full_name: &str) -> String {
let full_name = full_name.trim();
if full_name.is_empty() {
return full_name.into();
}
match full_name.as_bytes() {
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => full_name
.get(1..full_name.len() - 1)
.map_or("".to_string(), |s| s.trim().to_string()),
_ => full_name.to_string(),
}
}
const RTLO_CHARACTERS: [char; 5] = ['\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}'];
/// This method strips all occurrences of the RTLO Unicode character.
/// [Why is this needed](https://github.com/deltachat/deltachat-core-rust/issues/3479)?
pub fn strip_rtlo_characters(input_str: &str) -> String {
input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "")
}
/// Returns false if addr is an invalid address, otherwise true.
pub fn may_be_valid_addr(addr: &str) -> bool {
let res = EmailAddress::new(addr);
res.is_ok()
}
/// Returns address lowercased,
/// with whitespace trimmed and `mailto:` prefix removed.
pub fn addr_normalize(addr: &str) -> String {
let norm = addr.trim().to_lowercase();
if norm.starts_with("mailto:") {
norm.get(7..).unwrap_or(&norm).to_string()
} else {
norm
}
}
/// Compares two email addresses, normalizing them beforehand.
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
let norm1 = addr_normalize(addr1);
let norm2 = addr_normalize(addr2);
norm1 == norm2
}
///
/// Represents an email address, right now just the `name@domain` portion.
///
/// # Example
///
/// ```
/// use deltachat_contact_tools::EmailAddress;
/// let email = match EmailAddress::new("someone@example.com") {
/// Ok(addr) => addr,
/// Err(e) => panic!("Error parsing address, error was {}", e),
/// };
/// assert_eq!(&email.local, "someone");
/// assert_eq!(&email.domain, "example.com");
/// assert_eq!(email.to_string(), "someone@example.com");
/// ```
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct EmailAddress {
/// Local part of the email address.
pub local: String,
/// Email address domain.
pub domain: String,
}
impl fmt::Display for EmailAddress {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}@{}", self.local, self.domain)
}
}
impl EmailAddress {
/// Performs a dead-simple parse of an email address.
pub fn new(input: &str) -> Result<EmailAddress> {
if input.is_empty() {
bail!("empty string is not valid");
}
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
if input
.chars()
.any(|c| c.is_whitespace() || c == '<' || c == '>')
{
bail!("Email {:?} must not contain whitespaces, '>' or '<'", input);
}
match &parts[..] {
[domain, local] => {
if local.is_empty() {
bail!("empty string is not valid for local part in {:?}", input);
}
if domain.is_empty() {
bail!("missing domain after '@' in {:?}", input);
}
if domain.ends_with('.') {
bail!("Domain {domain:?} should not contain the dot in the end");
}
Ok(EmailAddress {
local: (*local).to_string(),
domain: (*domain).to_string(),
})
}
_ => bail!("Email {:?} must contain '@' character", input),
}
}
}
impl rusqlite::types::ToSql for EmailAddress {
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput> {
let val = rusqlite::types::Value::Text(self.to_string());
let out = rusqlite::types::ToSqlOutput::Owned(val);
Ok(out)
}
}
#[cfg(test)]
mod tests {
use chrono::TimeZone;
use super::*;
#[test]
fn test_vcard_thunderbird() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:4.0
FN:'Alice Mueller'
EMAIL;PREF=1:alice.mueller@posteo.de
UID:a8083264-ca47-4be7-98a8-8ec3db1447ca
END:VCARD
BEGIN:VCARD
VERSION:4.0
FN:'bobzzz@freenet.de'
EMAIL;PREF=1:bobzzz@freenet.de
UID:cac4fef4-6351-4854-bbe4-9b6df857eaed
END:VCARD
",
);
assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string());
assert_eq!(contacts[0].authname, "Alice Mueller".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image, None);
assert!(contacts[0].timestamp.is_err());
assert_eq!(contacts[1].addr, "bobzzz@freenet.de".to_string());
assert_eq!(contacts[1].authname, "".to_string());
assert_eq!(contacts[1].key, None);
assert_eq!(contacts[1].profile_image, None);
assert!(contacts[1].timestamp.is_err());
assert_eq!(contacts.len(), 2);
}
#[test]
fn test_vcard_simple_example() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:4.0
FN:Alice Wonderland
N:Wonderland;Alice;;;Ms.
GENDER:W
EMAIL;TYPE=work:alice@example.com
KEY;TYPE=PGP;ENCODING=b:[base64-data]
REV:20240418T184242Z
END:VCARD",
);
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
assert_eq!(contacts[0].key, Some("[base64-data]".to_string()));
assert_eq!(contacts[0].profile_image, None);
assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762);
assert_eq!(contacts.len(), 1);
}
#[test]
fn test_make_and_parse_vcard() {
let contacts = [
VcardContact {
addr: "alice@example.org".to_string(),
authname: "Alice Wonderland".to_string(),
key: Some("[base64-data]".to_string()),
profile_image: Some("image in Base64".to_string()),
timestamp: Ok(1713465762),
},
VcardContact {
addr: "bob@example.com".to_string(),
authname: "".to_string(),
key: None,
profile_image: None,
timestamp: Ok(0),
},
];
let items = [
"BEGIN:VCARD\n\
VERSION:4.0\n\
EMAIL:alice@example.org\n\
FN:Alice Wonderland\n\
KEY:data:application/pgp-keys;base64,[base64-data]\n\
PHOTO:data:image/jpeg;base64,image in Base64\n\
REV:20240418T184242Z\n\
END:VCARD\n",
"BEGIN:VCARD\n\
VERSION:4.0\n\
EMAIL:bob@example.com\n\
FN:bob@example.com\n\
REV:19700101T000000Z\n\
END:VCARD\n",
];
let mut expected = "".to_string();
for len in 0..=contacts.len() {
let contacts = &contacts[0..len];
let vcard = make_vcard(contacts);
if len > 0 {
expected += items[len - 1];
}
assert_eq!(vcard, expected);
let parsed = parse_vcard(&vcard);
assert_eq!(parsed.len(), contacts.len());
for i in 0..parsed.len() {
assert_eq!(parsed[i].addr, contacts[i].addr);
assert_eq!(parsed[i].authname, contacts[i].authname);
assert_eq!(parsed[i].key, contacts[i].key);
assert_eq!(parsed[i].profile_image, contacts[i].profile_image);
assert_eq!(
parsed[i].timestamp.as_ref().unwrap(),
contacts[i].timestamp.as_ref().unwrap()
);
}
}
}
#[test]
fn test_contact_address() -> Result<()> {
let alice_addr = "alice@example.org";
let contact_address = ContactAddress::new(alice_addr)?;
assert_eq!(contact_address.as_ref(), alice_addr);
let invalid_addr = "<> foobar";
assert!(ContactAddress::new(invalid_addr).is_err());
Ok(())
}
#[test]
fn test_emailaddress_parse() {
assert_eq!(EmailAddress::new("").is_ok(), false);
assert_eq!(
EmailAddress::new("user@domain.tld").unwrap(),
EmailAddress {
local: "user".into(),
domain: "domain.tld".into(),
}
);
assert_eq!(
EmailAddress::new("user@localhost").unwrap(),
EmailAddress {
local: "user".into(),
domain: "localhost".into()
}
);
assert_eq!(EmailAddress::new("uuu").is_ok(), false);
assert_eq!(EmailAddress::new("dd.tt").is_ok(), false);
assert!(EmailAddress::new("tt.dd@uu").is_ok());
assert!(EmailAddress::new("u@d").is_ok());
assert!(EmailAddress::new("u@d.").is_err());
assert!(EmailAddress::new("u@d.t").is_ok());
assert_eq!(
EmailAddress::new("u@d.tt").unwrap(),
EmailAddress {
local: "u".into(),
domain: "d.tt".into(),
}
);
assert!(EmailAddress::new("u@tt").is_ok());
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
}
#[test]
fn test_vcard_android() {
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:2.1
N:;Bob;;;
FN:Bob
TEL;CELL:+1-234-567-890
EMAIL;HOME:bob@example.org
END:VCARD
BEGIN:VCARD
VERSION:2.1
N:;Alice;;;
FN:Alice
EMAIL;HOME:alice@example.org
END:VCARD
",
);
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image, None);
assert_eq!(contacts[1].addr, "alice@example.org".to_string());
assert_eq!(contacts[1].authname, "Alice".to_string());
assert_eq!(contacts[1].key, None);
assert_eq!(contacts[1].profile_image, None);
assert_eq!(contacts.len(), 2);
}
#[test]
fn test_vcard_local_datetime() {
let contacts = parse_vcard(
"BEGIN:VCARD\n\
VERSION:4.0\n\
FN:Alice Wonderland\n\
EMAIL;TYPE=work:alice@example.org\n\
REV:20240418T184242\n\
END:VCARD",
);
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0].addr, "alice@example.org".to_string());
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
assert_eq!(
*contacts[0].timestamp.as_ref().unwrap(),
chrono::offset::Local
.with_ymd_and_hms(2024, 4, 18, 18, 42, 42)
.unwrap()
.timestamp()
);
}
#[test]
fn test_android_vcard_with_base64_avatar() {
// This is not an actual base64-encoded avatar, it's just to test the parsing
let contacts = parse_vcard(
"BEGIN:VCARD
VERSION:2.1
N:;Bob;;;
FN:Bob
EMAIL;HOME:bob@example.org
PHOTO;ENCODING=BASE64;JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEU
AAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAA
L8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==
END:VCARD
",
);
assert_eq!(contacts.len(), 1);
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
assert_eq!(contacts[0].authname, "Bob".to_string());
assert_eq!(contacts[0].key, None);
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
}
}

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "deltachat_ffi" name = "deltachat_ffi"
version = "1.138.5" version = "1.111.0"
description = "Deltachat FFI" description = "Deltachat FFI"
edition = "2018" edition = "2018"
readme = "README.md" readme = "README.md"
@@ -17,15 +17,14 @@ crate-type = ["cdylib", "staticlib"]
deltachat = { path = "../", default-features = false } deltachat = { path = "../", default-features = false }
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true } deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
libc = "0.2" libc = "0.2"
human-panic = { version = "2", default-features = false } human-panic = { version = "1", default-features = false }
num-traits = "0.2" num-traits = "0.2"
serde_json = "1.0" serde_json = "1.0"
tokio = { version = "1.37.0", features = ["rt-multi-thread"] } tokio = { version = "1", features = ["rt-multi-thread"] }
anyhow = "1" anyhow = "1"
thiserror = "1" thiserror = "1"
rand = "0.8" rand = "0.7"
once_cell = "1.18.0" once_cell = "1.17.0"
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
[features] [features]
default = ["vendored"] default = ["vendored"]

View File

@@ -846,7 +846,7 @@ EXCLUDE_PATTERNS =
# exclude all test directories use the pattern */test/* # exclude all test directories use the pattern */test/*
###################################################### ######################################################
EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_loginparam_t dc_mime*_t EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_keyring_t dc_loginparam_t dc_mime*_t
EXCLUDE_SYMBOLS += dc_saxparser_t dc_simplify_t dc_smtp_t dc_sqlite3_t dc_strbuilder_t dc_param_t dc_hash_t dc_hashelem_t EXCLUDE_SYMBOLS += dc_saxparser_t dc_simplify_t dc_smtp_t dc_sqlite3_t dc_strbuilder_t dc_param_t dc_hash_t dc_hashelem_t
EXCLUDE_SYMBOLS += _dc_* jsmn* EXCLUDE_SYMBOLS += _dc_* jsmn*
###################################################### ######################################################

View File

@@ -9,7 +9,7 @@
<tab type="hierarchy" visible="no" title="" intro=""/> <tab type="hierarchy" visible="no" title="" intro=""/>
<tab type="classmembers" visible="no" title="" intro=""/> <tab type="classmembers" visible="no" title="" intro=""/>
</tab> </tab>
<tab type="topics" visible="yes" title="Constants" intro="Here is a list of constants:"/> <tab type="modules" visible="yes" title="Constants" intro="Here is a list of constants:"/>
<tab type="pages" visible="yes" title="" intro=""/> <tab type="pages" visible="yes" title="" intro=""/>
<tab type="namespaces" visible="yes" title=""> <tab type="namespaces" visible="yes" title="">
<tab type="namespacelist" visible="yes" title="" intro=""/> <tab type="namespacelist" visible="yes" title="" intro=""/>

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -1,11 +1,10 @@
[package] [package]
name = "deltachat-jsonrpc" name = "deltachat-jsonrpc"
version = "1.138.5" version = "1.111.0"
description = "DeltaChat JSON-RPC API" description = "DeltaChat JSON-RPC API"
edition = "2021" edition = "2021"
default-run = "deltachat-jsonrpc-server" default-run = "deltachat-jsonrpc-server"
license = "MPL-2.0" license = "MPL-2.0"
repository = "https://github.com/deltachat/deltachat-core-rust"
[[bin]] [[bin]]
name = "deltachat-jsonrpc-server" name = "deltachat-jsonrpc-server"
@@ -15,28 +14,26 @@ required-features = ["webserver"]
[dependencies] [dependencies]
anyhow = "1" anyhow = "1"
deltachat = { path = ".." } deltachat = { path = ".." }
deltachat-contact-tools = { path = "../deltachat-contact-tools" }
num-traits = "0.2" num-traits = "0.2"
schemars = "0.8.19"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tempfile = "3.10.1" tempfile = "3.3.0"
log = "0.4" log = "0.4"
async-channel = { version = "2.2.1" } async-channel = { version = "1.8.0" }
futures = { version = "0.3.30" } futures = { version = "0.3.26" }
serde_json = "1" serde_json = "1.0.91"
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] } yerpc = { version = "0.4.3", features = ["anyhow_expose"] }
typescript-type-def = { version = "0.5.8", features = ["json_value"] } typescript-type-def = { version = "0.5.5", features = ["json_value"] }
tokio = { version = "1.37.0" } tokio = { version = "1.25.0" }
sanitize-filename = "0.5" sanitize-filename = "0.4"
walkdir = "2.5.0" walkdir = "2.3.2"
base64 = "0.22" base64 = "0.21"
# optional dependencies # optional dependencies
axum = { version = "0.7", optional = true, features = ["ws"] } axum = { version = "0.6.11", optional = true, features = ["ws"] }
env_logger = { version = "0.11.3", optional = true } env_logger = { version = "0.10.0", optional = true }
[dev-dependencies] [dev-dependencies]
tokio = { version = "1.37.0", features = ["full", "rt-multi-thread"] } tokio = { version = "1.25.0", features = ["full", "rt-multi-thread"] }
[features] [features]

View File

@@ -108,10 +108,10 @@ This will build the `deltachat-jsonrpc-server` binary and then run a test suite
The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, talk to DeltaChat developers to get a token for the `testrun.org` service, or use a local instance of [`mailadm`](https://github.com/deltachat/docker-mailadm). The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, talk to DeltaChat developers to get a token for the `testrun.org` service, or use a local instance of [`mailadm`](https://github.com/deltachat/docker-mailadm).
Then, set the `CHATMAIL_DOMAIN` environment variable to your testing email server domain. Then, set the `DCC_NEW_TMP_EMAIL` environment variable to your mailadm token before running the tests.
``` ```
CHATMAIL_DOMAIN=chat.example.org npm run test DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=yourtoken npm run test
``` ```
#### Test Coverage #### Test Coverage

View File

@@ -1,64 +1,69 @@
use deltachat::{Event as CoreEvent, EventType as CoreEventType}; use deltachat::{Event, EventType};
use serde::Serialize; use serde::Serialize;
use serde_json::{json, Value};
use typescript_type_def::TypeDef; use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)] pub fn event_to_json_rpc_notification(event: Event) -> Value {
#[serde(rename_all = "camelCase")] let id: JSONRPCEventType = event.typ.into();
pub struct Event { json!({
/// Event payload. "event": id,
event: EventType, "contextId": event.id,
})
/// Account ID.
context_id: u32,
} }
impl From<CoreEvent> for Event { #[derive(Serialize, TypeDef)]
fn from(event: CoreEvent) -> Self { #[serde(tag = "type", rename = "Event")]
Event { pub enum JSONRPCEventType {
event: event.typ.into(),
context_id: event.id,
}
}
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(tag = "kind")]
pub enum EventType {
/// The library-user may write an informational string to the log. /// 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 /// This event should *not* be reported to the end-user using a popup or something like
/// that. /// that.
Info { msg: String }, Info {
msg: String,
},
/// Emitted when SMTP connection is established and login was successful. /// Emitted when SMTP connection is established and login was successful.
SmtpConnected { msg: String }, SmtpConnected {
msg: String,
},
/// Emitted when IMAP connection is established and login was successful. /// Emitted when IMAP connection is established and login was successful.
ImapConnected { msg: String }, ImapConnected {
msg: String,
},
/// Emitted when a message was successfully sent to the SMTP server. /// Emitted when a message was successfully sent to the SMTP server.
SmtpMessageSent { msg: String }, SmtpMessageSent {
msg: String,
},
/// Emitted when an IMAP message has been marked as deleted /// Emitted when an IMAP message has been marked as deleted
ImapMessageDeleted { msg: String }, ImapMessageDeleted {
msg: String,
},
/// Emitted when an IMAP message has been moved /// Emitted when an IMAP message has been moved
ImapMessageMoved { msg: String }, ImapMessageMoved {
msg: String,
/// Emitted before going into IDLE on the Inbox folder. },
ImapInboxIdle,
/// Emitted when an new file in the $BLOBDIR was created /// Emitted when an new file in the $BLOBDIR was created
NewBlobFile { file: String }, NewBlobFile {
file: String,
},
/// Emitted when an file in the $BLOBDIR was deleted /// Emitted when an file in the $BLOBDIR was deleted
DeletedBlobFile { file: String }, DeletedBlobFile {
file: String,
},
/// The library-user should write a warning string to the log. /// The library-user should write a warning string to the log.
/// ///
/// This event should *not* be reported to the end-user using a popup or something like /// This event should *not* be reported to the end-user using a popup or something like
/// that. /// that.
Warning { msg: String }, Warning {
msg: String,
},
/// The library-user should report an error to the end-user. /// The library-user should report an error to the end-user.
/// ///
@@ -70,14 +75,18 @@ pub enum EventType {
/// it might be better to delay showing these events until the function has really /// it might be better to delay showing these events until the function has really
/// failed (returned false). It should be sufficient to report only the *last* error /// failed (returned false). It should be sufficient to report only the *last* error
/// in a messasge box then. /// in a messasge box then.
Error { msg: String }, Error {
msg: String,
},
/// An action cannot be performed because the user is not in the group. /// An action cannot be performed because the user is not in the group.
/// Reported eg. after a call to /// Reported eg. after a call to
/// setChatName(), setChatProfileImage(), /// setChatName(), setChatProfileImage(),
/// addContactToChat(), removeContactFromChat(), /// addContactToChat(), removeContactFromChat(),
/// and messages sending functions. /// and messages sending functions.
ErrorSelfNotInGroup { msg: String }, ErrorSelfNotInGroup {
msg: String,
},
/// Messages or chats changed. One or more messages or chats changed for various /// Messages or chats changed. One or more messages or chats changed for various
/// reasons in the database: /// reasons in the database:
@@ -88,7 +97,10 @@ pub enum EventType {
/// `chatId` is set if only a single chat is affected by the changes, otherwise 0. /// `chatId` is set if only a single chat is affected by the changes, otherwise 0.
/// `msgId` is set if only a single message is affected by the changes, otherwise 0. /// `msgId` is set if only a single message is affected by the changes, otherwise 0.
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
MsgsChanged { chat_id: u32, msg_id: u32 }, MsgsChanged {
chat_id: u32,
msg_id: u32,
},
/// Reactions for the message changed. /// Reactions for the message changed.
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@@ -101,39 +113,53 @@ pub enum EventType {
/// There is a fresh message. Typically, the user will show an notification /// There is a fresh message. Typically, the user will show an notification
/// when receiving this message. /// when receiving this message.
/// ///
/// There is no extra #DC_EVENT_MSGS_CHANGED event sent together with this event. /// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
IncomingMsg { chat_id: u32, msg_id: u32 }, IncomingMsg {
chat_id: u32,
msg_id: u32,
},
/// Downloading a bunch of messages just finished. This is an /// Downloading a bunch of messages just finished. This is an experimental
/// event to allow the UI to only show one notification per message bunch, /// event to allow the UI to only show one notification per message bunch,
/// instead of cluttering the user with many notifications. /// instead of cluttering the user with many notifications.
///
/// msg_ids contains the message ids.
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
IncomingMsgBunch, IncomingMsgBunch {
msg_ids: Vec<u32>,
},
/// Messages were seen or noticed. /// Messages were seen or noticed.
/// chat id is always set. /// chat id is always set.
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
MsgsNoticed { chat_id: u32 }, MsgsNoticed {
chat_id: u32,
},
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to /// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
/// DC_STATE_OUT_DELIVERED, see `Message.state`. /// DC_STATE_OUT_DELIVERED, see `Message.state`.
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
MsgDelivered { chat_id: u32, msg_id: u32 }, MsgDelivered {
chat_id: u32,
msg_id: u32,
},
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to /// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_FAILED, see `Message.state`. /// DC_STATE_OUT_FAILED, see `Message.state`.
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
MsgFailed { chat_id: u32, msg_id: u32 }, MsgFailed {
chat_id: u32,
msg_id: u32,
},
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to /// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
/// DC_STATE_OUT_MDN_RCVD, see `Message.state`. /// DC_STATE_OUT_MDN_RCVD, see `Message.state`.
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
MsgRead { chat_id: u32, msg_id: u32 }, MsgRead {
chat_id: u32,
/// A single message is deleted. msg_id: u32,
#[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. /// 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. /// Or the verify state of a chat has changed.
@@ -143,17 +169,24 @@ pub enum EventType {
/// This event does not include ephemeral timer modification, which /// This event does not include ephemeral timer modification, which
/// is a separate event. /// is a separate event.
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
ChatModified { chat_id: u32 }, ChatModified {
chat_id: u32,
},
/// Chat ephemeral timer changed. /// Chat ephemeral timer changed.
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
ChatEphemeralTimerModified { chat_id: u32, timer: u32 }, ChatEphemeralTimerModified {
chat_id: u32,
timer: u32,
},
/// Contact(s) created, renamed, blocked or deleted. /// Contact(s) created, renamed, blocked or deleted.
/// ///
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected. /// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
ContactsChanged { contact_id: Option<u32> }, ContactsChanged {
contact_id: Option<u32>,
},
/// Location of one or more contact has changed. /// Location of one or more contact has changed.
/// ///
@@ -161,7 +194,9 @@ pub enum EventType {
/// If the locations of several contacts have been changed, /// If the locations of several contacts have been changed,
/// this parameter is set to `None`. /// this parameter is set to `None`.
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
LocationChanged { contact_id: Option<u32> }, LocationChanged {
contact_id: Option<u32>,
},
/// Inform about the configuration progress started by configure(). /// Inform about the configuration progress started by configure().
ConfigureProgress { ConfigureProgress {
@@ -179,7 +214,9 @@ pub enum EventType {
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done /// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
/// @param data2 0 /// @param data2 0
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
ImexProgress { progress: usize }, ImexProgress {
progress: usize,
},
/// A file has been exported. A file has been written by imex(). /// A file has been exported. A file has been written by imex().
/// This event may be sent multiple times by a single call to imex(). /// This event may be sent multiple times by a single call to imex().
@@ -189,7 +226,9 @@ pub enum EventType {
/// ///
/// @param data2 0 /// @param data2 0
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
ImexFileWritten { path: String }, ImexFileWritten {
path: String,
},
/// Progress information of a secure-join handshake from the view of the inviter /// Progress information of a secure-join handshake from the view of the inviter
/// (Alice, the person who shows the QR code). /// (Alice, the person who shows the QR code).
@@ -204,7 +243,10 @@ pub enum EventType {
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol. /// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
/// 1000=Protocol finished for this contact. /// 1000=Protocol finished for this contact.
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
SecurejoinInviterProgress { contact_id: u32, progress: usize }, SecurejoinInviterProgress {
contact_id: u32,
progress: usize,
},
/// Progress information of a secure-join handshake from the view of the joiner /// Progress information of a secure-join handshake from the view of the joiner
/// (Bob, the person who scans the QR code). /// (Bob, the person who scans the QR code).
@@ -215,7 +257,10 @@ pub enum EventType {
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself." /// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
/// (Bob has verified alice and waits until Alice does the same for him) /// (Bob has verified alice and waits until Alice does the same for him)
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
SecurejoinJoinerProgress { contact_id: u32, progress: usize }, SecurejoinJoinerProgress {
contact_id: u32,
progress: usize,
},
/// The connectivity to the server changed. /// The connectivity to the server changed.
/// This means that you should refresh the connectivity view /// This means that you should refresh the connectivity view
@@ -223,69 +268,41 @@ pub enum EventType {
/// getConnectivityHtml() for details. /// getConnectivityHtml() for details.
ConnectivityChanged, ConnectivityChanged,
/// Deprecated by `ConfigSynced`.
SelfavatarChanged, SelfavatarChanged,
/// A multi-device synced config value changed. Maybe the app needs to refresh smth. For
/// uniformity this is emitted on the source device too. The value isn't here, otherwise it
/// would be logged which might not be good for privacy.
ConfigSynced {
/// Configuration key.
key: String,
},
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
WebxdcStatusUpdate { WebxdcStatusUpdate {
msg_id: u32, msg_id: u32,
status_update_serial: u32, status_update_serial: u32,
}, },
/// Data received over an ephemeral peer channel.
#[serde(rename_all = "camelCase")]
WebxdcRealtimeData { msg_id: u32, data: Vec<u8> },
/// Inform that a message containing a webxdc instance has been deleted /// Inform that a message containing a webxdc instance has been deleted
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
WebxdcInstanceDeleted { msg_id: u32 }, WebxdcInstanceDeleted {
msg_id: u32,
/// Tells that the Background fetch was completed (or timed out). },
/// This event acts as a marker, when you reach this event you can be sure
/// that all events emitted during the background fetch were processed.
///
/// This event is only emitted by the account manager
AccountsBackgroundFetchDone,
/// Inform that set of chats or the order of the chats in the chatlist has changed.
///
/// Sometimes this is emitted together with `UIChatlistItemChanged`.
ChatlistChanged,
/// Inform that a single chat list item changed and needs to be rerendered.
/// If `chat_id` is set to None, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
#[serde(rename_all = "camelCase")]
ChatlistItemChanged { chat_id: Option<u32> },
} }
impl From<CoreEventType> for EventType { impl From<EventType> for JSONRPCEventType {
fn from(event: CoreEventType) -> Self { fn from(event: EventType) -> Self {
use EventType::*; use JSONRPCEventType::*;
match event { match event {
CoreEventType::Info(msg) => Info { msg }, EventType::Info(msg) => Info { msg },
CoreEventType::SmtpConnected(msg) => SmtpConnected { msg }, EventType::SmtpConnected(msg) => SmtpConnected { msg },
CoreEventType::ImapConnected(msg) => ImapConnected { msg }, EventType::ImapConnected(msg) => ImapConnected { msg },
CoreEventType::SmtpMessageSent(msg) => SmtpMessageSent { msg }, EventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
CoreEventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg }, EventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
CoreEventType::ImapMessageMoved(msg) => ImapMessageMoved { msg }, EventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
CoreEventType::ImapInboxIdle => ImapInboxIdle, EventType::NewBlobFile(file) => NewBlobFile { file },
CoreEventType::NewBlobFile(file) => NewBlobFile { file }, EventType::DeletedBlobFile(file) => DeletedBlobFile { file },
CoreEventType::DeletedBlobFile(file) => DeletedBlobFile { file }, EventType::Warning(msg) => Warning { msg },
CoreEventType::Warning(msg) => Warning { msg }, EventType::Error(msg) => Error { msg },
CoreEventType::Error(msg) => Error { msg }, EventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg },
CoreEventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg }, EventType::MsgsChanged { chat_id, msg_id } => MsgsChanged {
CoreEventType::MsgsChanged { chat_id, msg_id } => MsgsChanged {
chat_id: chat_id.to_u32(), chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(), msg_id: msg_id.to_u32(),
}, },
CoreEventType::ReactionsChanged { EventType::ReactionsChanged {
chat_id, chat_id,
msg_id, msg_id,
contact_id, contact_id,
@@ -294,90 +311,92 @@ impl From<CoreEventType> for EventType {
msg_id: msg_id.to_u32(), msg_id: msg_id.to_u32(),
contact_id: contact_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(), chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(), msg_id: msg_id.to_u32(),
}, },
CoreEventType::IncomingMsgBunch => IncomingMsgBunch, EventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
CoreEventType::MsgsNoticed(chat_id) => MsgsNoticed { msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
},
EventType::MsgsNoticed(chat_id) => MsgsNoticed {
chat_id: chat_id.to_u32(), 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(), chat_id: chat_id.to_u32(),
msg_id: msg_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(), chat_id: chat_id.to_u32(),
msg_id: msg_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(), chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(), msg_id: msg_id.to_u32(),
}, },
CoreEventType::MsgDeleted { chat_id, msg_id } => MsgDeleted { EventType::ChatModified(chat_id) => ChatModified {
chat_id: chat_id.to_u32(),
msg_id: msg_id.to_u32(),
},
CoreEventType::ChatModified(chat_id) => ChatModified {
chat_id: chat_id.to_u32(), chat_id: chat_id.to_u32(),
}, },
CoreEventType::ChatEphemeralTimerModified { chat_id, timer } => { EventType::ChatEphemeralTimerModified { chat_id, timer } => {
ChatEphemeralTimerModified { ChatEphemeralTimerModified {
chat_id: chat_id.to_u32(), chat_id: chat_id.to_u32(),
timer: timer.to_u32(), timer: timer.to_u32(),
} }
} }
CoreEventType::ContactsChanged(contact) => ContactsChanged { EventType::ContactsChanged(contact) => ContactsChanged {
contact_id: contact.map(|c| c.to_u32()), contact_id: contact.map(|c| c.to_u32()),
}, },
CoreEventType::LocationChanged(contact) => LocationChanged { EventType::LocationChanged(contact) => LocationChanged {
contact_id: contact.map(|c| c.to_u32()), contact_id: contact.map(|c| c.to_u32()),
}, },
CoreEventType::ConfigureProgress { progress, comment } => { EventType::ConfigureProgress { progress, comment } => {
ConfigureProgress { progress, comment } ConfigureProgress { progress, comment }
} }
CoreEventType::ImexProgress(progress) => ImexProgress { progress }, EventType::ImexProgress(progress) => ImexProgress { progress },
CoreEventType::ImexFileWritten(path) => ImexFileWritten { EventType::ImexFileWritten(path) => ImexFileWritten {
path: path.to_str().unwrap_or_default().to_owned(), path: path.to_str().unwrap_or_default().to_owned(),
}, },
CoreEventType::SecurejoinInviterProgress { EventType::SecurejoinInviterProgress {
contact_id, contact_id,
progress, progress,
} => SecurejoinInviterProgress { } => SecurejoinInviterProgress {
contact_id: contact_id.to_u32(), contact_id: contact_id.to_u32(),
progress, progress,
}, },
CoreEventType::SecurejoinJoinerProgress { EventType::SecurejoinJoinerProgress {
contact_id, contact_id,
progress, progress,
} => SecurejoinJoinerProgress { } => SecurejoinJoinerProgress {
contact_id: contact_id.to_u32(), contact_id: contact_id.to_u32(),
progress, progress,
}, },
CoreEventType::ConnectivityChanged => ConnectivityChanged, EventType::ConnectivityChanged => ConnectivityChanged,
CoreEventType::SelfavatarChanged => SelfavatarChanged, EventType::SelfavatarChanged => SelfavatarChanged,
CoreEventType::ConfigSynced { key } => ConfigSynced { EventType::WebxdcStatusUpdate {
key: key.to_string(),
},
CoreEventType::WebxdcStatusUpdate {
msg_id, msg_id,
status_update_serial, status_update_serial,
} => WebxdcStatusUpdate { } => WebxdcStatusUpdate {
msg_id: msg_id.to_u32(), msg_id: msg_id.to_u32(),
status_update_serial: status_update_serial.to_u32(), status_update_serial: status_update_serial.to_u32(),
}, },
CoreEventType::WebxdcRealtimeData { msg_id, data } => WebxdcRealtimeData { EventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
msg_id: msg_id.to_u32(),
data,
},
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
msg_id: msg_id.to_u32(), msg_id: msg_id.to_u32(),
}, },
CoreEventType::AccountsBackgroundFetchDone => AccountsBackgroundFetchDone,
CoreEventType::ChatlistItemChanged { chat_id } => ChatlistItemChanged {
chat_id: chat_id.map(|id| id.to_u32()),
},
CoreEventType::ChatlistChanged => ChatlistChanged,
} }
} }
} }
#[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

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

View File

@@ -1,6 +1,6 @@
use std::time::{Duration, SystemTime}; 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::{self, get_chat_contacts, ChatVisibility};
use deltachat::chat::{Chat, ChatId}; use deltachat::chat::{Chat, ChatId};
use deltachat::constants::Chattype; use deltachat::constants::Chattype;
@@ -13,22 +13,11 @@ use typescript_type_def::TypeDef;
use super::color_int_to_hex_string; use super::color_int_to_hex_string;
use super::contact::ContactObject; use super::contact::ContactObject;
#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct FullChat { pub struct FullChat {
id: u32, id: u32,
name: String, name: String,
/// True if the chat is protected.
///
/// UI should display a green checkmark
/// in the chat title,
/// in the chat profile title and
/// in the chatlist item
/// if chat protection is enabled.
/// UI should also display a green checkmark
/// in the contact profile
/// if 1:1 chat with this contact exists and is protected.
is_protected: bool, is_protected: bool,
profile_image: Option<String>, //BLOBS ? profile_image: Option<String>, //BLOBS ?
archived: bool, archived: bool,
@@ -42,7 +31,6 @@ pub struct FullChat {
fresh_message_counter: usize, fresh_message_counter: usize,
// is_group - please check over chat.type in frontend instead // is_group - please check over chat.type in frontend instead
is_contact_request: bool, is_contact_request: bool,
is_protection_broken: bool,
is_device_chat: bool, is_device_chat: bool,
self_in_group: bool, self_in_group: bool,
is_muted: bool, is_muted: bool,
@@ -65,9 +53,7 @@ impl FullChat {
contacts.push( contacts.push(
ContactObject::try_from_dc_contact( ContactObject::try_from_dc_contact(
context, context,
Contact::get_by_id(context, *contact_id) Contact::load_from_db(context, *contact_id).await?,
.await
.context("failed to load contact")?,
) )
.await?, .await?,
) )
@@ -85,10 +71,9 @@ impl FullChat {
let can_send = chat.can_send(context).await?; let can_send = chat.can_send(context).await?;
let was_seen_recently = if chat.get_type() == Chattype::Single { let was_seen_recently = if chat.get_type() == Chattype::Single {
match contact_ids.first() { match contact_ids.get(0) {
Some(contact) => Contact::get_by_id(context, *contact) Some(contact) => Contact::load_from_db(context, *contact)
.await .await?
.context("failed to load contact for was_seen_recently")?
.was_seen_recently(), .was_seen_recently(),
None => false, None => false,
} }
@@ -104,7 +89,10 @@ impl FullChat {
is_protected: chat.is_protected(), is_protected: chat.is_protected(),
profile_image, //BLOBS ? profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived, 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_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(), is_self_talk: chat.is_self_talk(),
contacts, contacts,
@@ -112,7 +100,6 @@ impl FullChat {
color, color,
fresh_message_counter, fresh_message_counter,
is_contact_request: chat.is_contact_request(), is_contact_request: chat.is_contact_request(),
is_protection_broken: chat.is_protection_broken(),
is_device_chat: chat.is_device_talk(), is_device_chat: chat.is_device_talk(),
self_in_group: contact_ids.contains(&ContactId::SELF), self_in_group: contact_ids.contains(&ContactId::SELF),
is_muted: chat.is_muted(), is_muted: chat.is_muted(),
@@ -134,22 +121,11 @@ impl FullChat {
/// - can_send /// - can_send
/// ///
/// used when you only need the basic metadata of a chat like type, name, profile picture /// 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")] #[serde(rename_all = "camelCase")]
pub struct BasicChat { pub struct BasicChat {
id: u32, id: u32,
name: String, name: String,
/// True if the chat is protected.
///
/// UI should display a green checkmark
/// in the chat title,
/// in the chat profile title and
/// in the chatlist item
/// if chat protection is enabled.
/// UI should also display a green checkmark
/// in the contact profile
/// if 1:1 chat with this contact exists and is protected.
is_protected: bool, is_protected: bool,
profile_image: Option<String>, //BLOBS ? profile_image: Option<String>, //BLOBS ?
archived: bool, archived: bool,
@@ -158,7 +134,6 @@ pub struct BasicChat {
is_self_talk: bool, is_self_talk: bool,
color: String, color: String,
is_contact_request: bool, is_contact_request: bool,
is_protection_broken: bool,
is_device_chat: bool, is_device_chat: bool,
is_muted: bool, is_muted: bool,
} }
@@ -180,24 +155,25 @@ impl BasicChat {
is_protected: chat.is_protected(), is_protected: chat.is_protected(),
profile_image, //BLOBS ? profile_image, //BLOBS ?
archived: chat.get_visibility() == chat::ChatVisibility::Archived, 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_unpromoted: chat.is_unpromoted(),
is_self_talk: chat.is_self_talk(), is_self_talk: chat.is_self_talk(),
color, color,
is_contact_request: chat.is_contact_request(), is_contact_request: chat.is_contact_request(),
is_protection_broken: chat.is_protection_broken(),
is_device_chat: chat.is_device_talk(), is_device_chat: chat.is_device_talk(),
is_muted: chat.is_muted(), is_muted: chat.is_muted(),
}) })
} }
} }
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)] #[derive(Clone, Serialize, Deserialize, TypeDef)]
#[serde(tag = "kind")]
pub enum MuteDuration { pub enum MuteDuration {
NotMuted, NotMuted,
Forever, Forever,
Until { duration: i64 }, Until(i64),
} }
impl MuteDuration { impl MuteDuration {
@@ -205,20 +181,20 @@ impl MuteDuration {
match self { match self {
MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted), MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted),
MuteDuration::Forever => Ok(chat::MuteDuration::Forever), MuteDuration::Forever => Ok(chat::MuteDuration::Forever),
MuteDuration::Until { duration } => { MuteDuration::Until(n) => {
if duration <= 0 { if n <= 0 {
bail!("failed to read mute duration") bail!("failed to read mute duration")
} }
Ok(SystemTime::now() Ok(SystemTime::now()
.checked_add(Duration::from_secs(duration as u64)) .checked_add(Duration::from_secs(n as u64))
.map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until)) .map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until))
} }
} }
} }
} }
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)] #[derive(Clone, Serialize, Deserialize, TypeDef)]
#[serde(rename = "ChatVisibility")] #[serde(rename = "ChatVisibility")]
pub enum JSONRPCChatVisibility { pub enum JSONRPCChatVisibility {
Normal, Normal,

View File

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

View File

@@ -1,12 +1,12 @@
use anyhow::Result; use anyhow::Result;
use deltachat::color; use deltachat::contact::VerifiedStatus;
use deltachat::context::Context; use deltachat::context::Context;
use serde::Serialize; use serde::Serialize;
use typescript_type_def::TypeDef; use typescript_type_def::TypeDef;
use super::color_int_to_hex_string; use super::color_int_to_hex_string;
#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[derive(Serialize, TypeDef)]
#[serde(rename = "Contact", rename_all = "camelCase")] #[serde(rename = "Contact", rename_all = "camelCase")]
pub struct ContactObject { pub struct ContactObject {
address: String, address: String,
@@ -19,36 +19,14 @@ pub struct ContactObject {
profile_image: Option<String>, // BLOBS profile_image: Option<String>, // BLOBS
name_and_addr: String, name_and_addr: String,
is_blocked: bool, is_blocked: bool,
/// True if the contact can be added to verified groups.
///
/// If this is true
/// UI should display green checkmark after the contact name
/// in contact list items,
/// in chat member list items
/// and in profiles if no chat with the contact exist.
is_verified: bool, is_verified: bool,
/// the address that verified this contact
/// True if the contact profile title should have a green checkmark. verifier_addr: Option<String>,
/// /// the id of the contact that verified this contact
/// This indicates whether 1:1 chat has a green checkmark
/// or will have a green checkmark if created.
is_profile_verified: bool,
/// The ID of the contact that verified this contact.
///
/// If this is present,
/// display a green checkmark and "Introduced by ..."
/// string followed by the verifier contact name and address
/// in the contact profile.
verifier_id: Option<u32>, verifier_id: Option<u32>,
/// the contact's last seen timestamp /// the contact's last seen timestamp
last_seen: i64, last_seen: i64,
was_seen_recently: bool, was_seen_recently: bool,
/// If the contact is a bot.
is_bot: bool,
} }
impl ContactObject { impl ContactObject {
@@ -60,13 +38,19 @@ impl ContactObject {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()), Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None, None => None,
}; };
let is_verified = contact.is_verified(context).await?; let is_verified = contact.is_verified(context).await? == VerifiedStatus::BidirectVerified;
let is_profile_verified = contact.is_profile_verified(context).await?;
let verifier_id = contact let (verifier_addr, verifier_id) = if is_verified {
.get_verifier_id(context) (
.await? contact.get_verifier_addr(context).await?,
.map(|contact_id| contact_id.to_u32()); contact
.get_verifier_id(context)
.await?
.map(|contact_id| contact_id.to_u32()),
)
} else {
(None, None)
};
Ok(ContactObject { Ok(ContactObject {
address: contact.get_addr().to_owned(), address: contact.get_addr().to_owned(),
@@ -80,43 +64,10 @@ impl ContactObject {
name_and_addr: contact.get_name_n_addr(), name_and_addr: contact.get_name_n_addr(),
is_blocked: contact.is_blocked(), is_blocked: contact.is_blocked(),
is_verified, is_verified,
is_profile_verified, verifier_addr,
verifier_id, verifier_id,
last_seen: contact.last_seen(), last_seen: contact.last_seen(),
was_seen_recently: contact.was_seen_recently(), was_seen_recently: contact.was_seen_recently(),
is_bot: contact.is_bot(),
}) })
} }
} }
#[derive(Clone, Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct VcardContact {
/// Email address.
addr: String,
/// The contact's name, or the email address if no name was given.
display_name: String,
/// Public PGP key in Base64.
key: Option<String>,
/// Profile image in Base64.
profile_image: Option<String>,
/// Contact color as hex string.
color: String,
/// Last update timestamp.
timestamp: Option<i64>,
}
impl From<deltachat_contact_tools::VcardContact> for VcardContact {
fn from(vc: deltachat_contact_tools::VcardContact) -> Self {
let display_name = vc.display_name().to_string();
let color = color::str_to_color(&vc.addr.to_lowercase());
Self {
addr: vc.addr,
display_name,
key: vc.key,
profile_image: vc.profile_image,
color: color_int_to_hex_string(color),
timestamp: vc.timestamp.ok(),
}
}
}

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 serde::Serialize;
use typescript_type_def::TypeDef; use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[derive(Serialize, TypeDef)]
#[serde(rename = "Location", rename_all = "camelCase")] #[serde(rename = "Location", rename_all = "camelCase")]
pub struct JsonrpcLocation { pub struct JsonrpcLocation {
pub location_id: u32, pub location_id: u32,

View File

@@ -1,8 +1,7 @@
use crate::api::VcardContact; use anyhow::{anyhow, Result};
use anyhow::{Context as _, Result};
use deltachat::chat::Chat; use deltachat::chat::Chat;
use deltachat::chat::ChatItem; use deltachat::chat::ChatItem;
use deltachat::chat::ChatVisibility; use deltachat::constants::Chattype;
use deltachat::contact::Contact; use deltachat::contact::Contact;
use deltachat::context::Context; use deltachat::context::Context;
use deltachat::download; use deltachat::download;
@@ -11,7 +10,8 @@ use deltachat::message::MsgId;
use deltachat::message::Viewtype; use deltachat::message::Viewtype;
use deltachat::reaction::get_msg_reactions; use deltachat::reaction::get_msg_reactions;
use num_traits::cast::ToPrimitive; use num_traits::cast::ToPrimitive;
use serde::{Deserialize, Serialize}; use serde::Deserialize;
use serde::Serialize;
use typescript_type_def::TypeDef; use typescript_type_def::TypeDef;
use super::color_int_to_hex_string; use super::color_int_to_hex_string;
@@ -19,14 +19,14 @@ use super::contact::ContactObject;
use super::reactions::JSONRPCReactions; use super::reactions::JSONRPCReactions;
use super::webxdc::WebxdcMessageInfo; use super::webxdc::WebxdcMessageInfo;
#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase", tag = "kind")] #[serde(rename_all = "camelCase", tag = "variant")]
pub enum MessageLoadResult { pub enum MessageLoadResult {
Message(MessageObject), Message(MessageObject),
LoadingError { error: String }, LoadingError { error: String },
} }
#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[derive(Serialize, TypeDef)]
#[serde(rename = "Message", rename_all = "camelCase")] #[serde(rename = "Message", rename_all = "camelCase")]
pub struct MessageObject { pub struct MessageObject {
id: u32, id: u32,
@@ -35,11 +35,7 @@ pub struct MessageObject {
quote: Option<MessageQuote>, quote: Option<MessageQuote>,
parent_id: Option<u32>, parent_id: Option<u32>,
text: String, text: Option<String>,
/// Check if a message has a POI location bound to it.
/// These locations are also returned by `get_locations` method.
/// The UI may decide to display a special icon beside such messages.
has_location: bool, has_location: bool,
has_html: bool, has_html: bool,
view_type: MessageViewtype, view_type: MessageViewtype,
@@ -88,11 +84,9 @@ pub struct MessageObject {
download_state: DownloadState, download_state: DownloadState,
reactions: Option<JSONRPCReactions>, reactions: Option<JSONRPCReactions>,
vcard_contact: Option<VcardContact>,
} }
#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[derive(Serialize, TypeDef)]
#[serde(tag = "kind")] #[serde(tag = "kind")]
enum MessageQuote { enum MessageQuote {
JustText { JustText {
@@ -112,15 +106,16 @@ enum MessageQuote {
} }
impl MessageObject { impl MessageObject {
pub async fn from_message_id(context: &Context, message_id: u32) -> Result<Self> {
let msg_id = MsgId::new(message_id);
Self::from_msg_id(context, msg_id).await
}
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> { pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?; let message = Message::load_from_db(context, msg_id).await?;
let sender_contact = Contact::get_by_id(context, message.get_from_id()) let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?;
.await let sender = ContactObject::try_from_dc_contact(context, sender_contact).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 file_bytes = message.get_filebytes(context).await?.unwrap_or_default(); let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
let override_sender_name = message.get_override_sender_name(); let override_sender_name = message.get_override_sender_name();
@@ -137,9 +132,7 @@ impl MessageObject {
let quote = if let Some(quoted_text) = message.quoted_text() { let quote = if let Some(quoted_text) = message.quoted_text() {
match message.quoted_message(context).await? { match message.quoted_message(context).await? {
Some(quote) => { Some(quote) => {
let quote_author = Contact::get_by_id(context, quote.get_from_id()) let quote_author = Contact::load_from_db(context, quote.get_from_id()).await?;
.await
.context("failed to load quote author contact")?;
Some(MessageQuote::WithMessage { Some(MessageQuote::WithMessage {
text: quoted_text, text: quoted_text,
message_id: quote.get_id().to_u32(), message_id: quote.get_id().to_u32(),
@@ -167,22 +160,13 @@ impl MessageObject {
None None
}; };
let reactions = get_msg_reactions(context, msg_id) let reactions = get_msg_reactions(context, msg_id).await?;
.await
.context("failed to load message reactions")?;
let reactions = if reactions.is_empty() { let reactions = if reactions.is_empty() {
None None
} else { } else {
Some(reactions.into()) Some(reactions.into())
}; };
let vcard_contacts: Vec<VcardContact> = message
.vcard_contacts(context)
.await?
.into_iter()
.map(Into::into)
.collect();
Ok(MessageObject { Ok(MessageObject {
id: msg_id.to_u32(), id: msg_id.to_u32(),
chat_id: message.get_chat_id().to_u32(), chat_id: message.get_chat_id().to_u32(),
@@ -196,7 +180,7 @@ impl MessageObject {
state: message state: message
.get_state() .get_state()
.to_u32() .to_u32()
.context("state conversion to number failed")?, .ok_or_else(|| anyhow!("state conversion to number failed"))?,
error: message.error(), error: message.error(),
timestamp: message.get_timestamp(), timestamp: message.get_timestamp(),
@@ -219,7 +203,7 @@ impl MessageObject {
videochat_type: match message.get_videochat_type() { videochat_type: match message.get_videochat_type() {
Some(vct) => Some( Some(vct) => Some(
vct.to_u32() vct.to_u32()
.context("videochat type conversion to number failed")?, .ok_or_else(|| anyhow!("state conversion to number failed"))?,
), ),
None => None, None => None,
}, },
@@ -242,13 +226,11 @@ impl MessageObject {
download_state, download_state,
reactions, reactions,
vcard_contact: vcard_contacts.first().cloned(),
}) })
} }
} }
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)] #[derive(Serialize, Deserialize, TypeDef)]
#[serde(rename = "Viewtype")] #[serde(rename = "Viewtype")]
pub enum MessageViewtype { pub enum MessageViewtype {
Unknown, Unknown,
@@ -286,11 +268,6 @@ pub enum MessageViewtype {
/// Message is an webxdc instance. /// Message is an webxdc instance.
Webxdc, Webxdc,
/// Message containing shared contacts represented as a vCard (virtual contact file)
/// with email addresses and possibly other fields.
/// Use `parse_vcard()` to retrieve them.
Vcard,
} }
impl From<Viewtype> for MessageViewtype { impl From<Viewtype> for MessageViewtype {
@@ -307,7 +284,6 @@ impl From<Viewtype> for MessageViewtype {
Viewtype::File => MessageViewtype::File, Viewtype::File => MessageViewtype::File,
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation, Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
Viewtype::Webxdc => MessageViewtype::Webxdc, Viewtype::Webxdc => MessageViewtype::Webxdc,
Viewtype::Vcard => MessageViewtype::Vcard,
} }
} }
} }
@@ -326,17 +302,15 @@ impl From<MessageViewtype> for Viewtype {
MessageViewtype::File => Viewtype::File, MessageViewtype::File => Viewtype::File,
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation, MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
MessageViewtype::Webxdc => Viewtype::Webxdc, MessageViewtype::Webxdc => Viewtype::Webxdc,
MessageViewtype::Vcard => Viewtype::Vcard,
} }
} }
} }
#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[derive(Serialize, TypeDef)]
pub enum DownloadState { pub enum DownloadState {
Done, Done,
Available, Available,
Failure, Failure,
Undecipherable,
InProgress, InProgress,
} }
@@ -346,13 +320,12 @@ impl From<download::DownloadState> for DownloadState {
download::DownloadState::Done => DownloadState::Done, download::DownloadState::Done => DownloadState::Done,
download::DownloadState::Available => DownloadState::Available, download::DownloadState::Available => DownloadState::Available,
download::DownloadState::Failure => DownloadState::Failure, download::DownloadState::Failure => DownloadState::Failure,
download::DownloadState::Undecipherable => DownloadState::Undecipherable,
download::DownloadState::InProgress => DownloadState::InProgress, download::DownloadState::InProgress => DownloadState::InProgress,
} }
} }
} }
#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[derive(Serialize, TypeDef)]
pub enum SystemMessageType { pub enum SystemMessageType {
Unknown, Unknown,
GroupNameChanged, GroupNameChanged,
@@ -363,15 +336,6 @@ pub enum SystemMessageType {
SecurejoinMessage, SecurejoinMessage,
LocationStreamingEnabled, LocationStreamingEnabled,
LocationOnly, LocationOnly,
InvalidUnencryptedMail,
/// 1:1 chats info message telling that SecureJoin has started and the user should wait for it
/// to complete.
SecurejoinWait,
/// 1:1 chats info message telling that SecureJoin is still running, but the user may already
/// send messages.
SecurejoinWaitTimeout,
/// Chat ephemeral message timer is changed. /// Chat ephemeral message timer is changed.
EphemeralTimerChanged, EphemeralTimerChanged,
@@ -391,9 +355,6 @@ pub enum SystemMessageType {
/// Webxdc info added with `info` set in `send_webxdc_status_update()`. /// Webxdc info added with `info` set in `send_webxdc_status_update()`.
WebxdcInfoMessage, WebxdcInfoMessage,
/// This message contains a users iroh node address.
IrohNodeAddr,
} }
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType { impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
@@ -415,15 +376,11 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync, SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate, SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage, SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
SystemMessage::InvalidUnencryptedMail => SystemMessageType::InvalidUnencryptedMail,
SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr,
SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait,
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
} }
} }
} }
#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct MessageNotificationInfo { pub struct MessageNotificationInfo {
id: u32, id: u32,
@@ -481,22 +438,14 @@ impl MessageNotificationInfo {
} }
} }
#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct MessageSearchResult { pub struct MessageSearchResult {
id: u32, id: u32,
author_profile_image: Option<String>, author_profile_image: Option<String>,
/// if sender name if overridden it will show it as ~alias
author_name: String, author_name: String,
author_color: String, author_color: String,
author_id: u32, chat_name: Option<String>,
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,
message: String, message: String,
timestamp: i64, timestamp: i64,
} }
@@ -505,44 +454,30 @@ impl MessageSearchResult {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> { pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?; let message = Message::load_from_db(context, msg_id).await?;
let chat = Chat::load_from_db(context, message.get_chat_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? { let profile_image = match sender.get_profile_image(context).await? {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()), Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
None => None, 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 { Ok(Self {
id: msg_id.to_u32(), id: msg_id.to_u32(),
author_profile_image: profile_image, 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_color: color_int_to_hex_string(sender.get_color()),
author_id: sender.id.to_u32(), chat_name: if chat.get_type() == Chattype::Single {
chat_name: chat.get_name().to_owned(), Some(chat.get_name().to_owned())
chat_color, } else {
chat_type: chat.get_type().to_u32().context("unknown chat type id")?, None
chat_profile_image, },
is_chat_protected: chat.is_protected(), message: message.get_text().unwrap_or_default(),
is_chat_contact_request: chat.is_contact_request(),
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
message: message.get_text(),
timestamp: message.get_timestamp(), timestamp: message.get_timestamp(),
}) })
} }
} }
#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")] #[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")]
pub enum JSONRPCMessageListItem { pub enum JSONRPCMessageListItem {
Message { Message {
@@ -568,7 +503,7 @@ impl From<ChatItem> for JSONRPCMessageListItem {
} }
} }
#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)] #[derive(Deserialize, TypeDef)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct MessageData { pub struct MessageData {
pub text: Option<String>, pub text: Option<String>,
@@ -579,116 +514,3 @@ pub struct MessageData {
pub override_sender_name: Option<String>, pub override_sender_name: Option<String>,
pub quoted_message_id: Option<u32>, pub quoted_message_id: Option<u32>,
} }
impl MessageData {
pub(crate) async fn create_message(self, context: &Context) -> Result<Message> {
let mut message = Message::new(if let Some(viewtype) = self.viewtype {
viewtype.into()
} else if self.file.is_some() {
Viewtype::File
} else {
Viewtype::Text
});
message.set_text(self.text.unwrap_or_default());
if self.html.is_some() {
message.set_html(self.html);
}
if self.override_sender_name.is_some() {
message.set_override_sender_name(self.override_sender_name);
}
if let Some(file) = self.file {
message.set_file(file, None);
}
if let Some((latitude, longitude)) = self.location {
message.set_location(latitude, longitude);
}
if let Some(id) = self.quoted_message_id {
message
.set_quote(
context,
Some(
&Message::load_from_db(context, MsgId::new(id))
.await
.context("message to quote could not be loaded")?,
),
)
.await?;
}
Ok(message)
}
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct MessageReadReceipt {
pub contact_id: u32,
pub timestamp: i64,
}
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(rename_all = "camelCase")]
pub struct MessageInfo {
rawtext: String,
ephemeral_timer: EphemeralTimer,
/// When message is ephemeral this contains the timestamp of the message expiry
ephemeral_timestamp: Option<i64>,
error: Option<String>,
rfc724_mid: String,
server_urls: Vec<String>,
hop_info: Option<String>,
}
impl MessageInfo {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
let rawtext = msg_id.rawtext(context).await?;
let ephemeral_timer = message.get_ephemeral_timer().into();
let ephemeral_timestamp = match message.get_ephemeral_timer() {
deltachat::ephemeral::Timer::Disabled => None,
deltachat::ephemeral::Timer::Enabled { .. } => Some(message.get_ephemeral_timestamp()),
};
let server_urls =
MsgId::get_info_server_urls(context, message.rfc724_mid().to_owned()).await?;
let hop_info = msg_id.hop_info(context).await?;
Ok(Self {
rawtext,
ephemeral_timer,
ephemeral_timestamp,
error: message.error(),
rfc724_mid: message.rfc724_mid().to_owned(),
server_urls,
hop_info,
})
}
}
#[derive(
Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema,
)]
#[serde(rename_all = "camelCase", tag = "kind")]
pub enum EphemeralTimer {
/// Timer is disabled.
Disabled,
/// Timer is enabled.
Enabled {
/// Timer duration in seconds.
///
/// The value cannot be 0.
duration: u32,
},
}
impl From<deltachat::ephemeral::Timer> for EphemeralTimer {
fn from(value: deltachat::ephemeral::Timer) -> Self {
match value {
deltachat::ephemeral::Timer::Disabled => EphemeralTimer::Disabled,
deltachat::ephemeral::Timer::Enabled { duration } => {
EphemeralTimer::Enabled { duration }
}
}
}
}

View File

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

View File

@@ -3,11 +3,9 @@ use num_traits::cast::ToPrimitive;
use serde::Serialize; use serde::Serialize;
use typescript_type_def::TypeDef; use typescript_type_def::TypeDef;
#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ProviderInfo { pub struct ProviderInfo {
/// Unique ID, corresponding to provider database filename.
pub id: String,
pub before_login_hint: String, pub before_login_hint: String,
pub overview_page: String, pub overview_page: String,
pub status: u32, // in reality this is an enum, but for simplicity and because it gets converted into a number anyway, we use an u32 here. pub status: u32, // in reality this is an enum, but for simplicity and because it gets converted into a number anyway, we use an u32 here.
@@ -16,7 +14,6 @@ pub struct ProviderInfo {
impl ProviderInfo { impl ProviderInfo {
pub fn from_dc_type(provider: Option<&Provider>) -> Option<Self> { pub fn from_dc_type(provider: Option<&Provider>) -> Option<Self> {
provider.map(|p| ProviderInfo { provider.map(|p| ProviderInfo {
id: p.id.to_owned(),
before_login_hint: p.before_login_hint.to_owned(), before_login_hint: p.before_login_hint.to_owned(),
overview_page: p.overview_page.to_owned(), overview_page: p.overview_page.to_owned(),
status: p.status.to_u32().unwrap(), status: p.status.to_u32().unwrap(),

View File

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

View File

@@ -1,37 +1,23 @@
use std::collections::BTreeMap; use std::collections::BTreeMap;
use deltachat::contact::ContactId;
use deltachat::reaction::Reactions; use deltachat::reaction::Reactions;
use serde::Serialize; use serde::Serialize;
use typescript_type_def::TypeDef; 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. /// Structure representing all reactions to a particular message.
#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[derive(Serialize, TypeDef)]
#[serde(rename = "Reactions", rename_all = "camelCase")] #[serde(rename = "Reactions", rename_all = "camelCase")]
pub struct JSONRPCReactions { pub struct JSONRPCReactions {
/// Map from a contact to it's reaction to message. /// Map from a contact to it's reaction to message.
reactions_by_contact: BTreeMap<u32, Vec<String>>, reactions_by_contact: BTreeMap<u32, Vec<String>>,
/// Unique reactions and their count, sorted in descending order. /// Unique reactions and their count
reactions: Vec<JSONRPCReaction>, reactions: BTreeMap<String, u32>,
} }
impl From<Reactions> for JSONRPCReactions { impl From<Reactions> for JSONRPCReactions {
fn from(reactions: Reactions) -> Self { fn from(reactions: Reactions) -> Self {
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new(); 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() { for contact_id in reactions.contacts() {
let reaction = reactions.get(contact_id); let reaction = reactions.get(contact_id);
@@ -44,29 +30,18 @@ impl From<Reactions> for JSONRPCReactions {
.map(|emoji| emoji.to_owned()) .map(|emoji| emoji.to_owned())
.collect(); .collect();
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone()); reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
} for emoji in emojis {
if let Some(x) = unique_reactions.get_mut(&emoji) {
let self_reactions = reactions_by_contact.get(&ContactId::SELF.to_u32()); *x += 1;
} else {
let mut reactions_v = Vec::new(); unique_reactions.insert(emoji, 1);
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)
} }
JSONRPCReactions { JSONRPCReactions {
reactions_by_contact, 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; use super::maybe_empty_string_to_option;
#[derive(Serialize, TypeDef, schemars::JsonSchema)] #[derive(Serialize, TypeDef)]
#[serde(rename = "WebxdcMessageInfo", rename_all = "camelCase")] #[serde(rename = "WebxdcMessageInfo", rename_all = "camelCase")]
pub struct WebxdcMessageInfo { pub struct WebxdcMessageInfo {
/// The name of the app. /// The name of the app.

View File

@@ -1,5 +1,5 @@
#![recursion_limit = "256"]
pub mod api; pub mod api;
pub use api::events;
pub use yerpc; pub use yerpc;
#[cfg(test)] #[cfg(test)]
@@ -14,11 +14,10 @@ mod tests {
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn basic_json_rpc_functionality() -> anyhow::Result<()> { async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
let tmp_dir = TempDir::new().unwrap().path().into(); let tmp_dir = TempDir::new().unwrap().path().into();
let writable = true; let accounts = Accounts::new(tmp_dir).await?;
let accounts = Accounts::new(tmp_dir, writable).await?;
let api = CommandApi::new(accounts); let api = CommandApi::new(accounts);
let (sender, receiver) = unbounded::<String>(); let (sender, mut receiver) = unbounded::<String>();
let (client, mut rx) = RpcClient::new(); let (client, mut rx) = RpcClient::new();
let session = RpcSession::new(client, api); let session = RpcSession::new(client, api);
@@ -37,17 +36,17 @@ mod tests {
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#; let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#; let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
session.handle_incoming(request).await; session.handle_incoming(request).await;
let result = receiver.recv().await?; let result = receiver.next().await;
println!("{result:?}"); println!("{result:?}");
assert_eq!(result, response.to_owned()); assert_eq!(result, Some(response.to_owned()));
} }
{ {
let request = r#"{"jsonrpc":"2.0","method":"get_all_account_ids","params":[],"id":2}"#; let request = r#"{"jsonrpc":"2.0","method":"get_all_account_ids","params":[],"id":2}"#;
let response = r#"{"jsonrpc":"2.0","id":2,"result":[1]}"#; let response = r#"{"jsonrpc":"2.0","id":2,"result":[1]}"#;
session.handle_incoming(request).await; session.handle_incoming(request).await;
let result = receiver.recv().await?; let result = receiver.next().await;
println!("{result:?}"); println!("{result:?}");
assert_eq!(result, response.to_owned()); assert_eq!(result, Some(response.to_owned()));
} }
Ok(()) Ok(())
@@ -56,11 +55,10 @@ mod tests {
#[tokio::test(flavor = "multi_thread")] #[tokio::test(flavor = "multi_thread")]
async fn test_batch_set_config() -> anyhow::Result<()> { async fn test_batch_set_config() -> anyhow::Result<()> {
let tmp_dir = TempDir::new().unwrap().path().into(); let tmp_dir = TempDir::new().unwrap().path().into();
let writable = true; let accounts = Accounts::new(tmp_dir).await?;
let accounts = Accounts::new(tmp_dir, writable).await?;
let api = CommandApi::new(accounts); let api = CommandApi::new(accounts);
let (sender, receiver) = unbounded::<String>(); let (sender, mut receiver) = unbounded::<String>();
let (client, mut rx) = RpcClient::new(); let (client, mut rx) = RpcClient::new();
let session = RpcSession::new(client, api); let session = RpcSession::new(client, api);
@@ -79,15 +77,15 @@ mod tests {
let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#; let request = r#"{"jsonrpc":"2.0","method":"add_account","params":[],"id":1}"#;
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#; let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
session.handle_incoming(request).await; session.handle_incoming(request).await;
let result = receiver.recv().await?; let result = receiver.next().await;
assert_eq!(result, response.to_owned()); assert_eq!(result, Some(response.to_owned()));
} }
{ {
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":"","socks5_enabled":"0","socks5_host":"","socks5_port":"","socks5_user":"","socks5_password":""}]}"#; let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":"","socks5_enabled":"0","socks5_host":"","socks5_port":"","socks5_user":"","socks5_password":""}]}"#;
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#; let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
session.handle_incoming(request).await; session.handle_incoming(request).await;
let result = receiver.recv().await?; let result = receiver.next().await;
assert_eq!(result, response.to_owned()); assert_eq!(result, Some(response.to_owned()));
} }
Ok(()) Ok(())

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
use std::net::SocketAddr; use std::net::SocketAddr;
use std::path::PathBuf; use std::path::PathBuf;
@@ -7,6 +6,7 @@ use yerpc::axum::handle_ws_rpc;
use yerpc::{RpcClient, RpcSession}; use yerpc::{RpcClient, RpcSession};
mod api; mod api;
use api::events::event_to_json_rpc_notification;
use api::{Accounts, CommandApi}; use api::{Accounts, CommandApi};
const DEFAULT_PORT: u16 = 20808; const DEFAULT_PORT: u16 = 20808;
@@ -20,8 +20,7 @@ async fn main() -> Result<(), std::io::Error> {
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number")) .map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
.unwrap_or(DEFAULT_PORT); .unwrap_or(DEFAULT_PORT);
log::info!("Starting with accounts directory `{path}`."); log::info!("Starting with accounts directory `{path}`.");
let writable = true; let accounts = Accounts::new(PathBuf::from(&path)).await.unwrap();
let accounts = Accounts::new(PathBuf::from(&path), writable).await.unwrap();
let state = CommandApi::new(accounts); let state = CommandApi::new(accounts);
let app = Router::new() let app = Router::new()
@@ -29,13 +28,15 @@ async fn main() -> Result<(), std::io::Error> {
.layer(Extension(state.clone())); .layer(Extension(state.clone()));
tokio::spawn(async move { tokio::spawn(async move {
state.accounts.write().await.start_io().await; state.accounts.read().await.start_io().await;
}); });
let addr = SocketAddr::from(([127, 0, 0, 1], port)); let addr = SocketAddr::from(([127, 0, 0, 1], port));
log::info!("JSON-RPC WebSocket server listening on {}", addr); log::info!("JSON-RPC WebSocket server listening on {}", addr);
let listener = tokio::net::TcpListener::bind(addr).await.unwrap(); axum::Server::bind(&addr)
axum::serve(listener, app).await.unwrap(); .serve(app.into_make_service())
.await
.unwrap();
Ok(()) Ok(())
} }
@@ -43,5 +44,12 @@ async fn main() -> Result<(), std::io::Error> {
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response { async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response {
let (client, out_receiver) = RpcClient::new(); let (client, out_receiver) = RpcClient::new();
let session = RpcSession::new(client.clone(), api.clone()); 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 handle_ws_rpc(ws, out_receiver, session).await
} }

View File

@@ -35,7 +35,7 @@ async function run() {
const accounts = await client.rpc.getAllAccounts(); const accounts = await client.rpc.getAllAccounts();
console.log("accounts loaded", accounts); console.log("accounts loaded", accounts);
for (const account of accounts) { for (const account of accounts) {
if (account.kind === "Configured") { if (account.type === "Configured") {
write( write(
$head, $head,
`<a href="#" onclick="selectDeltaAccount(${account.id})"> `<a href="#" onclick="selectDeltaAccount(${account.id})">
@@ -57,7 +57,7 @@ async function run() {
clear($main); clear($main);
const selectedAccount = SELECTED_ACCOUNT; const selectedAccount = SELECTED_ACCOUNT;
const info = await client.rpc.getAccountInfo(selectedAccount); const info = await client.rpc.getAccountInfo(selectedAccount);
if (info.kind !== "Configured") { if (info.type !== "Configured") {
return write($main, "Account is not configured"); return write($main, "Account is not configured");
} }
write($main, `<h2>${info.addr!}</h2>`); write($main, `<h2>${info.addr!}</h2>`);
@@ -67,7 +67,7 @@ async function run() {
null, null,
null null
); );
for (const chatId of chats) { for (const [chatId, _messageId] of chats) {
const chat = await client.rpc.getFullChatById(selectedAccount, chatId); const chat = await client.rpc.getFullChatById(selectedAccount, chatId);
write($main, `<h3>${chat.name}</h3>`); write($main, `<h3>${chat.name}</h3>`);
const messageIds = await client.rpc.getMessageIds( const messageIds = await client.rpc.getMessageIds(
@@ -81,7 +81,8 @@ async function run() {
messageIds messageIds
); );
for (const [_messageId, message] of Object.entries(messages)) { for (const [_messageId, message] of Object.entries(messages)) {
if (message.kind === "message") write($main, `<p>${message.text}</p>`); if (message.variant === "message")
write($main, `<p>${message.text}</p>`);
else write($main, `<p>loading error: ${message.error}</p>`); else write($main, `<p>loading error: ${message.error}</p>`);
} }
} }
@@ -92,9 +93,9 @@ async function run() {
$side, $side,
` `
<p class="message"> <p class="message">
[<strong>${event.kind}</strong> on account ${accountId}]<br> [<strong>${event.type}</strong> on account ${accountId}]<br>
<em>f1:</em> ${JSON.stringify( <em>f1:</em> ${JSON.stringify(
Object.assign({}, event, { kind: undefined }) Object.assign({}, event, { type: undefined })
)} )}
</p>` </p>`
); );

View File

@@ -9,6 +9,7 @@
"@types/chai": "^4.2.21", "@types/chai": "^4.2.21",
"@types/chai-as-promised": "^7.1.5", "@types/chai-as-promised": "^7.1.5",
"@types/mocha": "^9.0.0", "@types/mocha": "^9.0.0",
"@types/node-fetch": "^2.5.7",
"@types/ws": "^7.2.4", "@types/ws": "^7.2.4",
"c8": "^7.10.0", "c8": "^7.10.0",
"chai": "^4.3.4", "chai": "^4.3.4",
@@ -16,6 +17,7 @@
"esbuild": "^0.17.9", "esbuild": "^0.17.9",
"http-server": "^14.1.1", "http-server": "^14.1.1",
"mocha": "^9.1.1", "mocha": "^9.1.1",
"node-fetch": "^2.6.1",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.6.2", "prettier": "^2.6.2",
"typedoc": "^0.23.2", "typedoc": "^0.23.2",
@@ -25,8 +27,7 @@
"exports": { "exports": {
".": { ".": {
"import": "./dist/deltachat.js", "import": "./dist/deltachat.js",
"require": "./dist/deltachat.cjs", "require": "./dist/deltachat.cjs"
"types": "./dist/deltachat.d.ts"
} }
}, },
"license": "MPL-2.0", "license": "MPL-2.0",
@@ -54,5 +55,5 @@
}, },
"type": "module", "type": "module",
"types": "dist/deltachat.d.ts", "types": "dist/deltachat.d.ts",
"version": "1.138.5" "version": "1.111.0"
} }

View File

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

View File

@@ -12,7 +12,7 @@ describe("basic tests", () => {
before(async () => { before(async () => {
serverHandle = await startServer(); serverHandle = await startServer();
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true); dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
// dc.on("ALL", (event) => { // dc.on("ALL", (event) => {
//console.log("event", event); //console.log("event", event);
// }); // });
@@ -79,9 +79,6 @@ describe("basic tests", () => {
accountId = await dc.rpc.addAccount(); accountId = await dc.rpc.addAccount();
}); });
it("should block and unblock contact", async function () { it("should block and unblock contact", async function () {
// Cannot send sync messages to self as we do not have a self address.
await dc.rpc.setConfig(accountId, "sync_msgs", "0");
const contactId = await dc.rpc.createContact( const contactId = await dc.rpc.createContact(
accountId, accountId,
"example@delta.chat", "example@delta.chat",

View File

@@ -13,27 +13,27 @@ describe("online tests", function () {
before(async function () { before(async function () {
this.timeout(60000); this.timeout(60000);
if (!process.env.CHATMAIL_DOMAIN) { if (!process.env.DCC_NEW_TMP_EMAIL) {
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) { if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
console.error( console.error(
"CAN NOT RUN COVERAGE correctly: Missing CHATMAIL_DOMAIN environment variable!\n\n", "CAN NOT RUN COVERAGE correctly: Missing DCC_NEW_TMP_EMAIL environment variable!\n\n",
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test" "You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test"
); );
process.exit(1); process.exit(1);
} }
console.log( console.log(
"Missing CHATMAIL_DOMAIN environment variable!, skip integration tests" "Missing DCC_NEW_TMP_EMAIL environment variable!, skip integration tests"
); );
this.skip(); this.skip();
} }
serverHandle = await startServer(); serverHandle = await startServer();
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true); dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
dc.on("ALL", (contextId, { kind }) => { dc.on("ALL", (contextId, { type }) => {
if (kind !== "Info") console.log(contextId, kind); if (type !== "Info") console.log(contextId, type);
}); });
account1 = createTempUser(process.env.CHATMAIL_DOMAIN); account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
if (!account1 || !account1.email || !account1.password) { if (!account1 || !account1.email || !account1.password) {
console.log( console.log(
"We didn't got back an account from the api, skip integration tests" "We didn't got back an account from the api, skip integration tests"
@@ -41,7 +41,7 @@ describe("online tests", function () {
this.skip(); this.skip();
} }
account2 = createTempUser(process.env.CHATMAIL_DOMAIN); account2 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
if (!account2 || !account2.email || !account2.password) { if (!account2 || !account2.email || !account2.password) {
console.log( console.log(
"We didn't got back an account2 from the api, skip integration tests" "We didn't got back an account2 from the api, skip integration tests"
@@ -148,7 +148,7 @@ describe("online tests", function () {
waitForEvent(dc, "IncomingMsg", accountId1), waitForEvent(dc, "IncomingMsg", accountId1),
]); ]);
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message"); dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
// Check if answer arrives at A and if it is encrypted // Check if answer arives at A and if it is encrypted
await eventPromise2; await eventPromise2;
const messageId = ( const messageId = (
@@ -177,12 +177,12 @@ describe("online tests", function () {
}); });
}); });
async function waitForEvent<T extends DcEvent["kind"]>( async function waitForEvent<T extends DcEvent["type"]>(
dc: DeltaChat, dc: DeltaChat,
eventType: T, eventType: T,
accountId: number, accountId: number,
timeout: number = EVENT_TIMEOUT timeout: number = EVENT_TIMEOUT
): Promise<Extract<DcEvent, { kind: T }>> { ): Promise<Extract<DcEvent, { type: T }>> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const rejectTimeout = setTimeout( const rejectTimeout = setTimeout(
() => reject(new Error("Timeout reached before event came in")), () => reject(new Error("Timeout reached before event came in")),

View File

@@ -2,6 +2,7 @@ import { tmpdir } from "os";
import { join, resolve } from "path"; import { join, resolve } from "path";
import { mkdtemp, rm } from "fs/promises"; import { mkdtemp, rm } from "fs/promises";
import { spawn, exec } from "child_process"; import { spawn, exec } from "child_process";
import fetch from "node-fetch";
import { Readable, Writable } from "node:stream"; import { Readable, Writable } from "node:stream";
export type RpcServerHandle = { export type RpcServerHandle = {
@@ -56,14 +57,15 @@ export async function startServer(): Promise<RpcServerHandle> {
}; };
} }
export function createTempUser(chatmailDomain: String) { export async function createTempUser(url: string) {
const charset = "2345789acdefghjkmnpqrstuvwxyz"; const response = await fetch(url, {
let user = "ci-"; method: "POST",
for (let i = 0; i < 6; i++) { headers: {
user += charset[Math.floor(Math.random() * charset.length)]; "cache-control": "no-cache",
} },
const email = user + "@" + chatmailDomain; });
return { email: email, password: user + "$" + user }; if (!response.ok) throw new Error("Received invalid response");
return response.json();
} }
function getTargetDir(): Promise<string> { function getTargetDir(): Promise<string> {

View File

@@ -1,20 +1,19 @@
[package] [package]
name = "deltachat-repl" name = "deltachat-repl"
version = "1.138.5" version = "1.111.0"
license = "MPL-2.0" license = "MPL-2.0"
edition = "2021" edition = "2021"
repository = "https://github.com/deltachat/deltachat-core-rust"
[dependencies] [dependencies]
ansi_term = "0.12.1" ansi_term = "0.12.1"
anyhow = "1" anyhow = "1"
deltachat = { path = "..", features = ["internals"]} deltachat = { path = "..", features = ["internals"]}
dirs = "5" dirs = "4"
log = "0.4.21" log = "0.4.16"
pretty_env_logger = "0.5" pretty_env_logger = "0.4"
rusqlite = "0.31" rusqlite = "0.28"
rustyline = "14" rustyline = "11"
tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread", "macros"] } tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
[features] [features]
default = ["vendored"] default = ["vendored"]

View File

@@ -3,7 +3,7 @@ extern crate dirs;
use std::path::Path; use std::path::Path;
use std::str::FromStr; use std::str::FromStr;
use std::time::Duration; use std::time::{Duration, SystemTime};
use anyhow::{bail, ensure, Result}; use anyhow::{bail, ensure, Result};
use deltachat::chat::{ use deltachat::chat::{
@@ -18,8 +18,6 @@ use deltachat::imex::*;
use deltachat::location; use deltachat::location;
use deltachat::log::LogExt; use deltachat::log::LogExt;
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype}; use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
use deltachat::mimeparser::SystemMessage;
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
use deltachat::peerstate::*; use deltachat::peerstate::*;
use deltachat::qr::*; use deltachat::qr::*;
use deltachat::reaction::send_reaction; use deltachat::reaction::send_reaction;
@@ -34,6 +32,14 @@ use tokio::fs;
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4. /// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
async fn reset_tables(context: &Context, bits: i32) { async fn reset_tables(context: &Context, bits: i32) {
println!("Resetting tables ({bits})..."); println!("Resetting tables ({bits})...");
if 0 != bits & 1 {
context
.sql()
.execute("DELETE FROM jobs;", ())
.await
.unwrap();
println!("(1) Jobs reset.");
}
if 0 != bits & 2 { if 0 != bits & 2 {
context context
.sql() .sql()
@@ -132,7 +138,11 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
/* import a directory */ /* import a directory */
let dir_name = std::path::Path::new(&real_spec); let dir_name = std::path::Path::new(&real_spec);
let dir = fs::read_dir(dir_name).await; let dir = fs::read_dir(dir_name).await;
if let Ok(mut dir) = dir { if dir.is_err() {
error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
return false;
} else {
let mut dir = dir.unwrap();
while let Ok(Some(entry)) = dir.next_entry().await { while let Ok(Some(entry)) = dir.next_entry().await {
let name_f = entry.file_name(); let name_f = entry.file_name();
let name = name_f.to_string_lossy(); let name = name_f.to_string_lossy();
@@ -144,9 +154,6 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
} }
} }
} }
} else {
error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
return false;
} }
} }
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec); println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
@@ -180,7 +187,6 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
DownloadState::Available => " [⬇ Download available]", DownloadState::Available => " [⬇ Download available]",
DownloadState::InProgress => " [⬇ Download in progress...]", DownloadState::InProgress => " [⬇ Download in progress...]",
DownloadState::Failure => " [⬇ Download failed]", DownloadState::Failure => " [⬇ Download failed]",
DownloadState::Undecipherable => " [⬇ Decryption failed]",
}; };
let temp2 = timestamp_to_str(msg.get_timestamp()); let temp2 = timestamp_to_str(msg.get_timestamp());
@@ -193,7 +199,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
if msg.has_location() { "📍" } else { "" }, if msg.has_location() { "📍" } else { "" },
&contact_name, &contact_name,
contact_id, contact_id,
msgtext, msgtext.unwrap_or_default(),
if msg.has_html() { "[HAS-HTML]" } else { "" }, if msg.has_html() { "[HAS-HTML]" } else { "" },
if msg.get_from_id() == ContactId::SELF { if msg.get_from_id() == ContactId::SELF {
"" ""
@@ -204,17 +210,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
} else { } else {
"[FRESH]" "[FRESH]"
}, },
if msg.is_info() { if msg.is_info() { "[INFO]" } else { "" },
if msg.get_info_type() == SystemMessage::ChatProtectionEnabled {
"[INFO 🛡️]"
} else if msg.get_info_type() == SystemMessage::ChatProtectionDisabled {
"[INFO 🛡️❌]"
} else {
"[INFO]"
}
} else {
""
},
if msg.get_viewtype() == Viewtype::VideochatInvitation { if msg.get_viewtype() == Viewtype::VideochatInvitation {
format!( format!(
"[VIDEOCHAT-INVITATION: {}, type={}]", "[VIDEOCHAT-INVITATION: {}, type={}]",
@@ -277,8 +273,13 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()
let contact = Contact::get_by_id(context, *contact_id).await?; let contact = Contact::get_by_id(context, *contact_id).await?;
let name = contact.get_display_name(); let name = contact.get_display_name();
let addr = contact.get_addr(); let addr = contact.get_addr();
let verified_str = if contact.is_verified(context).await? { let verified_state = contact.is_verified(context).await?;
"" let verified_str = if VerifiedStatus::Unverified != verified_state {
if verified_state == VerifiedStatus::BidirectVerified {
" √√"
} else {
""
}
} else { } else {
"" ""
}; };
@@ -394,6 +395,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
unpin <chat-id>\n\ unpin <chat-id>\n\
mute <chat-id> [<seconds>]\n\ mute <chat-id> [<seconds>]\n\
unmute <chat-id>\n\ unmute <chat-id>\n\
protect <chat-id>\n\
unprotect <chat-id>\n\
delchat <chat-id>\n\ delchat <chat-id>\n\
accept <chat-id>\n\ accept <chat-id>\n\
decline <chat-id>\n\ decline <chat-id>\n\
@@ -560,7 +563,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
context.maybe_network().await; context.maybe_network().await;
} }
"housekeeping" => { "housekeeping" => {
sql::housekeeping(&context).await.log_err(&context).ok(); sql::housekeeping(&context).await.ok_or_log(&context);
} }
"listchats" | "listarchived" | "chats" => { "listchats" | "listarchived" | "chats" => {
let listflags = if arg0 == "listarchived" { let listflags = if arg0 == "listarchived" {
@@ -643,30 +646,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("{cnt} chats"); println!("{cnt} chats");
println!("{time_needed:?} to create this list"); println!("{time_needed:?} to create this list");
} }
"start-realtime" => {
if arg1.is_empty() {
bail!("missing msgid");
}
let msg_id = MsgId::new(arg1.parse()?);
let res = send_webxdc_realtime_advertisement(&context, msg_id).await?;
if let Some(res) = res {
println!("waiting for peer channel join");
res.await?;
}
println!("joined peer channel");
}
"send-realtime" => {
if arg1.is_empty() {
bail!("missing msgid");
}
if arg2.is_empty() {
bail!("no message");
}
let msg_id = MsgId::new(arg1.parse()?);
send_webxdc_realtime_data(&context, msg_id, arg2.as_bytes().to_vec()).await?;
println!("sent realtime message");
}
"chat" => { "chat" => {
if sel_chat.is_none() && arg1.is_empty() { if sel_chat.is_none() && arg1.is_empty() {
bail!("Argument [chat-id] is missing."); bail!("Argument [chat-id] is missing.");
@@ -826,30 +805,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
} }
"chatinfo" => { "chatinfo" => {
ensure!(sel_chat.is_some(), "No chat selected."); 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:"); println!("Memberlist:");
log_contactlist(&context, &contacts).await?; 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!( println!(
"Location streaming: {}", "{} contacts\nLocation streaming: {}",
contacts.len(),
location::is_sending_locations_to_chat( location::is_sending_locations_to_chat(
&context, &context,
Some(sel_chat.as_ref().unwrap().get_id()) Some(sel_chat.as_ref().unwrap().get_id())
@@ -914,7 +878,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let latitude = arg1.parse()?; let latitude = arg1.parse()?;
let longitude = arg2.parse()?; let longitude = arg2.parse()?;
let continue_streaming = location::set(&context, latitude, longitude, 0.).await?; let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
if continue_streaming { if continue_streaming {
println!("Success, streaming should be continued."); println!("Success, streaming should be continued.");
} else { } else {
@@ -948,7 +912,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
Viewtype::File Viewtype::File
}); });
msg.set_file(arg1, None); 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?; chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
} }
"sendhtml" => { "sendhtml" => {
@@ -960,11 +926,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let mut msg = Message::new(Viewtype::Text); let mut msg = Message::new(Viewtype::Text);
msg.set_html(Some(html.to_string())); 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() path.file_name().unwrap().to_string_lossy().to_string()
} else { } else {
arg2.to_string() arg2.to_string()
}); }));
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?; chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
} }
"sendsyncmsg" => match context.send_sync_msg().await? { "sendsyncmsg" => match context.send_sync_msg().await? {
@@ -1013,7 +979,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
if !arg1.is_empty() { if !arg1.is_empty() {
let mut draft = Message::new(Viewtype::Text); let mut draft = Message::new(Viewtype::Text);
draft.set_text(arg1.to_string()); draft.set_text(Some(arg1.to_string()));
sel_chat sel_chat
.as_ref() .as_ref()
.unwrap() .unwrap()
@@ -1037,7 +1003,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"Please specify text to add as device message." "Please specify text to add as device message."
); );
let mut msg = Message::new(Viewtype::Text); 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?; chat::add_device_msg(&context, None, Some(&mut msg)).await?;
} }
"listmedia" => { "listmedia" => {
@@ -1092,6 +1058,20 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}; };
chat::set_muted(&context, chat_id, duration).await?; chat::set_muted(&context, chat_id, duration).await?;
} }
"protect" | "unprotect" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?);
chat_id
.set_protection(
&context,
match arg0 {
"protect" => ProtectionStatus::Protected,
"unprotect" => ProtectionStatus::Unprotected,
_ => unreachable!("arg0={:?}", arg0),
},
)
.await?;
}
"delchat" => { "delchat" => {
ensure!(!arg1.is_empty(), "Argument <chat-id> missing."); ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
let chat_id = ChatId::new(arg1.parse()?); let chat_id = ChatId::new(arg1.parse()?);
@@ -1110,7 +1090,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"msginfo" => { "msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing."); ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?); let id = MsgId::new(arg1.parse()?);
let res = id.get_info(&context).await?; let res = message::get_msg_info(&context, id).await?;
println!("{res}"); println!("{res}");
} }
"download" => { "download" => {

View File

@@ -1,4 +1,3 @@
#![recursion_limit = "256"]
//! This is a CLI program and a little testing frame. This file must not be //! This is a CLI program and a little testing frame. This file must not be
//! included when using Delta Chat Core as a library. //! included when using Delta Chat Core as a library.
//! //!
@@ -10,6 +9,7 @@ extern crate deltachat;
use std::borrow::Cow::{self, Borrowed, Owned}; use std::borrow::Cow::{self, Borrowed, Owned};
use std::io::{self, Write}; use std::io::{self, Write};
use std::path::Path;
use std::process::Command; use std::process::Command;
use ansi_term::Color; use ansi_term::Color;
@@ -20,7 +20,8 @@ use deltachat::context::*;
use deltachat::oauth2::*; use deltachat::oauth2::*;
use deltachat::qr_code_generator::get_securejoin_qr_svg; use deltachat::qr_code_generator::get_securejoin_qr_svg;
use deltachat::securejoin::*; use deltachat::securejoin::*;
use deltachat::EventType; use deltachat::stock_str::StockStrings;
use deltachat::{EventType, Events};
use log::{error, info, warn}; use log::{error, info, warn};
use rustyline::completion::{Completer, FilenameCompleter, Pair}; use rustyline::completion::{Completer, FilenameCompleter, Pair};
use rustyline::error::ReadlineError; use rustyline::error::ReadlineError;
@@ -298,8 +299,8 @@ impl Highlighter for DcHelper {
self.highlighter.highlight(line, pos) self.highlighter.highlight(line, pos)
} }
fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool { fn highlight_char(&self, line: &str, pos: usize) -> bool {
self.highlighter.highlight_char(line, pos, forced) self.highlighter.highlight_char(line, pos)
} }
} }
@@ -311,10 +312,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
println!("Error: Bad arguments, expected [db-name]."); println!("Error: Bad arguments, expected [db-name].");
bail!("No db-name specified"); bail!("No db-name specified");
} }
let context = ContextBuilder::new(args[1].clone().into()) let context = Context::new(Path::new(&args[1]), 0, Events::new(), StockStrings::new()).await?;
.with_id(1)
.open()
.await?;
let events = context.get_event_emitter(); let events = context.get_event_emitter();
tokio::task::spawn(async move { tokio::task::spawn(async move {
@@ -483,10 +481,7 @@ async fn handle_cmd(
#[tokio::main] #[tokio::main]
async fn main() -> Result<(), Error> { async fn main() -> Result<(), Error> {
pretty_env_logger::formatted_timed_builder() let _ = pretty_env_logger::try_init();
.parse_default_env()
.filter_module("deltachat_repl", log::LevelFilter::Info)
.init();
let args = std::env::args().collect(); let args = std::env::args().collect();
start(args).await?; start(args).await?;

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,27 +5,13 @@ and provides asynchronous interface to it.
## Getting started ## Getting started
To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server` To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
or download a prebuilt release.
Install it anywhere in your `PATH`. 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 ## Testing
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`. 1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
2. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`. 2. Run `PATH="../target/debug:$PATH" tox`.
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output. Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
@@ -37,14 +23,19 @@ $ tox --devenv env
$ . env/bin/activate $ . env/bin/activate
``` ```
It is recommended to use IPython, because it supports using `await` directly
from the REPL.
``` ```
$ python $ pip install ipython
>>> from deltachat_rpc_client import * $ PATH="../target/debug:$PATH" ipython
>>> rpc = Rpc() ...
>>> rpc.start() In [1]: from deltachat_rpc_client import *
>>> dc = DeltaChat(rpc) In [2]: rpc = Rpc()
>>> system_info = dc.get_system_info() In [3]: await rpc.start()
>>> system_info["level"] In [4]: dc = DeltaChat(rpc)
'awesome' In [5]: system_info = await dc.get_system_info()
>>> rpc.close() In [6]: system_info["level"]
Out[6]: 'awesome'
In [7]: await rpc.close()
``` ```

View File

@@ -4,6 +4,7 @@
it will echo back any text send to it, it also will print to console all Delta Chat core events. it will echo back any text send to it, it also will print to console all Delta Chat core events.
Pass --help to the CLI to see available options. Pass --help to the CLI to see available options.
""" """
import asyncio
from deltachat_rpc_client import events, run_bot_cli from deltachat_rpc_client import events, run_bot_cli
@@ -11,15 +12,15 @@ hooks = events.HookCollection()
@hooks.on(events.RawEvent) @hooks.on(events.RawEvent)
def log_event(event): async def log_event(event):
print(event) print(event)
@hooks.on(events.NewMessage) @hooks.on(events.NewMessage)
def echo(event): async def echo(event):
snapshot = event.message_snapshot snapshot = event.message_snapshot
snapshot.chat.send_text(snapshot.text) await snapshot.chat.send_text(snapshot.text)
if __name__ == "__main__": if __name__ == "__main__":
run_bot_cli(hooks) asyncio.run(run_bot_cli(hooks))

45
deltachat-rpc-client/examples/echobot_advanced.py Executable file → Normal file
View File

@@ -3,10 +3,9 @@
it will echo back any message that has non-empty text and also supports the /help command. it will echo back any message that has non-empty text and also supports the /help command.
""" """
import asyncio
import logging import logging
import sys import sys
from threading import Thread
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
@@ -14,62 +13,62 @@ hooks = events.HookCollection()
@hooks.on(events.RawEvent) @hooks.on(events.RawEvent)
def log_event(event): async def log_event(event):
if event.kind == EventType.INFO: if event.type == EventType.INFO:
logging.info(event.msg) logging.info(event.msg)
elif event.kind == EventType.WARNING: elif event.type == EventType.WARNING:
logging.warning(event.msg) logging.warning(event.msg)
@hooks.on(events.RawEvent(EventType.ERROR)) @hooks.on(events.RawEvent(EventType.ERROR))
def log_error(event): async def log_error(event):
logging.error(event.msg) logging.error(event.msg)
@hooks.on(events.MemberListChanged) @hooks.on(events.MemberListChanged)
def on_memberlist_changed(event): async def on_memberlist_changed(event):
logging.info("member %s was %s", event.member, "added" if event.member_added else "removed") logging.info("member %s was %s", event.member, "added" if event.member_added else "removed")
@hooks.on(events.GroupImageChanged) @hooks.on(events.GroupImageChanged)
def on_group_image_changed(event): async def on_group_image_changed(event):
logging.info("group image %s", "deleted" if event.image_deleted else "changed") logging.info("group image %s", "deleted" if event.image_deleted else "changed")
@hooks.on(events.GroupNameChanged) @hooks.on(events.GroupNameChanged)
def on_group_name_changed(event): async def on_group_name_changed(event):
logging.info("group name changed, old name: %s", event.old_name) logging.info("group name changed, old name: %s", event.old_name)
@hooks.on(events.NewMessage(func=lambda e: not e.command)) @hooks.on(events.NewMessage(func=lambda e: not e.command))
def echo(event): async def echo(event):
snapshot = event.message_snapshot snapshot = event.message_snapshot
if snapshot.text or snapshot.file: if snapshot.text or snapshot.file:
snapshot.chat.send_message(text=snapshot.text, file=snapshot.file) await snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
@hooks.on(events.NewMessage(command="/help")) @hooks.on(events.NewMessage(command="/help"))
def help_command(event): async def help_command(event):
snapshot = event.message_snapshot snapshot = event.message_snapshot
snapshot.chat.send_text("Send me any message and I will echo it back") await snapshot.chat.send_text("Send me any message and I will echo it back")
def main(): async def main():
with Rpc() as rpc: async with Rpc() as rpc:
deltachat = DeltaChat(rpc) deltachat = DeltaChat(rpc)
system_info = deltachat.get_system_info() system_info = await deltachat.get_system_info()
logging.info("Running deltachat core %s", system_info.deltachat_core_version) logging.info("Running deltachat core %s", system_info.deltachat_core_version)
accounts = deltachat.get_all_accounts() accounts = await deltachat.get_all_accounts()
account = accounts[0] if accounts else deltachat.add_account() account = accounts[0] if accounts else await deltachat.add_account()
bot = Bot(account, hooks) bot = Bot(account, hooks)
if not bot.is_configured(): if not await bot.is_configured():
configure_thread = Thread(run=bot.configure, kwargs={"email": sys.argv[1], "password": sys.argv[2]}) # Save a reference to avoid garbage collection of the task.
configure_thread.start() _configure_task = asyncio.create_task(bot.configure(email=sys.argv[1], password=sys.argv[2]))
bot.run_forever() await bot.run_forever()
if __name__ == "__main__": if __name__ == "__main__":
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
main() asyncio.run(main())

54
deltachat-rpc-client/examples/echobot_no_hooks.py Executable file → Normal file
View File

@@ -2,56 +2,56 @@
""" """
Example echo bot without using hooks Example echo bot without using hooks
""" """
import asyncio
import logging import logging
import sys import sys
from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId from deltachat_rpc_client import DeltaChat, EventType, Rpc
def main(): async def main():
with Rpc() as rpc: async with Rpc() as rpc:
deltachat = DeltaChat(rpc) deltachat = DeltaChat(rpc)
system_info = deltachat.get_system_info() system_info = await deltachat.get_system_info()
logging.info("Running deltachat core %s", system_info["deltachat_core_version"]) logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
accounts = deltachat.get_all_accounts() accounts = await deltachat.get_all_accounts()
account = accounts[0] if accounts else deltachat.add_account() account = accounts[0] if accounts else await deltachat.add_account()
account.set_config("bot", "1") await account.set_config("bot", "1")
if not account.is_configured(): if not await account.is_configured():
logging.info("Account is not configured, configuring") logging.info("Account is not configured, configuring")
account.set_config("addr", sys.argv[1]) await account.set_config("addr", sys.argv[1])
account.set_config("mail_pw", sys.argv[2]) await account.set_config("mail_pw", sys.argv[2])
account.configure() await account.configure()
logging.info("Configured") logging.info("Configured")
else: else:
logging.info("Account is already configured") logging.info("Account is already configured")
deltachat.start_io() await deltachat.start_io()
def process_messages(): async def process_messages():
for message in account.get_next_messages(): for message in await account.get_fresh_messages_in_arrival_order():
snapshot = message.get_snapshot() 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:
snapshot.chat.send_text(snapshot.text) await snapshot.chat.send_text(snapshot.text)
snapshot.message.mark_seen() await snapshot.message.mark_seen()
# Process old messages. # Process old messages.
process_messages() await process_messages()
while True: while True:
event = account.wait_for_event() event = await account.wait_for_event()
if event["kind"] == EventType.INFO: if event["type"] == EventType.INFO:
logging.info("%s", event["msg"]) logging.info("%s", event["msg"])
elif event["kind"] == EventType.WARNING: elif event["type"] == EventType.WARNING:
logging.warning("%s", event["msg"]) logging.warning("%s", event["msg"])
elif event["kind"] == EventType.ERROR: elif event["type"] == EventType.ERROR:
logging.error("%s", event["msg"]) logging.error("%s", event["msg"])
elif event["kind"] == EventType.INCOMING_MSG: elif event["type"] == EventType.INCOMING_MSG:
logging.info("Got an incoming message") logging.info("Got an incoming message")
process_messages() await process_messages()
if __name__ == "__main__": if __name__ == "__main__":
logging.basicConfig(level=logging.INFO) logging.basicConfig(level=logging.INFO)
main() asyncio.run(main())

View File

@@ -1,28 +1,16 @@
[build-system] [build-system]
requires = ["setuptools>=45"] requires = ["setuptools>=45", "setuptools_scm[toml]>=6.2"]
build-backend = "setuptools.build_meta" build-backend = "setuptools.build_meta"
[project] [project]
name = "deltachat-rpc-client" name = "deltachat-rpc-client"
version = "1.138.5"
description = "Python client for Delta Chat core JSON-RPC interface" description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
"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"
]
readme = "README.md"
dependencies = [ dependencies = [
"imap-tools", "aiohttp",
"aiodns"
]
dynamic = [
"version"
] ]
[tool.setuptools.package-data] [tool.setuptools.package-data]
@@ -37,7 +25,7 @@ deltachat_rpc_client = [
line-length = 120 line-length = 120
[tool.ruff] [tool.ruff]
lint.select = [ select = [
"E", "W", # pycodestyle "E", "W", # pycodestyle
"F", # Pyflakes "F", # Pyflakes
"N", # pep8-naming "N", # pep8-naming

View File

@@ -1,10 +1,9 @@
"""Delta Chat JSON-RPC high-level API""" """Delta Chat asynchronous high-level API"""
from ._utils import AttrDict, run_bot_cli, run_client_cli from ._utils import AttrDict, run_bot_cli, run_client_cli
from .account import Account from .account import Account
from .chat import Chat from .chat import Chat
from .client import Bot, Client from .client import Bot, Client
from .const import EventType, SpecialContactId from .const import EventType
from .contact import Contact from .contact import Contact
from .deltachat import DeltaChat from .deltachat import DeltaChat
from .message import Message from .message import Message
@@ -20,7 +19,6 @@ __all__ = [
"DeltaChat", "DeltaChat",
"EventType", "EventType",
"Message", "Message",
"SpecialContactId",
"Rpc", "Rpc",
"run_bot_cli", "run_bot_cli",
"run_client_cli", "run_client_cli",

View File

@@ -1,7 +1,7 @@
import argparse import argparse
import asyncio
import re import re
import sys import sys
from threading import Thread
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -43,7 +43,7 @@ class AttrDict(dict):
super().__setattr__(attr, val) super().__setattr__(attr, val)
def run_client_cli( async def run_client_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None, hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None, argv: Optional[list] = None,
**kwargs, **kwargs,
@@ -54,10 +54,10 @@ def run_client_cli(
""" """
from .client import Client from .client import Client
_run_cli(Client, hooks, argv, **kwargs) await _run_cli(Client, hooks, argv, **kwargs)
def run_bot_cli( async def run_bot_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None, hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None, argv: Optional[list] = None,
**kwargs, **kwargs,
@@ -68,10 +68,10 @@ def run_bot_cli(
""" """
from .client import Bot from .client import Bot
_run_cli(Bot, hooks, argv, **kwargs) await _run_cli(Bot, hooks, argv, **kwargs)
def _run_cli( async def _run_cli(
client_type: Type["Client"], client_type: Type["Client"],
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None, hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None, argv: Optional[list] = None,
@@ -93,24 +93,20 @@ def _run_cli(
parser.add_argument("--password", action="store", help="password") parser.add_argument("--password", action="store", help="password")
args = parser.parse_args(argv[1:]) args = parser.parse_args(argv[1:])
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc: async with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
deltachat = DeltaChat(rpc) deltachat = DeltaChat(rpc)
core_version = (deltachat.get_system_info()).deltachat_core_version core_version = (await deltachat.get_system_info()).deltachat_core_version
accounts = deltachat.get_all_accounts() accounts = await deltachat.get_all_accounts()
account = accounts[0] if accounts else deltachat.add_account() account = accounts[0] if accounts else await deltachat.add_account()
client = client_type(account, hooks) client = client_type(account, hooks)
client.logger.debug("Running deltachat core %s", core_version) client.logger.debug("Running deltachat core %s", core_version)
if not client.is_configured(): if not await client.is_configured():
assert args.email, "Account is not configured and email must be provided" assert args.email, "Account is not configured and email must be provided"
assert args.password, "Account is not configured and password must be provided" assert args.password, "Account is not configured and password must be provided"
configure_thread = Thread( # Save a reference to avoid garbage collection of the task.
target=client.configure, _configure_task = asyncio.create_task(client.configure(email=args.email, password=args.password))
daemon=True, await client.run_forever()
kwargs={"email": args.email, "password": args.password},
)
configure_thread.start()
client.run_forever()
def extract_addr(text: str) -> str: def extract_addr(text: str) -> str:
@@ -172,33 +168,3 @@ def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
return "removed", addr, addr return "removed", addr, addr
return None return None
class futuremethod: # noqa: N801
"""Decorator for async methods."""
def __init__(self, func):
self._func = func
def __get__(self, instance, owner=None):
if instance is None:
return self
def future(*args):
generator = self._func(instance, *args)
res = next(generator)
def f():
try:
generator.send(res())
except StopIteration as e:
return e.value
return f
def wrapper(*args):
f = future(*args)
return f()
wrapper.future = future
return wrapper

View File

@@ -1,12 +1,9 @@
from __future__ import annotations
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union from typing import TYPE_CHECKING, List, Optional, Tuple, Union
from warnings import warn
from ._utils import AttrDict, futuremethod from ._utils import AttrDict
from .chat import Chat from .chat import Chat
from .const import ChatlistFlag, ContactFlag, EventType, SpecialContactId from .const import ChatlistFlag, ContactFlag, SpecialContactId
from .contact import Contact from .contact import Contact
from .message import Message from .message import Message
@@ -26,82 +23,63 @@ class Account:
def _rpc(self) -> "Rpc": def _rpc(self) -> "Rpc":
return self.manager.rpc return self.manager.rpc
def wait_for_event(self) -> AttrDict: async def wait_for_event(self) -> AttrDict:
"""Wait until the next event and return it.""" """Wait until the next event and return it."""
return AttrDict(self._rpc.wait_for_event(self.id)) return AttrDict(await self._rpc.wait_for_event(self.id))
def clear_all_events(self): async def remove(self) -> None:
"""Removes all queued-up events for a given account. Useful for tests."""
self._rpc.clear_all_events(self.id)
def remove(self) -> None:
"""Remove the account.""" """Remove the account."""
self._rpc.remove_account(self.id) await self._rpc.remove_account(self.id)
def start_io(self) -> None: async def start_io(self) -> None:
"""Start the account I/O.""" """Start the account I/O."""
self._rpc.start_io(self.id) await self._rpc.start_io(self.id)
def stop_io(self) -> None: async def stop_io(self) -> None:
"""Stop the account I/O.""" """Stop the account I/O."""
self._rpc.stop_io(self.id) await self._rpc.stop_io(self.id)
def get_info(self) -> AttrDict: async def get_info(self) -> AttrDict:
"""Return dictionary of this account configuration parameters.""" """Return dictionary of this account configuration parameters."""
return AttrDict(self._rpc.get_info(self.id)) return AttrDict(await self._rpc.get_info(self.id))
def get_size(self) -> int: async def get_size(self) -> int:
"""Get the combined filesize of an account in bytes.""" """Get the combined filesize of an account in bytes."""
return self._rpc.get_account_file_size(self.id) return await self._rpc.get_account_file_size(self.id)
def is_configured(self) -> bool: async def is_configured(self) -> bool:
"""Return True if this account is configured.""" """Return True if this account is configured."""
return self._rpc.is_configured(self.id) return await self._rpc.is_configured(self.id)
def set_config(self, key: str, value: Optional[str] = None) -> None: async def set_config(self, key: str, value: Optional[str] = None) -> None:
"""Set configuration value.""" """Set configuration value."""
self._rpc.set_config(self.id, key, value) await self._rpc.set_config(self.id, key, value)
def get_config(self, key: str) -> Optional[str]: async def get_config(self, key: str) -> Optional[str]:
"""Get configuration value.""" """Get configuration value."""
return self._rpc.get_config(self.id, key) return await self._rpc.get_config(self.id, key)
def update_config(self, **kwargs) -> None: async def update_config(self, **kwargs) -> None:
"""update config values.""" """update config values."""
for key, value in kwargs.items(): for key, value in kwargs.items():
self.set_config(key, value) await self.set_config(key, value)
def set_avatar(self, img_path: Optional[str] = None) -> None: async def set_avatar(self, img_path: Optional[str] = None) -> None:
"""Set self avatar. """Set self avatar.
Passing None will discard the currently set avatar. Passing None will discard the currently set avatar.
""" """
self.set_config("selfavatar", img_path) await self.set_config("selfavatar", img_path)
def get_avatar(self) -> Optional[str]: async def get_avatar(self) -> Optional[str]:
"""Get self avatar.""" """Get self avatar."""
return self.get_config("selfavatar") return await self.get_config("selfavatar")
def check_qr(self, qr): async def configure(self) -> None:
return self._rpc.check_qr(self.id, qr)
def set_config_from_qr(self, qr: str):
self._rpc.set_config_from_qr(self.id, qr)
@futuremethod
def configure(self):
"""Configure an account.""" """Configure an account."""
yield self._rpc.configure.future(self.id) await self._rpc.configure(self.id)
def bring_online(self): async def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
"""Start I/O and wait until IMAP becomes IDLE."""
self.start_io()
while True:
event = self.wait_for_event()
if event.kind == EventType.IMAP_INBOX_IDLE:
break
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
"""Create a new Contact or return an existing one. """Create a new Contact or return an existing one.
Calling this method will always result in the same Calling this method will always result in the same
@@ -115,49 +93,30 @@ class Account:
if isinstance(obj, int): if isinstance(obj, int):
obj = Contact(self, obj) obj = Contact(self, obj)
if isinstance(obj, Contact): if isinstance(obj, Contact):
obj = obj.get_snapshot().address obj = (await obj.get_snapshot()).address
return Contact(self, self._rpc.create_contact(self.id, obj, name)) return Contact(self, await self._rpc.create_contact(self.id, obj, name))
def create_chat(self, account: "Account") -> Chat:
addr = account.get_config("addr")
contact = self.create_contact(addr)
return contact.create_chat()
def get_contact_by_id(self, contact_id: int) -> Contact: def get_contact_by_id(self, contact_id: int) -> Contact:
"""Return Contact instance for the given contact ID.""" """Return Contact instance for the given contact ID."""
return Contact(self, contact_id) return Contact(self, contact_id)
def get_contact_by_addr(self, address: str) -> Optional[Contact]: async def get_contact_by_addr(self, address: str) -> Optional[Contact]:
"""Check if an e-mail address belongs to a known and unblocked contact.""" """Check if an e-mail address belongs to a known and unblocked contact."""
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address) contact_id = await self._rpc.lookup_contact_id_by_addr(self.id, address)
return contact_id and Contact(self, contact_id) return contact_id and Contact(self, contact_id)
def get_blocked_contacts(self) -> list[AttrDict]: async def get_blocked_contacts(self) -> List[AttrDict]:
"""Return a list with snapshots of all blocked contacts.""" """Return a list with snapshots of all blocked contacts."""
contacts = self._rpc.get_blocked_contacts(self.id) contacts = await self._rpc.get_blocked_contacts(self.id)
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts] return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
def get_chat_by_contact(self, contact: Union[int, Contact]) -> Optional[Chat]: async def get_contacts(
"""Return 1:1 chat for a contact if it exists."""
if isinstance(contact, Contact):
assert contact.account == self
contact_id = contact.id
elif isinstance(contact, int):
contact_id = contact
else:
raise ValueError(f"{contact!r} is not a contact")
chat_id = self._rpc.get_chat_id_by_contact_id(self.id, contact_id)
if chat_id:
return Chat(self, chat_id)
return None
def get_contacts(
self, self,
query: Optional[str] = None, query: Optional[str] = None,
with_self: bool = False, with_self: bool = False,
verified_only: bool = False, verified_only: bool = False,
snapshot: bool = False, snapshot: bool = False,
) -> Union[list[Contact], list[AttrDict]]: ) -> Union[List[Contact], List[AttrDict]]:
"""Get a filtered list of contacts. """Get a filtered list of contacts.
:param query: if a string is specified, only return contacts :param query: if a string is specified, only return contacts
@@ -173,9 +132,9 @@ class Account:
flags |= ContactFlag.ADD_SELF flags |= ContactFlag.ADD_SELF
if snapshot: if snapshot:
contacts = self._rpc.get_contacts(self.id, flags, query) contacts = await self._rpc.get_contacts(self.id, flags, query)
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts] return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
contacts = self._rpc.get_contact_ids(self.id, flags, query) contacts = await self._rpc.get_contact_ids(self.id, flags, query)
return [Contact(self, contact_id) for contact_id in contacts] return [Contact(self, contact_id) for contact_id in contacts]
@property @property
@@ -183,7 +142,7 @@ class Account:
"""This account's identity as a Contact.""" """This account's identity as a Contact."""
return Contact(self, SpecialContactId.SELF) return Contact(self, SpecialContactId.SELF)
def get_chatlist( async def get_chatlist(
self, self,
query: Optional[str] = None, query: Optional[str] = None,
contact: Optional[Contact] = None, contact: Optional[Contact] = None,
@@ -192,7 +151,7 @@ class Account:
no_specials: bool = False, no_specials: bool = False,
alldone_hint: bool = False, alldone_hint: bool = False,
snapshot: bool = False, snapshot: bool = False,
) -> Union[list[Chat], list[AttrDict]]: ) -> Union[List[Chat], List[AttrDict]]:
"""Return list of chats. """Return list of chats.
:param query: if a string is specified only chats matching this query are returned. :param query: if a string is specified only chats matching this query are returned.
@@ -215,130 +174,72 @@ class Account:
if alldone_hint: if alldone_hint:
flags |= ChatlistFlag.ADD_ALLDONE_HINT flags |= ChatlistFlag.ADD_ALLDONE_HINT
entries = self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id) entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
if not snapshot: if not snapshot:
return [Chat(self, entry) for entry in entries] return [Chat(self, entry[0]) for entry in entries]
items = self._rpc.get_chatlist_items_by_entries(self.id, entries) items = await self._rpc.get_chatlist_items_by_entries(self.id, entries)
chats = [] chats = []
for item in items.values(): for item in items.values():
item["chat"] = Chat(self, item["id"]) item["chat"] = Chat(self, item["id"])
chats.append(AttrDict(item)) chats.append(AttrDict(item))
return chats return chats
def create_group(self, name: str, protect: bool = False) -> Chat: async def create_group(self, name: str, protect: bool = False) -> Chat:
"""Create a new group chat. """Create a new group chat.
After creation, the group has only self-contact as member and is in unpromoted state. After creation, the group has only self-contact as member and is in unpromoted state.
""" """
return Chat(self, self._rpc.create_group_chat(self.id, name, protect)) return Chat(self, await self._rpc.create_group_chat(self.id, name, protect))
def get_chat_by_id(self, chat_id: int) -> Chat: def get_chat_by_id(self, chat_id: int) -> Chat:
"""Return the Chat instance with the given ID.""" """Return the Chat instance with the given ID."""
return Chat(self, chat_id) return Chat(self, chat_id)
def secure_join(self, qrdata: str) -> Chat: async def secure_join(self, qrdata: str) -> Chat:
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on """Continue a Setup-Contact or Verified-Group-Invite protocol started on
another device. another device.
The function returns immediately and the handshake runs in background, sending The function returns immediately and the handshake runs in background, sending
and receiving several messages. and receiving several messages.
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes. Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
See https://securejoin.delta.chat/ for protocol details. See https://countermitm.readthedocs.io/en/latest/new.html for protocol details.
:param qrdata: The text of the scanned QR code. :param qrdata: The text of the scanned QR code.
""" """
return Chat(self, self._rpc.secure_join(self.id, qrdata)) return Chat(self, await self._rpc.secure_join(self.id, qrdata))
def get_qr_code(self) -> tuple[str, str]: async def get_qr_code(self) -> Tuple[str, str]:
"""Get Setup-Contact QR Code text and SVG data. """Get Setup-Contact QR Code text and SVG data.
this data needs to be transferred to another Delta Chat account this data needs to be transferred to another Delta Chat account
in a second channel, typically used by mobiles with QRcode-show + scan UX. in a second channel, typically used by mobiles with QRcode-show + scan UX.
""" """
return self._rpc.get_chat_securejoin_qr_code_svg(self.id, None) return await self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
def get_message_by_id(self, msg_id: int) -> Message: def get_message_by_id(self, msg_id: int) -> Message:
"""Return the Message instance with the given ID.""" """Return the Message instance with the given ID."""
return Message(self, msg_id) return Message(self, msg_id)
def mark_seen_messages(self, messages: list[Message]) -> None: async def mark_seen_messages(self, messages: List[Message]) -> None:
"""Mark the given set of messages as seen.""" """Mark the given set of messages as seen."""
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages]) await self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
def delete_messages(self, messages: list[Message]) -> None: async def delete_messages(self, messages: List[Message]) -> None:
"""Delete messages (local and remote).""" """Delete messages (local and remote)."""
self._rpc.delete_messages(self.id, [msg.id for msg in messages]) await self._rpc.delete_messages(self.id, [msg.id for msg in messages])
def get_fresh_messages(self) -> list[Message]: async def get_fresh_messages(self) -> List[Message]:
"""Return the list of fresh messages, newest messages first. """Return the list of fresh messages, newest messages first.
This call is intended for displaying notifications. This call is intended for displaying notifications.
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead, If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
to process oldest messages first. to process oldest messages first.
""" """
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id) fresh_msg_ids = await self._rpc.get_fresh_msgs(self.id)
return [Message(self, msg_id) for msg_id in fresh_msg_ids] return [Message(self, msg_id) for msg_id in fresh_msg_ids]
def get_next_messages(self) -> list[Message]: async def get_fresh_messages_in_arrival_order(self) -> List[Message]:
"""Return a list of next messages."""
next_msg_ids = self._rpc.get_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
def wait_next_messages(self) -> list[Message]:
"""Wait for new messages and return a list of them."""
next_msg_ids = self._rpc.wait_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
def wait_for_incoming_msg_event(self):
"""Wait for incoming message event and return it."""
while True:
event = self.wait_for_event()
if event.kind == EventType.INCOMING_MSG:
return event
def wait_for_securejoin_inviter_success(self):
while True:
event = self.wait_for_event()
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
break
def wait_for_securejoin_joiner_success(self):
while True:
event = self.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
def wait_for_reactions_changed(self):
while True:
event = self.wait_for_event()
if event.kind == EventType.REACTIONS_CHANGED:
return event
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs.""" """Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
warn( fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id))
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
DeprecationWarning,
stacklevel=2,
)
fresh_msg_ids = sorted(self._rpc.get_fresh_msgs(self.id))
return [Message(self, msg_id) for msg_id in fresh_msg_ids] return [Message(self, msg_id) for msg_id in fresh_msg_ids]
def export_backup(self, path, passphrase: str = "") -> None:
"""Export backup."""
self._rpc.export_backup(self.id, str(path), passphrase)
def import_backup(self, path, passphrase: str = "") -> None:
"""Import backup."""
self._rpc.import_backup(self.id, str(path), passphrase)
def export_self_keys(self, path) -> None:
"""Export keys."""
passphrase = "" # Setting passphrase is currently not supported.
self._rpc.export_self_keys(self.id, str(path), passphrase)
def import_self_keys(self, path) -> None:
"""Import keys."""
passphrase = "" # Importing passphrase-protected keys is currently not supported.
self._rpc.import_self_keys(self.id, str(path), passphrase)

View File

@@ -1,8 +1,6 @@
from __future__ import annotations
import calendar import calendar
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
from ._utils import AttrDict from ._utils import AttrDict
from .const import ChatVisibility, ViewType from .const import ChatVisibility, ViewType
@@ -27,7 +25,7 @@ class Chat:
def _rpc(self) -> "Rpc": def _rpc(self) -> "Rpc":
return self.account._rpc return self.account._rpc
def delete(self) -> None: async def delete(self) -> None:
"""Delete this chat and all its messages. """Delete this chat and all its messages.
Note: Note:
@@ -35,91 +33,85 @@ class Chat:
- does not delete messages on server - does not delete messages on server
- the chat or contact is not blocked, new message will arrive - the chat or contact is not blocked, new message will arrive
""" """
self._rpc.delete_chat(self.account.id, self.id) await self._rpc.delete_chat(self.account.id, self.id)
def block(self) -> None: async def block(self) -> None:
"""Block this chat.""" """Block this chat."""
self._rpc.block_chat(self.account.id, self.id) await self._rpc.block_chat(self.account.id, self.id)
def accept(self) -> None: async def accept(self) -> None:
"""Accept this contact request chat.""" """Accept this contact request chat."""
self._rpc.accept_chat(self.account.id, self.id) await self._rpc.accept_chat(self.account.id, self.id)
def leave(self) -> None: async def leave(self) -> None:
"""Leave this chat.""" """Leave this chat."""
self._rpc.leave_group(self.account.id, self.id) await self._rpc.leave_group(self.account.id, self.id)
def mute(self, duration: Optional[int] = None) -> None: async def mute(self, duration: Optional[int] = None) -> None:
"""Mute this chat, if a duration is not provided the chat is muted forever. """Mute this chat, if a duration is not provided the chat is muted forever.
:param duration: mute duration from now in seconds. Must be greater than zero. :param duration: mute duration from now in seconds. Must be greater than zero.
""" """
if duration is not None: if duration is not None:
assert duration > 0, "Invalid duration" assert duration > 0, "Invalid duration"
dur: dict = {"kind": "Until", "duration": duration} dur: Union[str, dict] = {"Until": duration}
else: else:
dur = {"kind": "Forever"} dur = "Forever"
self._rpc.set_chat_mute_duration(self.account.id, self.id, dur) await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
def unmute(self) -> None: async def unmute(self) -> None:
"""Unmute this chat.""" """Unmute this chat."""
self._rpc.set_chat_mute_duration(self.account.id, self.id, {"kind": "NotMuted"}) await self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
def pin(self) -> None: async def pin(self) -> None:
"""Pin this chat.""" """Pin this chat."""
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED) await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
def unpin(self) -> None: async def unpin(self) -> None:
"""Unpin this chat.""" """Unpin this chat."""
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL) await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
def archive(self) -> None: async def archive(self) -> None:
"""Archive this chat.""" """Archive this chat."""
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED) await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
def unarchive(self) -> None: async def unarchive(self) -> None:
"""Unarchive this chat.""" """Unarchive this chat."""
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL) await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
def set_name(self, name: str) -> None: async def set_name(self, name: str) -> None:
"""Set name of this chat.""" """Set name of this chat."""
self._rpc.set_chat_name(self.account.id, self.id, name) await self._rpc.set_chat_name(self.account.id, self.id, name)
def set_ephemeral_timer(self, timer: int) -> None: async def set_ephemeral_timer(self, timer: int) -> None:
"""Set ephemeral timer of this chat in seconds. """Set ephemeral timer of this chat."""
await self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
0 means the timer is disabled, use 1 for immediate deletion.""" async def get_encryption_info(self) -> str:
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
def get_encryption_info(self) -> str:
"""Return encryption info for this chat.""" """Return encryption info for this chat."""
return self._rpc.get_chat_encryption_info(self.account.id, self.id) return await self._rpc.get_chat_encryption_info(self.account.id, self.id)
def get_qr_code(self) -> tuple[str, str]: async def get_qr_code(self) -> Tuple[str, str]:
"""Get Join-Group QR code text and SVG data.""" """Get Join-Group QR code text and SVG data."""
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id) return await self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
def get_basic_snapshot(self) -> AttrDict: async def get_basic_snapshot(self) -> AttrDict:
"""Get a chat snapshot with basic info about this chat.""" """Get a chat snapshot with basic info about this chat."""
info = self._rpc.get_basic_chat_info(self.account.id, self.id) info = await self._rpc.get_basic_chat_info(self.account.id, self.id)
return AttrDict(chat=self, **info) return AttrDict(chat=self, **info)
def get_full_snapshot(self) -> AttrDict: async def get_full_snapshot(self) -> AttrDict:
"""Get a full snapshot of this chat.""" """Get a full snapshot of this chat."""
info = self._rpc.get_full_chat_by_id(self.account.id, self.id) info = await self._rpc.get_full_chat_by_id(self.account.id, self.id)
return AttrDict(chat=self, **info) return AttrDict(chat=self, **info)
def can_send(self) -> bool: async def send_message(
"""Return true if messages can be sent to the chat."""
return self._rpc.can_send(self.account.id, self.id)
def send_message(
self, self,
text: Optional[str] = None, text: Optional[str] = None,
html: Optional[str] = None, html: Optional[str] = None,
viewtype: Optional[ViewType] = None, viewtype: Optional[ViewType] = None,
file: Optional[str] = None, file: Optional[str] = None,
location: Optional[tuple[float, float]] = None, location: Optional[Tuple[float, float]] = None,
override_sender_name: Optional[str] = None, override_sender_name: Optional[str] = None,
quoted_msg: Optional[Union[int, Message]] = None, quoted_msg: Optional[Union[int, Message]] = None,
) -> Message: ) -> Message:
@@ -134,54 +126,49 @@ class Chat:
"file": file, "file": file,
"location": location, "location": location,
"overrideSenderName": override_sender_name, "overrideSenderName": override_sender_name,
"quotedMessageId": quoted_msg, "quotedMsg": quoted_msg,
} }
msg_id = self._rpc.send_msg(self.account.id, self.id, draft) msg_id = await self._rpc.send_msg(self.account.id, self.id, draft)
return Message(self.account, msg_id) return Message(self.account, msg_id)
def send_text(self, text: str) -> Message: async def send_text(self, text: str) -> Message:
"""Send a text message and return the resulting Message instance.""" """Send a text message and return the resulting Message instance."""
msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text) msg_id = await self._rpc.misc_send_text_message(self.account.id, self.id, text)
return Message(self.account, msg_id) return Message(self.account, msg_id)
def send_file(self, path): async def send_videochat_invitation(self) -> Message:
"""Send a file and return the resulting Message instance."""
return self.send_message(file=path)
def send_videochat_invitation(self) -> Message:
"""Send a videochat invitation and return the resulting Message instance.""" """Send a videochat invitation and return the resulting Message instance."""
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id) msg_id = await self._rpc.send_videochat_invitation(self.account.id, self.id)
return Message(self.account, msg_id) return Message(self.account, msg_id)
def send_sticker(self, path: str) -> Message: async def send_sticker(self, path: str) -> Message:
"""Send an sticker and return the resulting Message instance.""" """Send an sticker and return the resulting Message instance."""
msg_id = self._rpc.send_sticker(self.account.id, self.id, path) msg_id = await self._rpc.send_sticker(self.account.id, self.id, path)
return Message(self.account, msg_id) return Message(self.account, msg_id)
def forward_messages(self, messages: list[Message]) -> None: async def forward_messages(self, messages: List[Message]) -> None:
"""Forward a list of messages to this chat.""" """Forward a list of messages to this chat."""
msg_ids = [msg.id for msg in messages] msg_ids = [msg.id for msg in messages]
self._rpc.forward_messages(self.account.id, msg_ids, self.id) await self._rpc.forward_messages(self.account.id, msg_ids, self.id)
def set_draft( async def set_draft(
self, self,
text: Optional[str] = None, text: Optional[str] = None,
file: Optional[str] = None, file: Optional[str] = None,
quoted_msg: Optional[int] = None, quoted_msg: Optional[int] = None,
viewtype: Optional[str] = None,
) -> None: ) -> None:
"""Set draft message.""" """Set draft message."""
if isinstance(quoted_msg, Message): if isinstance(quoted_msg, Message):
quoted_msg = quoted_msg.id quoted_msg = quoted_msg.id
self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg, viewtype) await self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
def remove_draft(self) -> None: async def remove_draft(self) -> None:
"""Remove draft message.""" """Remove draft message."""
self._rpc.remove_draft(self.account.id, self.id) await self._rpc.remove_draft(self.account.id, self.id)
def get_draft(self) -> Optional[AttrDict]: async def get_draft(self) -> Optional[AttrDict]:
"""Get draft message.""" """Get draft message."""
snapshot = self._rpc.get_draft(self.account.id, self.id) snapshot = await self._rpc.get_draft(self.account.id, self.id)
if not snapshot: if not snapshot:
return None return None
snapshot = AttrDict(snapshot) snapshot = AttrDict(snapshot)
@@ -190,74 +177,74 @@ class Chat:
snapshot["message"] = Message(self.account, snapshot.id) snapshot["message"] = Message(self.account, snapshot.id)
return snapshot return snapshot
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]: async def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
"""get the list of messages in this chat.""" """get the list of messages in this chat."""
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker) msgs = await self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
return [Message(self.account, msg_id) for msg_id in msgs] return [Message(self.account, msg_id) for msg_id in msgs]
def get_fresh_message_count(self) -> int: async def get_fresh_message_count(self) -> int:
"""Get number of fresh messages in this chat""" """Get number of fresh messages in this chat"""
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id) return await self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
def mark_noticed(self) -> None: async def mark_noticed(self) -> None:
"""Mark all messages in this chat as noticed.""" """Mark all messages in this chat as noticed."""
self._rpc.marknoticed_chat(self.account.id, self.id) await self._rpc.marknoticed_chat(self.account.id, self.id)
def add_contact(self, *contact: Union[int, str, Contact]) -> None: async def add_contact(self, *contact: Union[int, str, Contact]) -> None:
"""Add contacts to this group.""" """Add contacts to this group."""
for cnt in contact: for cnt in contact:
if isinstance(cnt, str): if isinstance(cnt, str):
contact_id = self.account.create_contact(cnt).id contact_id = (await self.account.create_contact(cnt)).id
elif not isinstance(cnt, int): elif not isinstance(cnt, int):
contact_id = cnt.id contact_id = cnt.id
else: else:
contact_id = cnt contact_id = cnt
self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id) await self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
def remove_contact(self, *contact: Union[int, str, Contact]) -> None: async def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
"""Remove members from this group.""" """Remove members from this group."""
for cnt in contact: for cnt in contact:
if isinstance(cnt, str): if isinstance(cnt, str):
contact_id = self.account.create_contact(cnt).id contact_id = (await self.account.create_contact(cnt)).id
elif not isinstance(cnt, int): elif not isinstance(cnt, int):
contact_id = cnt.id contact_id = cnt.id
else: else:
contact_id = cnt contact_id = cnt
self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id) await self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
def get_contacts(self) -> list[Contact]: async def get_contacts(self) -> List[Contact]:
"""Get the contacts belonging to this chat. """Get the contacts belonging to this chat.
For single/direct chats self-address is not included. For single/direct chats self-address is not included.
""" """
contacts = self._rpc.get_chat_contacts(self.account.id, self.id) contacts = await self._rpc.get_chat_contacts(self.account.id, self.id)
return [Contact(self.account, contact_id) for contact_id in contacts] return [Contact(self.account, contact_id) for contact_id in contacts]
def set_image(self, path: str) -> None: async def set_image(self, path: str) -> None:
"""Set profile image of this chat. """Set profile image of this chat.
:param path: Full path of the image to use as the group image. :param path: Full path of the image to use as the group image.
""" """
self._rpc.set_chat_profile_image(self.account.id, self.id, path) await self._rpc.set_chat_profile_image(self.account.id, self.id, path)
def remove_image(self) -> None: async def remove_image(self) -> None:
"""Remove profile image of this chat.""" """Remove profile image of this chat."""
self._rpc.set_chat_profile_image(self.account.id, self.id, None) await self._rpc.set_chat_profile_image(self.account.id, self.id, None)
def get_locations( async def get_locations(
self, self,
contact: Optional[Contact] = None, contact: Optional[Contact] = None,
timestamp_from: Optional["datetime"] = None, timestamp_from: Optional["datetime"] = None,
timestamp_to: Optional["datetime"] = None, timestamp_to: Optional["datetime"] = None,
) -> list[AttrDict]: ) -> List[AttrDict]:
"""Get list of location snapshots for the given contact in the given timespan.""" """Get list of location snapshots for the given contact in the given timespan."""
time_from = calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0 time_from = calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0 time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
contact_id = contact.id if contact else 0 contact_id = contact.id if contact else 0
result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to) result = await self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
locations = [] locations = []
contacts: dict[int, Contact] = {} contacts: Dict[int, Contact] = {}
for loc in result: for loc in result:
location = AttrDict(loc) location = AttrDict(loc)
location["chat"] = self location["chat"] = self

View File

@@ -1,13 +1,15 @@
"""Event loop implementations offering high level event handling/hooking.""" """Event loop implementations offering high level event handling/hooking."""
import inspect
from __future__ import annotations
import logging import logging
from typing import ( from typing import (
TYPE_CHECKING, TYPE_CHECKING,
Callable, Callable,
Coroutine,
Dict,
Iterable, Iterable,
Optional, Optional,
Set,
Tuple,
Type, Type,
Union, Union,
) )
@@ -18,7 +20,7 @@ from ._utils import (
parse_system_image_changed, parse_system_image_changed,
parse_system_title_changed, parse_system_title_changed,
) )
from .const import COMMAND_PREFIX, EventType, SpecialContactId, SystemMessageType from .const import COMMAND_PREFIX, EventType, SystemMessageType
from .events import ( from .events import (
EventFilter, EventFilter,
GroupImageChanged, GroupImageChanged,
@@ -38,16 +40,16 @@ class Client:
def __init__( def __init__(
self, self,
account: "Account", account: "Account",
hooks: Optional[Iterable[tuple[Callable, Union[type, EventFilter]]]] = None, hooks: Optional[Iterable[Tuple[Callable, Union[type, EventFilter]]]] = None,
logger: Optional[logging.Logger] = None, logger: Optional[logging.Logger] = None,
) -> None: ) -> None:
self.account = account self.account = account
self.logger = logger or logging self.logger = logger or logging
self._hooks: dict[type, set[tuple]] = {} self._hooks: Dict[type, Set[tuple]] = {}
self._should_process_messages = 0 self._should_process_messages = 0
self.add_hooks(hooks or []) self.add_hooks(hooks or [])
def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None: def add_hooks(self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]) -> None:
for hook, event in hooks: for hook, event in hooks:
self.add_hook(hook, event) self.add_hook(hook, event)
@@ -76,22 +78,22 @@ class Client:
) )
self._hooks.get(type(event), set()).remove((hook, event)) self._hooks.get(type(event), set()).remove((hook, event))
def is_configured(self) -> bool: async def is_configured(self) -> bool:
return self.account.is_configured() return await self.account.is_configured()
def configure(self, email: str, password: str, **kwargs) -> None: async def configure(self, email: str, password: str, **kwargs) -> None:
self.account.set_config("addr", email) await self.account.set_config("addr", email)
self.account.set_config("mail_pw", password) await self.account.set_config("mail_pw", password)
for key, value in kwargs.items(): for key, value in kwargs.items():
self.account.set_config(key, value) await self.account.set_config(key, value)
self.account.configure() await self.account.configure()
self.logger.debug("Account configured") self.logger.debug("Account configured")
def run_forever(self) -> None: async def run_forever(self) -> None:
"""Process events forever.""" """Process events forever."""
self.run_until(lambda _: False) await self.run_until(lambda _: False)
def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict: async def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
"""Process events until the given callable evaluates to True. """Process events until the given callable evaluates to True.
The callable should accept an AttrDict object representing the The callable should accept an AttrDict object representing the
@@ -99,37 +101,39 @@ class Client:
evaluates to True. evaluates to True.
""" """
self.logger.debug("Listening to incoming events...") self.logger.debug("Listening to incoming events...")
if self.is_configured(): if await self.is_configured():
self.account.start_io() await self.account.start_io()
self._process_messages() # Process old messages. await self._process_messages() # Process old messages.
while True: while True:
event = self.account.wait_for_event() event = await self.account.wait_for_event()
event["kind"] = EventType(event.kind) event["type"] = EventType(event.type)
event["account"] = self.account event["account"] = self.account
self._on_event(event) await self._on_event(event)
if event.kind == EventType.INCOMING_MSG: if event.type == EventType.INCOMING_MSG:
self._process_messages() await self._process_messages()
stop = func(event) stop = func(event)
if inspect.isawaitable(stop):
stop = await stop
if stop: if stop:
return event return event
def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None: async def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
for hook, evfilter in self._hooks.get(filter_type, []): for hook, evfilter in self._hooks.get(filter_type, []):
if evfilter.filter(event): if await evfilter.filter(event):
try: try:
hook(event) await hook(event)
except Exception as ex: except Exception as ex:
self.logger.exception(ex) self.logger.exception(ex)
def _parse_command(self, event: AttrDict) -> None: async def _parse_command(self, event: AttrDict) -> None:
cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command] cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
parts = event.message_snapshot.text.split(maxsplit=1) parts = event.message_snapshot.text.split(maxsplit=1)
payload = parts[1] if len(parts) > 1 else "" payload = parts[1] if len(parts) > 1 else ""
cmd = parts.pop(0) cmd = parts.pop(0)
if "@" in cmd: if "@" in cmd:
suffix = "@" + self.account.self_contact.get_snapshot().address suffix = "@" + (await self.account.self_contact.get_snapshot()).address
if cmd.endswith(suffix): if cmd.endswith(suffix):
cmd = cmd[: -len(suffix)] cmd = cmd[: -len(suffix)]
else: else:
@@ -149,32 +153,32 @@ class Client:
event["command"], event["payload"] = cmd, payload event["command"], event["payload"] = cmd, payload
def _on_new_msg(self, snapshot: AttrDict) -> None: async def _on_new_msg(self, snapshot: AttrDict) -> None:
event = AttrDict(command="", payload="", message_snapshot=snapshot) event = AttrDict(command="", payload="", message_snapshot=snapshot)
if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX): if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX):
self._parse_command(event) await self._parse_command(event)
self._on_event(event, NewMessage) await self._on_event(event, NewMessage)
def _handle_info_msg(self, snapshot: AttrDict) -> None: async def _handle_info_msg(self, snapshot: AttrDict) -> None:
event = AttrDict(message_snapshot=snapshot) event = AttrDict(message_snapshot=snapshot)
img_changed = parse_system_image_changed(snapshot.text) img_changed = parse_system_image_changed(snapshot.text)
if img_changed: if img_changed:
_, event["image_deleted"] = img_changed _, event["image_deleted"] = img_changed
self._on_event(event, GroupImageChanged) await self._on_event(event, GroupImageChanged)
return return
title_changed = parse_system_title_changed(snapshot.text) title_changed = parse_system_title_changed(snapshot.text)
if title_changed: if title_changed:
_, event["old_name"] = title_changed _, event["old_name"] = title_changed
self._on_event(event, GroupNameChanged) await self._on_event(event, GroupNameChanged)
return return
members_changed = parse_system_add_remove(snapshot.text) members_changed = parse_system_add_remove(snapshot.text)
if members_changed: if members_changed:
action, event["member"], _ = members_changed action, event["member"], _ = members_changed
event["member_added"] = action == "added" event["member_added"] = action == "added"
self._on_event(event, MemberListChanged) await self._on_event(event, MemberListChanged)
return return
self.logger.warning( self.logger.warning(
@@ -183,20 +187,19 @@ class Client:
snapshot.text, snapshot.text,
) )
def _process_messages(self) -> None: async def _process_messages(self) -> None:
if self._should_process_messages: if self._should_process_messages:
for message in self.account.get_next_messages(): for message in await self.account.get_fresh_messages_in_arrival_order():
snapshot = message.get_snapshot() snapshot = await message.get_snapshot()
if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]: await self._on_new_msg(snapshot)
self._on_new_msg(snapshot)
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE: if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
self._handle_info_msg(snapshot) await self._handle_info_msg(snapshot)
snapshot.message.mark_seen() await snapshot.message.mark_seen()
class Bot(Client): class Bot(Client):
"""Simple bot implementation that listens to events of a single account.""" """Simple bot implementation that listent to events of a single account."""
def configure(self, email: str, password: str, **kwargs) -> None: async def configure(self, email: str, password: str, **kwargs) -> None:
kwargs.setdefault("bot", "1") kwargs.setdefault("bot", "1")
super().configure(email, password, **kwargs) await super().configure(email, password, **kwargs)

View File

@@ -31,7 +31,6 @@ class EventType(str, Enum):
SMTP_MESSAGE_SENT = "SmtpMessageSent" SMTP_MESSAGE_SENT = "SmtpMessageSent"
IMAP_MESSAGE_DELETED = "ImapMessageDeleted" IMAP_MESSAGE_DELETED = "ImapMessageDeleted"
IMAP_MESSAGE_MOVED = "ImapMessageMoved" IMAP_MESSAGE_MOVED = "ImapMessageMoved"
IMAP_INBOX_IDLE = "ImapInboxIdle"
NEW_BLOB_FILE = "NewBlobFile" NEW_BLOB_FILE = "NewBlobFile"
DELETED_BLOB_FILE = "DeletedBlobFile" DELETED_BLOB_FILE = "DeletedBlobFile"
WARNING = "Warning" WARNING = "Warning"
@@ -45,7 +44,6 @@ class EventType(str, Enum):
MSG_DELIVERED = "MsgDelivered" MSG_DELIVERED = "MsgDelivered"
MSG_FAILED = "MsgFailed" MSG_FAILED = "MsgFailed"
MSG_READ = "MsgRead" MSG_READ = "MsgRead"
MSG_DELETED = "MsgDeleted"
CHAT_MODIFIED = "ChatModified" CHAT_MODIFIED = "ChatModified"
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified" CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
CONTACTS_CHANGED = "ContactsChanged" CONTACTS_CHANGED = "ContactsChanged"
@@ -59,19 +57,6 @@ class EventType(str, Enum):
SELFAVATAR_CHANGED = "SelfavatarChanged" SELFAVATAR_CHANGED = "SelfavatarChanged"
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate" WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted" WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
CHATLIST_CHANGED = "ChatlistChanged"
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
CONFIG_SYNCED = "ConfigSynced"
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
class ChatId(IntEnum):
"""Special chat ids"""
TRASH = 3
ARCHIVED_LINK = 6
ALLDONE_HINT = 7
LAST_SPECIAL = 9
class ChatType(IntEnum): class ChatType(IntEnum):
@@ -135,107 +120,3 @@ class SystemMessageType(str, Enum):
EPHEMERAL_TIMER_CHANGED = "EphemeralTimerChanged" EPHEMERAL_TIMER_CHANGED = "EphemeralTimerChanged"
MULTI_DEVICE_SYNC = "MultiDeviceSync" MULTI_DEVICE_SYNC = "MultiDeviceSync"
WEBXDC_INFO_MESSAGE = "WebxdcInfoMessage" WEBXDC_INFO_MESSAGE = "WebxdcInfoMessage"
class MessageState(IntEnum):
"""State of the message."""
UNDEFINED = 0
IN_FRESH = 10
IN_NOTICED = 13
IN_SEEN = 16
OUT_PREPARING = 18
OUT_DRAFT = 19
OUT_PENDING = 20
OUT_FAILED = 24
OUT_DELIVERED = 26
OUT_MDN_RCVD = 28
class MessageId(IntEnum):
"""Special message ids"""
DAYMARKER = 9
LAST_SPECIAL = 9
class CertificateChecks(IntEnum):
"""Certificate checks mode"""
AUTOMATIC = 0
STRICT = 1
ACCEPT_INVALID_CERTIFICATES = 3
class Connectivity(IntEnum):
"""Connectivity states"""
NOT_CONNECTED = 1000
CONNECTING = 2000
WORKING = 3000
CONNECTED = 4000
class KeyGenType(IntEnum):
"""Type of the key to generate"""
DEFAULT = 0
RSA2048 = 1
ED25519 = 2
RSA4096 = 3
# "Lp" means "login parameters"
class LpAuthFlag(IntEnum):
"""Authorization flags"""
OAUTH2 = 0x2
NORMAL = 0x4
class MediaQuality(IntEnum):
"""Media quality setting"""
BALANCED = 0
WORSE = 1
class ProviderStatus(IntEnum):
"""Provider status according to manual testing"""
OK = 1
PREPARATION = 2
BROKEN = 3
class PushNotifyState(IntEnum):
"""Push notifications state"""
NOT_CONNECTED = 0
HEARTBEAT = 1
CONNECTED = 2
class ShowEmails(IntEnum):
"""Show emails mode"""
OFF = 0
ACCEPTED_CONTACTS = 1
ALL = 2
class SocketSecurity(IntEnum):
"""Socket security"""
AUTOMATIC = 0
SSL = 1
STARTTLS = 2
PLAIN = 3
class VideochatType(IntEnum):
"""Video chat URL type"""
UNKNOWN = 0
BASICWEBRTC = 1
JITSI = 2

View File

@@ -24,39 +24,39 @@ class Contact:
def _rpc(self) -> "Rpc": def _rpc(self) -> "Rpc":
return self.account._rpc return self.account._rpc
def block(self) -> None: async def block(self) -> None:
"""Block contact.""" """Block contact."""
self._rpc.block_contact(self.account.id, self.id) await self._rpc.block_contact(self.account.id, self.id)
def unblock(self) -> None: async def unblock(self) -> None:
"""Unblock contact.""" """Unblock contact."""
self._rpc.unblock_contact(self.account.id, self.id) await self._rpc.unblock_contact(self.account.id, self.id)
def delete(self) -> None: async def delete(self) -> None:
"""Delete contact.""" """Delete contact."""
self._rpc.delete_contact(self.account.id, self.id) await self._rpc.delete_contact(self.account.id, self.id)
def set_name(self, name: str) -> None: async def set_name(self, name: str) -> None:
"""Change the name of this contact.""" """Change the name of this contact."""
self._rpc.change_contact_name(self.account.id, self.id, name) await self._rpc.change_contact_name(self.account.id, self.id, name)
def get_encryption_info(self) -> str: async def get_encryption_info(self) -> str:
"""Get a multi-line encryption info, containing your fingerprint and """Get a multi-line encryption info, containing your fingerprint and
the fingerprint of the contact. the fingerprint of the contact.
""" """
return self._rpc.get_contact_encryption_info(self.account.id, self.id) return await self._rpc.get_contact_encryption_info(self.account.id, self.id)
def get_snapshot(self) -> AttrDict: async def get_snapshot(self) -> AttrDict:
"""Return a dictionary with a snapshot of all contact properties.""" """Return a dictionary with a snapshot of all contact properties."""
snapshot = AttrDict(self._rpc.get_contact(self.account.id, self.id)) snapshot = AttrDict(await self._rpc.get_contact(self.account.id, self.id))
snapshot["contact"] = self snapshot["contact"] = self
return snapshot return snapshot
def create_chat(self) -> "Chat": async def create_chat(self) -> "Chat":
"""Create or get an existing 1:1 chat for this contact.""" """Create or get an existing 1:1 chat for this contact."""
from .chat import Chat from .chat import Chat
return Chat( return Chat(
self.account, self.account,
self._rpc.create_chat_by_contact_id(self.account.id, self.id), await self._rpc.create_chat_by_contact_id(self.account.id, self.id),
) )

View File

@@ -1,6 +1,4 @@
from __future__ import annotations from typing import TYPE_CHECKING, Dict, List
from typing import TYPE_CHECKING
from ._utils import AttrDict from ._utils import AttrDict
from .account import Account from .account import Account
@@ -18,34 +16,34 @@ class DeltaChat:
def __init__(self, rpc: "Rpc") -> None: def __init__(self, rpc: "Rpc") -> None:
self.rpc = rpc self.rpc = rpc
def add_account(self) -> Account: async def add_account(self) -> Account:
"""Create a new account database.""" """Create a new account database."""
account_id = self.rpc.add_account() account_id = await self.rpc.add_account()
return Account(self, account_id) return Account(self, account_id)
def get_all_accounts(self) -> list[Account]: async def get_all_accounts(self) -> List[Account]:
"""Return a list of all available accounts.""" """Return a list of all available accounts."""
account_ids = self.rpc.get_all_account_ids() account_ids = await self.rpc.get_all_account_ids()
return [Account(self, account_id) for account_id in account_ids] return [Account(self, account_id) for account_id in account_ids]
def start_io(self) -> None: async def start_io(self) -> None:
"""Start the I/O of all accounts.""" """Start the I/O of all accounts."""
self.rpc.start_io_for_all_accounts() await self.rpc.start_io_for_all_accounts()
def stop_io(self) -> None: async def stop_io(self) -> None:
"""Stop the I/O of all accounts.""" """Stop the I/O of all accounts."""
self.rpc.stop_io_for_all_accounts() await self.rpc.stop_io_for_all_accounts()
def maybe_network(self) -> None: async def maybe_network(self) -> None:
"""Indicate that the network likely has come back or just that the network """Indicate that the network likely has come back or just that the network
conditions might have changed. conditions might have changed.
""" """
self.rpc.maybe_network() await self.rpc.maybe_network()
def get_system_info(self) -> AttrDict: async def get_system_info(self) -> AttrDict:
"""Get information about the Delta Chat core in this system.""" """Get information about the Delta Chat core in this system."""
return AttrDict(self.rpc.get_system_info()) return AttrDict(await self.rpc.get_system_info())
def set_translations(self, translations: dict[str, str]) -> None: async def set_translations(self, translations: Dict[str, str]) -> None:
"""Set stock translation strings.""" """Set stock translation strings."""
self.rpc.set_stock_strings(translations) await self.rpc.set_stock_strings(translations)

View File

@@ -1,226 +0,0 @@
"""
Internal Python-level IMAP handling used by the tests.
"""
from __future__ import annotations
import imaplib
import io
import pathlib
import ssl
from contextlib import contextmanager
from imap_tools import (
AND,
Header,
MailBox,
MailBoxTls,
MailMessage,
MailMessageFlags,
errors,
)
from . import Account, const
FLAGS = b"FLAGS"
FETCH = b"FETCH"
ALL = "1:*"
class DirectImap:
def __init__(self, account: Account) -> None:
self.account = account
self.logid = account.get_config("displayname") or id(account)
self._idling = False
self.connect()
def connect(self):
host = self.account.get_config("configured_mail_server")
port = int(self.account.get_config("configured_mail_port"))
security = int(self.account.get_config("configured_mail_security"))
user = self.account.get_config("addr")
pw = self.account.get_config("mail_pw")
if security == const.SocketSecurity.PLAIN:
ssl_context = None
else:
ssl_context = ssl.create_default_context()
# don't check if certificate hostname doesn't match target hostname
ssl_context.check_hostname = False
# don't check if the certificate is trusted by a certificate authority
ssl_context.verify_mode = ssl.CERT_NONE
if security == const.SocketSecurity.STARTTLS:
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
elif security == const.SocketSecurity.PLAIN or security == const.SocketSecurity.SSL:
self.conn = MailBox(host, port, ssl_context=ssl_context)
self.conn.login(user, pw)
self.select_folder("INBOX")
def shutdown(self):
try:
self.conn.logout()
except (OSError, imaplib.IMAP4.abort):
print("Could not logout direct_imap conn")
def create_folder(self, foldername):
try:
self.conn.folder.create(foldername)
except errors.MailboxFolderCreateError as e:
print("Can't create", foldername, "probably it already exists:", str(e))
def select_folder(self, foldername: str) -> tuple:
assert not self._idling
return self.conn.folder.set(foldername)
def select_config_folder(self, config_name: str):
"""Return info about selected folder if it is
configured, otherwise None.
"""
if "_" not in config_name:
config_name = f"configured_{config_name}_folder"
foldername = self.account.get_config(config_name)
if foldername:
return self.select_folder(foldername)
return None
def list_folders(self) -> list[str]:
"""return list of all existing folder names."""
assert not self._idling
return [folder.name for folder in self.conn.folder.list()]
def delete(self, uid_list: str, expunge=True):
"""delete a range of messages (imap-syntax).
If expunge is true, perform the expunge-operation
to make sure the messages are really gone and not
just flagged as deleted.
"""
self.conn.client.uid("STORE", uid_list, "+FLAGS", r"(\Deleted)")
if expunge:
self.conn.expunge()
def get_all_messages(self) -> list[MailMessage]:
assert not self._idling
return list(self.conn.fetch())
def get_unread_messages(self) -> list[str]:
assert not self._idling
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
def mark_all_read(self):
messages = self.get_unread_messages()
if messages:
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
print("marked seen:", messages, res)
def get_unread_cnt(self) -> int:
return len(self.get_unread_messages())
def dump_imap_structures(self, dir, logfile):
assert not self._idling
stream = io.StringIO()
def log(*args, **kwargs):
kwargs["file"] = stream
print(*args, **kwargs)
empty_folders = []
for imapfolder in self.list_folders():
self.select_folder(imapfolder)
messages = list(self.get_all_messages())
if not messages:
empty_folders.append(imapfolder)
continue
log("---------", imapfolder, len(messages), "messages ---------")
# get message content without auto-marking it as seen
# fetching 'RFC822' would mark it as seen.
for msg in self.conn.fetch(mark_seen=False):
body = getattr(msg.obj, "text", None)
if not body:
body = getattr(msg.obj, "html", None)
if not body:
log("Message", msg.uid, "has empty body")
continue
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
path.mkdir(parents=True, exist_ok=True)
fn = path.joinpath(str(msg.uid))
fn.write_bytes(body)
log("Message", msg.uid, fn)
log(
"Message",
msg.uid,
msg.flags,
"Message-Id:",
msg.obj.get("Message-Id"),
)
if empty_folders:
log("--------- EMPTY FOLDERS:", empty_folders)
print(stream.getvalue(), file=logfile)
@contextmanager
def idle(self):
"""return Idle ContextManager."""
idle_manager = IdleManager(self)
try:
yield idle_manager
finally:
idle_manager.done()
def append(self, folder: str, msg: str):
"""Upload a message to *folder*.
Trailing whitespace or a linebreak at the beginning will be removed automatically.
"""
if msg.startswith("\n"):
msg = msg[1:]
msg = "\n".join([s.lstrip() for s in msg.splitlines()])
self.conn.append(bytes(msg, encoding="ascii"), folder)
def get_uid_by_message_id(self, message_id) -> str:
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)))]
if len(msgs) == 0:
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
return msgs[0]
class IdleManager:
def __init__(self, direct_imap) -> None:
self.direct_imap = direct_imap
self.log = direct_imap.account.log
# fetch latest messages before starting idle so that it only
# returns messages that arrive anew
self.direct_imap.conn.fetch("1:*")
self.direct_imap.conn.idle.start()
def check(self, timeout=None) -> list[bytes]:
"""(blocking) wait for next idle message from server."""
self.log("imap-direct: calling idle_check")
res = self.direct_imap.conn.idle.poll(timeout=timeout)
self.log(f"imap-direct: idle_check returned {res!r}")
return res
def wait_for_new_message(self, timeout=None) -> bytes:
while True:
for item in self.check(timeout=timeout):
if b"EXISTS" in item or b"RECENT" in item:
return item
def wait_for_seen(self, timeout=None) -> int:
"""Return first message with SEEN flag from a running idle-stream."""
while True:
for item in self.check(timeout=timeout):
if FETCH in item:
self.log(str(item))
if FLAGS in item and rb"\Seen" in item:
return int(item.split(b" ")[1])
def done(self):
"""send idle-done to server if we are currently in idle mode."""
return self.direct_imap.conn.idle.stop()

View File

@@ -1,10 +1,8 @@
"""High-level classes for event processing and filtering.""" """High-level classes for event processing and filtering."""
import inspect
from __future__ import annotations
import re import re
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Union from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
from .const import EventType from .const import EventType
@@ -26,7 +24,7 @@ def _tuple_of(obj, type_: type) -> tuple:
class EventFilter(ABC): class EventFilter(ABC):
"""The base event filter. """The base event filter.
:param func: A Callable function that should accept the event as input :param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event parameter, and return a bool value indicating whether the event
should be dispatched or not. should be dispatched or not.
""" """
@@ -45,13 +43,16 @@ class EventFilter(ABC):
def __ne__(self, other): def __ne__(self, other):
return not self == other return not self == other
def _call_func(self, event) -> bool: async def _call_func(self, event) -> bool:
if not self.func: if not self.func:
return True return True
return self.func(event) res = self.func(event)
if inspect.isawaitable(res):
return await res
return res
@abstractmethod @abstractmethod
def filter(self, event): async def filter(self, event):
"""Return True-like value if the event passed the filter and should be """Return True-like value if the event passed the filter and should be
used, or False-like value otherwise. used, or False-like value otherwise.
""" """
@@ -61,7 +62,7 @@ class RawEvent(EventFilter):
"""Matches raw core events. """Matches raw core events.
:param types: The types of event to match. :param types: The types of event to match.
:param func: A Callable function that should accept the event as input :param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event parameter, and return a bool value indicating whether the event
should be dispatched or not. should be dispatched or not.
""" """
@@ -81,10 +82,10 @@ class RawEvent(EventFilter):
return (self.types, self.func) == (other.types, other.func) return (self.types, self.func) == (other.types, other.func)
return False return False
def filter(self, event: "AttrDict") -> bool: async def filter(self, event: "AttrDict") -> bool:
if self.types and event.kind not in self.types: if self.types and event.type not in self.types:
return False return False
return self._call_func(event) return await self._call_func(event)
class NewMessage(EventFilter): class NewMessage(EventFilter):
@@ -103,7 +104,7 @@ class NewMessage(EventFilter):
:param is_info: If set to True only match info/system messages, if set to False :param is_info: If set to True only match info/system messages, if set to False
only match messages that are not info/system messages. If omitted only match messages that are not info/system messages. If omitted
info/system messages as well as normal messages will be matched. info/system messages as well as normal messages will be matched.
:param func: A Callable function that should accept the event as input :param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event parameter, and return a bool value indicating whether the event
should be dispatched or not. should be dispatched or not.
""" """
@@ -158,7 +159,7 @@ class NewMessage(EventFilter):
) )
return False return False
def filter(self, event: "AttrDict") -> bool: async def filter(self, event: "AttrDict") -> bool:
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot: if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
return False return False
if self.is_info is not None and self.is_info != event.message_snapshot.is_info: if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
@@ -167,9 +168,11 @@ class NewMessage(EventFilter):
return False return False
if self.pattern: if self.pattern:
match = self.pattern(event.message_snapshot.text) match = self.pattern(event.message_snapshot.text)
if inspect.isawaitable(match):
match = await match
if not match: if not match:
return False return False
return super()._call_func(event) return await super()._call_func(event)
class MemberListChanged(EventFilter): class MemberListChanged(EventFilter):
@@ -181,7 +184,7 @@ class MemberListChanged(EventFilter):
:param added: If set to True only match if a member was added, if set to False :param added: If set to True only match if a member was added, if set to False
only match if a member was removed. If omitted both, member additions only match if a member was removed. If omitted both, member additions
and removals, will be matched. and removals, will be matched.
:param func: A Callable function that should accept the event as input :param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event parameter, and return a bool value indicating whether the event
should be dispatched or not. should be dispatched or not.
""" """
@@ -198,10 +201,10 @@ class MemberListChanged(EventFilter):
return (self.added, self.func) == (other.added, other.func) return (self.added, self.func) == (other.added, other.func)
return False return False
def filter(self, event: "AttrDict") -> bool: async def filter(self, event: "AttrDict") -> bool:
if self.added is not None and self.added != event.member_added: if self.added is not None and self.added != event.member_added:
return False return False
return self._call_func(event) return await self._call_func(event)
class GroupImageChanged(EventFilter): class GroupImageChanged(EventFilter):
@@ -213,7 +216,7 @@ class GroupImageChanged(EventFilter):
:param deleted: If set to True only match if the image was deleted, if set to False :param deleted: If set to True only match if the image was deleted, if set to False
only match if a new image was set. If omitted both, image changes and only match if a new image was set. If omitted both, image changes and
removals, will be matched. removals, will be matched.
:param func: A Callable function that should accept the event as input :param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event parameter, and return a bool value indicating whether the event
should be dispatched or not. should be dispatched or not.
""" """
@@ -230,10 +233,10 @@ class GroupImageChanged(EventFilter):
return (self.deleted, self.func) == (other.deleted, other.func) return (self.deleted, self.func) == (other.deleted, other.func)
return False return False
def filter(self, event: "AttrDict") -> bool: async def filter(self, event: "AttrDict") -> bool:
if self.deleted is not None and self.deleted != event.image_deleted: if self.deleted is not None and self.deleted != event.image_deleted:
return False return False
return self._call_func(event) return await self._call_func(event)
class GroupNameChanged(EventFilter): class GroupNameChanged(EventFilter):
@@ -242,7 +245,7 @@ class GroupNameChanged(EventFilter):
Warning: registering a handler for this event will cause the messages Warning: registering a handler for this event will cause the messages
to be marked as read. Its usage is mainly intended for bots. to be marked as read. Its usage is mainly intended for bots.
:param func: A Callable function that should accept the event as input :param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event parameter, and return a bool value indicating whether the event
should be dispatched or not. should be dispatched or not.
""" """
@@ -255,8 +258,8 @@ class GroupNameChanged(EventFilter):
return self.func == other.func return self.func == other.func
return False return False
def filter(self, event: "AttrDict") -> bool: async def filter(self, event: "AttrDict") -> bool:
return self._call_func(event) return await self._call_func(event)
class HookCollection: class HookCollection:
@@ -265,9 +268,9 @@ class HookCollection:
""" """
def __init__(self) -> None: def __init__(self) -> None:
self._hooks: set[tuple[Callable, Union[type, EventFilter]]] = set() self._hooks: Set[Tuple[Callable, Union[type, EventFilter]]] = set()
def __iter__(self) -> Iterator[tuple[Callable, Union[type, EventFilter]]]: def __iter__(self) -> Iterator[Tuple[Callable, Union[type, EventFilter]]]:
return iter(self._hooks) return iter(self._hooks)
def on(self, event: Union[type, EventFilter]) -> Callable: # noqa def on(self, event: Union[type, EventFilter]) -> Callable: # noqa

View File

@@ -1,9 +1,8 @@
import json import json
from dataclasses import dataclass from dataclasses import dataclass
from typing import TYPE_CHECKING, Optional, Union from typing import TYPE_CHECKING, Union
from ._utils import AttrDict from ._utils import AttrDict
from .const import EventType
from .contact import Contact from .contact import Contact
if TYPE_CHECKING: if TYPE_CHECKING:
@@ -22,51 +21,32 @@ class Message:
def _rpc(self) -> "Rpc": def _rpc(self) -> "Rpc":
return self.account._rpc return self.account._rpc
def send_reaction(self, *reaction: str) -> "Message": async def send_reaction(self, *reaction: str):
"""Send a reaction to this message.""" """Send a reaction to this message."""
msg_id = self._rpc.send_reaction(self.account.id, self.id, reaction) await self._rpc.send_reaction(self.account.id, self.id, reaction)
return Message(self.account, msg_id)
def get_snapshot(self) -> AttrDict: async def get_snapshot(self) -> AttrDict:
"""Get a snapshot with the properties of this message.""" """Get a snapshot with the properties of this message."""
from .chat import Chat from .chat import Chat
snapshot = AttrDict(self._rpc.get_message(self.account.id, self.id)) snapshot = AttrDict(await self._rpc.get_message(self.account.id, self.id))
snapshot["chat"] = Chat(self.account, snapshot.chat_id) snapshot["chat"] = Chat(self.account, snapshot.chat_id)
snapshot["sender"] = Contact(self.account, snapshot.from_id) snapshot["sender"] = Contact(self.account, snapshot.from_id)
snapshot["message"] = self snapshot["message"] = self
return snapshot return snapshot
def get_reactions(self) -> Optional[AttrDict]: async def mark_seen(self) -> None:
"""Get message reactions."""
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
if reactions:
return AttrDict(reactions)
return None
def get_sender_contact(self) -> Contact:
from_id = self.get_snapshot().from_id
return self.account.get_contact_by_id(from_id)
def mark_seen(self) -> None:
"""Mark the message as seen.""" """Mark the message as seen."""
self._rpc.markseen_msgs(self.account.id, [self.id]) await self._rpc.markseen_msgs(self.account.id, [self.id])
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None: async def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
"""Send a webxdc status update. This message must be a webxdc.""" """Send a webxdc status update. This message must be a webxdc."""
if not isinstance(update, str): if not isinstance(update, str):
update = json.dumps(update) update = json.dumps(update)
self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description) await self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list: async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial)) return json.loads(await self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
def get_webxdc_info(self) -> dict: async def get_webxdc_info(self) -> dict:
return self._rpc.get_webxdc_info(self.account.id, self.id) return await self._rpc.get_webxdc_info(self.account.id, self.id)
def wait_until_delivered(self) -> None:
"""Consume events until the message is delivered."""
while True:
event = self.account.wait_for_event()
if event.kind == EventType.MSG_DELIVERED and event.msg_id == self.id:
break

View File

@@ -1,83 +1,66 @@
from __future__ import annotations import asyncio
import json
import os import os
import random from typing import AsyncGenerator, List, Optional
from typing import AsyncGenerator, Optional
import pytest import aiohttp
import pytest_asyncio
from . import Account, AttrDict, Bot, Chat, Client, DeltaChat, EventType, Message from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
from ._utils import futuremethod
from .rpc import Rpc from .rpc import Rpc
def get_temp_credentials() -> dict: async def get_temp_credentials() -> dict:
domain = os.getenv("CHATMAIL_DOMAIN") url = os.getenv("DCC_NEW_TMP_EMAIL")
username = "ci-" + "".join(random.choice("2345789acdefghjkmnpqrstuvwxyz") for i in range(6)) assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set"
password = f"{username}${username}"
addr = f"{username}@{domain}" # Replace default 5 minute timeout with a 1 minute timeout.
return {"email": addr, "password": password} timeout = aiohttp.ClientTimeout(total=60)
async with aiohttp.ClientSession() as session:
async with session.post(url, timeout=timeout) as response:
return json.loads(await response.text())
class ACFactory: class ACFactory:
def __init__(self, deltachat: DeltaChat) -> None: def __init__(self, deltachat: DeltaChat) -> None:
self.deltachat = deltachat self.deltachat = deltachat
def get_unconfigured_account(self) -> Account: async def get_unconfigured_account(self) -> Account:
account = self.deltachat.add_account() return await self.deltachat.add_account()
account.set_config("verified_one_on_one_chats", "1")
return account
def get_unconfigured_bot(self) -> Bot: async def get_unconfigured_bot(self) -> Bot:
return Bot(self.get_unconfigured_account()) return Bot(await self.get_unconfigured_account())
def new_preconfigured_account(self) -> Account: async def new_preconfigured_account(self) -> Account:
"""Make a new account with configuration options set, but configuration not started.""" """Make a new account with configuration options set, but configuration not started."""
credentials = get_temp_credentials() credentials = await get_temp_credentials()
account = self.get_unconfigured_account() account = await self.get_unconfigured_account()
account.set_config("addr", credentials["email"]) await account.set_config("addr", credentials["email"])
account.set_config("mail_pw", credentials["password"]) await account.set_config("mail_pw", credentials["password"])
assert not account.is_configured() assert not await account.is_configured()
return account return account
@futuremethod async def new_configured_account(self) -> Account:
def new_configured_account(self): account = await self.new_preconfigured_account()
account = self.new_preconfigured_account() await account.configure()
yield account.configure.future() assert await account.is_configured()
assert account.is_configured()
return account return account
def new_configured_bot(self) -> Bot: async def new_configured_bot(self) -> Bot:
credentials = get_temp_credentials() credentials = await get_temp_credentials()
bot = self.get_unconfigured_bot() bot = await self.get_unconfigured_bot()
bot.configure(credentials["email"], credentials["password"]) await bot.configure(credentials["email"], credentials["password"])
return bot return bot
@futuremethod async def get_online_account(self) -> Account:
def get_online_account(self): account = await self.new_configured_account()
account = yield self.new_configured_account.future() await account.start_io()
account.bring_online()
return account return account
def get_online_accounts(self, num: int) -> list[Account]: async def get_online_accounts(self, num: int) -> List[Account]:
futures = [self.get_online_account.future() for _ in range(num)] return await asyncio.gather(*[self.get_online_account() for _ in range(num)])
return [f() for f in futures]
def resetup_account(self, ac: Account) -> Account: async def send_message(
"""Resetup account from scratch, losing the encryption key."""
ac.stop_io()
ac_clone = self.get_unconfigured_account()
for i in ["addr", "mail_pw"]:
ac_clone.set_config(i, ac.get_config(i))
ac.remove()
ac_clone.configure()
return ac_clone
def get_accepted_chat(self, ac1: Account, ac2: Account) -> Chat:
ac2.create_chat(ac1)
return ac1.create_chat(ac2)
def send_message(
self, self,
to_account: Account, to_account: Account,
from_account: Optional[Account] = None, from_account: Optional[Account] = None,
@@ -86,16 +69,16 @@ class ACFactory:
group: Optional[str] = None, group: Optional[str] = None,
) -> Message: ) -> Message:
if not from_account: if not from_account:
from_account = (self.get_online_accounts(1))[0] from_account = (await self.get_online_accounts(1))[0]
to_contact = from_account.create_contact(to_account.get_config("addr")) to_contact = await from_account.create_contact(await to_account.get_config("addr"))
if group: if group:
to_chat = from_account.create_group(group) to_chat = await from_account.create_group(group)
to_chat.add_contact(to_contact) await to_chat.add_contact(to_contact)
else: else:
to_chat = to_contact.create_chat() to_chat = await to_contact.create_chat()
return to_chat.send_message(text=text, file=file) return await to_chat.send_message(text=text, file=file)
def process_message( async def process_message(
self, self,
to_client: Client, to_client: Client,
from_account: Optional[Account] = None, from_account: Optional[Account] = None,
@@ -103,7 +86,7 @@ class ACFactory:
file: Optional[str] = None, file: Optional[str] = None,
group: Optional[str] = None, group: Optional[str] = None,
) -> AttrDict: ) -> AttrDict:
self.send_message( await self.send_message(
to_account=to_client.account, to_account=to_client.account,
from_account=from_account, from_account=from_account,
text=text, text=text,
@@ -111,16 +94,16 @@ class ACFactory:
group=group, group=group,
) )
return to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG) return await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
@pytest.fixture() @pytest_asyncio.fixture
def rpc(tmp_path) -> AsyncGenerator: async def rpc(tmp_path) -> AsyncGenerator:
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts")) rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
with rpc_server: async with rpc_server:
yield rpc_server yield rpc_server
@pytest.fixture() @pytest_asyncio.fixture
def acfactory(rpc) -> AsyncGenerator: async def acfactory(rpc) -> AsyncGenerator:
return ACFactory(DeltaChat(rpc)) yield ACFactory(DeltaChat(rpc))

View File

@@ -1,65 +1,16 @@
from __future__ import annotations import asyncio
import itertools
import json import json
import logging
import os import os
import subprocess from typing import Any, Dict, Optional
import sys
from queue import Empty, Queue
from threading import Event, Thread
from typing import Any, Iterator, Optional
class JsonRpcError(Exception): class JsonRpcError(Exception):
pass pass
class RpcFuture:
def __init__(self, rpc: "Rpc", request_id: int, event: Event):
self.rpc = rpc
self.request_id = request_id
self.event = event
def __call__(self):
self.event.wait()
response = self.rpc.request_results.pop(self.request_id)
if "error" in response:
raise JsonRpcError(response["error"])
if "result" in response:
return response["result"]
return None
class RpcMethod:
def __init__(self, rpc: "Rpc", name: str):
self.rpc = rpc
self.name = name
def __call__(self, *args) -> Any:
"""Synchronously calls JSON-RPC method."""
future = self.future(*args)
return future()
def future(self, *args) -> Any:
"""Asynchronously calls JSON-RPC method."""
request_id = next(self.rpc.id_iterator)
request = {
"jsonrpc": "2.0",
"method": self.name,
"params": args,
"id": request_id,
}
event = Event()
self.rpc.request_events[request_id] = event
self.rpc.request_queue.put(request)
return RpcFuture(self.rpc, request_id, event)
class Rpc: class Rpc:
def __init__(self, accounts_dir: Optional[str] = None, **kwargs): def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
"""The given arguments will be passed to subprocess.Popen()""" """The given arguments will be passed to asyncio.create_subprocess_exec()"""
if accounts_dir: if accounts_dir:
kwargs["env"] = { kwargs["env"] = {
**kwargs.get("env", os.environ), **kwargs.get("env", os.environ),
@@ -67,135 +18,85 @@ class Rpc:
} }
self._kwargs = kwargs self._kwargs = kwargs
self.process: subprocess.Popen self.process: asyncio.subprocess.Process
self.id_iterator: Iterator[int] self.id: int
self.event_queues: dict[int, Queue] self.event_queues: Dict[int, asyncio.Queue]
# Map from request ID to `threading.Event`. # Map from request ID to `asyncio.Future` returning the response.
self.request_events: dict[int, Event] self.request_events: Dict[int, asyncio.Future]
# Map from request ID to the result. self.reader_task: asyncio.Task
self.request_results: dict[int, Any]
self.request_queue: Queue[Any]
self.closing: bool
self.reader_thread: Thread
self.writer_thread: Thread
self.events_thread: Thread
def start(self) -> None: async def start(self) -> None:
if sys.version_info >= (3, 11): self.process = await asyncio.create_subprocess_exec(
self.process = subprocess.Popen( "deltachat-rpc-server",
"deltachat-rpc-server", stdin=asyncio.subprocess.PIPE,
stdin=subprocess.PIPE, stdout=asyncio.subprocess.PIPE,
stdout=subprocess.PIPE, **self._kwargs,
# Prevent subprocess from capturing SIGINT. )
process_group=0, self.id = 0
**self._kwargs,
)
else:
self.process = subprocess.Popen(
"deltachat-rpc-server",
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
# `process_group` is not supported before Python 3.11.
preexec_fn=os.setpgrp, # noqa: PLW1509
**self._kwargs,
)
self.id_iterator = itertools.count(start=1)
self.event_queues = {} self.event_queues = {}
self.request_events = {} self.request_events = {}
self.request_results = {} self.reader_task = asyncio.create_task(self.reader_loop())
self.request_queue = Queue()
self.closing = False
self.reader_thread = Thread(target=self.reader_loop)
self.reader_thread.start()
self.writer_thread = Thread(target=self.writer_loop)
self.writer_thread.start()
self.events_thread = Thread(target=self.events_loop)
self.events_thread.start()
def close(self) -> None: async def close(self) -> None:
"""Terminate RPC server process and wait until the reader loop finishes.""" """Terminate RPC server process and wait until the reader loop finishes."""
self.closing = True self.process.terminate()
self.stop_io_for_all_accounts() await self.reader_task
self.events_thread.join()
self.process.stdin.close()
self.reader_thread.join()
self.request_queue.put(None)
self.writer_thread.join()
def __enter__(self): async def __aenter__(self):
self.start() await self.start()
return self return self
def __exit__(self, _exc_type, _exc, _tb): async def __aexit__(self, _exc_type, _exc, _tb):
self.close() await self.close()
def reader_loop(self) -> None: async def reader_loop(self) -> None:
try: while True:
while True: line = await self.process.stdout.readline() # noqa
line = self.process.stdout.readline() if not line: # EOF
if not line: # EOF break
break response = json.loads(line)
response = json.loads(line) if "id" in response:
if "id" in response: fut = self.request_events.pop(response["id"])
response_id = response["id"] fut.set_result(response)
event = self.request_events.pop(response_id) elif response["method"] == "event":
self.request_results[response_id] = response # An event notification.
event.set() params = response["params"]
else: account_id = params["contextId"]
logging.warning("Got a response without ID: %s", response) if account_id not in self.event_queues:
except Exception: self.event_queues[account_id] = asyncio.Queue()
# Log an exception if the reader loop dies. await self.event_queues[account_id].put(params["event"])
logging.exception("Exception in the reader loop") else:
print(response)
def writer_loop(self) -> None: async def wait_for_event(self, account_id: int) -> Optional[dict]:
"""Writer loop ensuring only a single thread writes requests."""
try:
while True:
request = self.request_queue.get()
if not request:
break
data = (json.dumps(request) + "\n").encode()
self.process.stdin.write(data)
self.process.stdin.flush()
except Exception:
# Log an exception if the writer loop dies.
logging.exception("Exception in the writer loop")
def get_queue(self, account_id: int) -> Queue:
if account_id not in self.event_queues:
self.event_queues[account_id] = Queue()
return self.event_queues[account_id]
def events_loop(self) -> None:
"""Requests new events and distributes them between queues."""
try:
while True:
if self.closing:
return
event = self.get_next_event()
account_id = event["contextId"]
queue = self.get_queue(account_id)
event = event["event"]
print("account_id=%d got an event %s" % (account_id, event), file=sys.stderr)
queue.put(event)
except Exception:
# Log an exception if the event loop dies.
logging.exception("Exception in the event loop")
def wait_for_event(self, account_id: int) -> Optional[dict]:
"""Waits for the next event from the given account and returns it.""" """Waits for the next event from the given account and returns it."""
queue = self.get_queue(account_id) if account_id in self.event_queues:
return queue.get() return await self.event_queues[account_id].get()
return None
def clear_all_events(self, account_id: int):
"""Removes all queued-up events for a given account. Useful for tests."""
queue = self.get_queue(account_id)
try:
while True:
queue.get_nowait()
except Empty:
pass
def __getattr__(self, attr: str): def __getattr__(self, attr: str):
return RpcMethod(self, attr) 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": kwargs or args,
"id": self.id,
}
data = (json.dumps(request) + "\n").encode()
self.process.stdin.write(data) # noqa
loop = asyncio.get_running_loop()
fut = loop.create_future()
self.request_events[request_id] = fut
response = await fut
if "error" in response:
raise JsonRpcError(response["error"])
if "result" in response:
return response["result"]
return None
return method

View File

@@ -1,218 +0,0 @@
from __future__ import annotations
import base64
import os
from typing import TYPE_CHECKING
from deltachat_rpc_client import Account, EventType, const
if TYPE_CHECKING:
from deltachat_rpc_client.pytestplugin import ACFactory
def wait_for_chatlist_and_specific_item(account, chat_id):
first_event = ""
while True:
event = account.wait_for_event()
if event.kind == EventType.CHATLIST_CHANGED:
first_event = "change"
break
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id:
first_event = "item_change"
break
while True:
event = account.wait_for_event()
if event.kind == EventType.CHATLIST_CHANGED and first_event == "item_change":
break
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id and first_event == "change":
break
def wait_for_chatlist_specific_item(account, chat_id):
while True:
event = account.wait_for_event()
if event.kind == EventType.CHATLIST_ITEM_CHANGED and event.chat_id == chat_id:
break
def wait_for_chatlist(account):
while True:
event = account.wait_for_event()
if event.kind == EventType.CHATLIST_CHANGED:
break
def test_delivery_status(acfactory: ACFactory) -> None:
"""
Test change status on chatlistitem when status changes (delivered, read)
"""
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice.clear_all_events()
bob.stop_io()
alice.stop_io()
alice_chat_bob.send_text("hi")
wait_for_chatlist_and_specific_item(alice, chat_id=alice_chat_bob.id)
alice.clear_all_events()
alice.start_io()
wait_for_chatlist_specific_item(alice, chat_id=alice_chat_bob.id)
bob.clear_all_events()
bob.start_io()
event = bob.wait_for_incoming_msg_event()
msg = bob.get_message_by_id(event.msg_id)
msg.get_snapshot().chat.accept()
msg.mark_seen()
chat_item = alice._rpc.get_chatlist_items_by_entries(alice.id, [alice_chat_bob.id])[str(alice_chat_bob.id)]
assert chat_item["summaryStatus"] == const.MessageState.OUT_DELIVERED
alice.clear_all_events()
while True:
event = alice.wait_for_event()
if event.kind == EventType.MSG_READ:
break
wait_for_chatlist_specific_item(alice, chat_id=alice_chat_bob.id)
chat_item = alice._rpc.get_chatlist_items_by_entries(alice.id, [alice_chat_bob.id])[str(alice_chat_bob.id)]
assert chat_item["summaryStatus"] == const.MessageState.OUT_MDN_RCVD
def test_delivery_status_failed(acfactory: ACFactory) -> None:
"""
Test change status on chatlistitem when status changes failed
"""
(alice,) = acfactory.get_online_accounts(1)
invalid_contact = alice.create_contact("example@example.com", "invalid address")
invalid_chat = alice.get_chat_by_id(alice._rpc.create_chat_by_contact_id(alice.id, invalid_contact.id))
alice.clear_all_events()
failing_message = invalid_chat.send_text("test")
wait_for_chatlist_and_specific_item(alice, invalid_chat.id)
assert failing_message.get_snapshot().state == const.MessageState.OUT_PENDING
while True:
event = alice.wait_for_event()
if event.kind == EventType.MSG_FAILED:
break
wait_for_chatlist_specific_item(alice, invalid_chat.id)
assert failing_message.get_snapshot().state == const.MessageState.OUT_FAILED
def test_download_on_demand(acfactory: ACFactory) -> None:
"""
Test if download on demand emits chatlist update events.
This is only needed for last message in chat, but finding that out is too expensive, so it's always emitted
"""
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("hi")
alice.set_config("download_limit", "1")
event = bob.wait_for_incoming_msg_event()
msg = bob.get_message_by_id(event.msg_id)
chat_id = msg.get_snapshot().chat_id
msg.get_snapshot().chat.accept()
bob.get_chat_by_id(chat_id).send_message(
"Hello World, this message is bigger than 5 bytes",
html=base64.b64encode(os.urandom(300000)).decode("utf-8"),
)
msg_id = alice.wait_for_incoming_msg_event().msg_id
assert alice.get_message_by_id(msg_id).get_snapshot().download_state == const.DownloadState.AVAILABLE
alice.clear_all_events()
chat_id = alice.get_message_by_id(msg_id).get_snapshot().chat_id
alice._rpc.download_full_message(alice.id, msg_id)
wait_for_chatlist_specific_item(alice, chat_id)
def get_multi_account_test_setup(acfactory: ACFactory) -> [Account, Account, Account]:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("hi")
bob.wait_for_incoming_msg_event()
alice_second_device: Account = acfactory.get_unconfigured_account()
alice._rpc.provide_backup.future(alice.id)
backup_code = alice._rpc.get_backup_qr(alice.id)
alice_second_device._rpc.get_backup(alice_second_device.id, backup_code)
alice_second_device.start_io()
alice.clear_all_events()
alice_second_device.clear_all_events()
bob.clear_all_events()
return [alice, alice_second_device, bob, alice_chat_bob]
def test_imap_sync_seen_msgs(acfactory: ACFactory) -> None:
"""
Test that chatlist changed events are emitted for the second device
when the message is marked as read on the first device
"""
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
alice_chat_bob.send_text("hello")
event = bob.wait_for_incoming_msg_event()
msg = bob.get_message_by_id(event.msg_id)
bob_chat_id = msg.get_snapshot().chat_id
msg.get_snapshot().chat.accept()
alice.clear_all_events()
alice_second_device.clear_all_events()
bob.get_chat_by_id(bob_chat_id).send_text("hello")
# make sure alice_second_device already received the message
alice_second_device.wait_for_incoming_msg_event()
event = alice.wait_for_incoming_msg_event()
msg = alice.get_message_by_id(event.msg_id)
alice_second_device.clear_all_events()
msg.mark_seen()
wait_for_chatlist_specific_item(bob, bob_chat_id)
wait_for_chatlist_specific_item(alice, alice_chat_bob.id)
def test_multidevice_sync_chat(acfactory: ACFactory) -> None:
"""
Test multidevice sync: syncing chat visibility and muting across multiple devices
"""
alice, alice_second_device, bob, alice_chat_bob = get_multi_account_test_setup(acfactory)
alice_chat_bob.archive()
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
assert alice_second_device.get_chat_by_id(alice_chat_bob.id).get_basic_snapshot().archived
alice_second_device.clear_all_events()
alice_chat_bob.pin()
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
alice_second_device.clear_all_events()
alice_chat_bob.mute()
wait_for_chatlist_specific_item(alice_second_device, alice_chat_bob.id)
assert alice_second_device.get_chat_by_id(alice_chat_bob.id).get_basic_snapshot().is_muted

View File

@@ -1,618 +0,0 @@
import logging
import pytest
from deltachat_rpc_client import Chat, EventType, SpecialContactId
def test_qr_setup_contact(acfactory, tmp_path) -> None:
alice, bob = acfactory.get_online_accounts(2)
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
bob.wait_for_securejoin_joiner_success()
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
# Test that if Bob changes the key, backwards verification is lost.
logging.info("Bob 2 is created")
bob2 = acfactory.new_configured_account()
bob2.export_self_keys(tmp_path)
logging.info("Bob imports a key")
bob.import_self_keys(tmp_path / "private-key-default.asc")
assert bob.get_config("key_id") == "2"
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert not bob_contact_alice_snapshot.is_verified
@pytest.mark.parametrize("protect", [True, False])
def test_qr_securejoin(acfactory, protect):
alice, bob = acfactory.get_online_accounts(2)
logging.info("Alice creates a verified group")
alice_chat = alice.create_group("Verified group", protect=protect)
assert alice_chat.get_basic_snapshot().is_protected == protect
logging.info("Bob joins verified group")
qr_code, _svg = alice_chat.get_qr_code()
bob.secure_join(qr_code)
# Check that at least some of the handshake messages are deleted.
for ac in [alice, bob]:
while True:
event = ac.wait_for_event()
if event["kind"] == "ImapMessageDeleted":
break
alice.wait_for_securejoin_inviter_success()
# Test that Alice verified Bob's profile.
alice_contact_bob = alice.get_contact_by_addr(bob.get_config("addr"))
alice_contact_bob_snapshot = alice_contact_bob.get_snapshot()
assert alice_contact_bob_snapshot.is_verified
bob.wait_for_securejoin_joiner_success()
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected == protect
# Test that Bob verified Alice's profile.
bob_contact_alice = bob.get_contact_by_addr(alice.get_config("addr"))
bob_contact_alice_snapshot = bob_contact_alice.get_snapshot()
assert bob_contact_alice_snapshot.is_verified
def test_qr_securejoin_contact_request(acfactory) -> None:
"""Alice invites Bob to a group when Bob's chat with Alice is in a contact request mode."""
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hello!"
bob_chat_alice = snapshot.chat
assert bob_chat_alice.get_basic_snapshot().is_contact_request
alice_chat = alice.create_group("Verified group", protect=True)
logging.info("Bob joins verified group")
qr_code, _svg = alice_chat.get_qr_code()
bob.secure_join(qr_code)
while True:
event = bob.wait_for_event()
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
break
# Chat stays being a contact request.
assert bob_chat_alice.get_basic_snapshot().is_contact_request
def test_qr_readreceipt(acfactory) -> None:
alice, bob, charlie = acfactory.get_online_accounts(3)
logging.info("Bob and Charlie setup contact with Alice")
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
charlie.secure_join(qr_code)
for joiner in [bob, charlie]:
joiner.wait_for_securejoin_joiner_success()
logging.info("Alice creates a verified group")
group = alice.create_group("Group", protect=True)
bob_addr = bob.get_config("addr")
charlie_addr = charlie.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_contact_charlie = alice.create_contact(charlie_addr, "Charlie")
group.add_contact(alice_contact_bob)
group.add_contact(alice_contact_charlie)
# Promote a group.
group.send_message(text="Hello")
logging.info("Bob and Charlie receive a group")
bob_msg_id = bob.wait_for_incoming_msg_event().msg_id
bob_message = bob.get_message_by_id(bob_msg_id)
bob_snapshot = bob_message.get_snapshot()
assert bob_snapshot.text == "Hello"
# Charlie receives the same "Hello" message as Bob.
charlie.wait_for_incoming_msg_event()
logging.info("Bob sends a message to the group")
bob_out_message = bob_snapshot.chat.send_message(text="Hi from Bob!")
charlie_msg_id = charlie.wait_for_incoming_msg_event().msg_id
charlie_message = charlie.get_message_by_id(charlie_msg_id)
charlie_snapshot = charlie_message.get_snapshot()
assert charlie_snapshot.text == "Hi from Bob!"
bob_contact_charlie = bob.create_contact(charlie_addr, "Charlie")
assert not bob.get_chat_by_contact(bob_contact_charlie)
logging.info("Charlie reads Bob's message")
charlie_message.mark_seen()
while True:
event = bob.wait_for_event()
if event["kind"] == "MsgRead" and event["msg_id"] == bob_out_message.id:
break
# Receiving a read receipt from Charlie
# should not unblock hidden chat with Charlie for Bob.
assert not bob.get_chat_by_contact(bob_contact_charlie)
def test_setup_contact_resetup(acfactory) -> None:
"""Tests that setup contact works after Alice resets the device and changes the key."""
alice, bob = acfactory.get_online_accounts(2)
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
alice = acfactory.resetup_account(alice)
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
def test_verified_group_recovery(acfactory) -> None:
"""Tests verified group recovery by reverifying a member and sending a message in a group."""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates verified group")
chat = ac1.create_group("Verified group", protect=True)
assert chat.get_basic_snapshot().is_protected
logging.info("ac2 joins verified group")
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
ac3.wait_for_securejoin_joiner_success()
ac3.wait_for_incoming_msg_event() # Member added
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
logging.info("ac2 reverifies with ac3")
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
logging.info("ac3 sends a message to the group")
assert len(ac3_chat.get_contacts()) == 3
ac3_chat.send_text("Hi!")
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hi!"
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert snapshot.text == "Hi!"
# ac1 contact is verified for ac2 because ac3 gossiped ac1 key in the "Hi!" message.
ac1_contact = ac2.get_contact_by_addr(ac1.get_config("addr"))
assert ac1_contact.get_snapshot().is_verified
# ac2 can write messages to the group.
snapshot.chat.send_text("Works again!")
snapshot = ac3.get_message_by_id(ac3.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Works again!"
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Works again!"
ac1_chat_messages = snapshot.chat.get_messages()
ac2_addr = ac2.get_config("addr")
assert ac1_chat_messages[-2].get_snapshot().text == f"Changed setup for {ac2_addr}"
# ac2 is now verified by ac3 for ac1
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
def test_verified_group_member_added_recovery(acfactory) -> None:
"""Tests verified group recovery by reverifiying than removing and adding a member back."""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
logging.info("ac1 creates verified group")
chat = ac1.create_group("Verified group", protect=True)
assert chat.get_basic_snapshot().is_protected
logging.info("ac2 joins verified group")
qr_code, _svg = chat.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
# ac1 has ac2 directly verified.
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == SpecialContactId.SELF
logging.info("ac3 joins verified group")
ac3_chat = ac3.secure_join(qr_code)
ac3.wait_for_securejoin_joiner_success()
ac3.wait_for_incoming_msg_event() # Member added
logging.info("ac2 logs in on a new device")
ac2 = acfactory.resetup_account(ac2)
logging.info("ac2 reverifies with ac3")
qr_code, _svg = ac3.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
logging.info("ac3 sends a message to the group")
assert len(ac3_chat.get_contacts()) == 3
ac3_chat.send_text("Hi!")
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("Received message %s", snapshot.text)
assert snapshot.text == "Hi!"
ac1.wait_for_incoming_msg_event() # Hi!
ac3_contact_ac2 = ac3.get_contact_by_addr(ac2.get_config("addr"))
ac3_chat.remove_contact(ac3_contact_ac2)
ac3_chat.add_contact(ac3_contact_ac2)
msg_id = ac2.wait_for_incoming_msg_event().msg_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert "removed" in snapshot.text
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert "removed" in snapshot.text
event = ac2.wait_for_incoming_msg_event()
msg_id = event.msg_id
chat_id = event.chat_id
message = ac2.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
logging.info("ac2 got event message: %s", snapshot.text)
assert "added" in snapshot.text
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert "added" in snapshot.text
chat = Chat(ac2, chat_id)
chat.send_text("Works again!")
msg_id = ac3.wait_for_incoming_msg_event().msg_id
message = ac3.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert snapshot.text == "Works again!"
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Works again!"
ac1_contact_ac2 = ac1.get_contact_by_addr(ac2.get_config("addr"))
ac1_contact_ac2_snapshot = ac1_contact_ac2.get_snapshot()
assert ac1_contact_ac2_snapshot.is_verified
assert ac1_contact_ac2_snapshot.verifier_id == ac1.get_contact_by_addr(ac3.get_config("addr")).id
# ac2 is now verified by ac3 for ac1
ac1_contact_ac3 = ac1.get_contact_by_addr(ac3.get_config("addr"))
assert ac1_contact_ac2.get_snapshot().verifier_id == ac1_contact_ac3.id
def test_qr_join_chat_with_pending_bobstate_issue4894(acfactory):
"""Regression test for
issue <https://github.com/deltachat/deltachat-core-rust/issues/4894>.
"""
ac1, ac2, ac3, ac4 = acfactory.get_online_accounts(4)
logging.info("ac3: verify with ac2")
qr_code, _svg = ac2.get_qr_code()
ac3.secure_join(qr_code)
ac2.wait_for_securejoin_inviter_success()
# in order for ac2 to have pending bobstate with a verified group
# we first create a fully joined verified group, and then start
# joining a second time but interrupt it, to create pending bob state
logging.info("ac1: create verified group that ac2 fully joins")
ch1 = ac1.create_group("Group", protect=True)
qr_code, _svg = ch1.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
# ensure ac1 can write and ac2 receives messages in verified chat
ch1.send_text("ac1 says hello")
while 1:
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
if snapshot.text == "ac1 says hello":
assert snapshot.chat.get_basic_snapshot().is_protected
break
logging.info("ac1: let ac2 join again but shutoff ac1 in the middle of securejoin")
qr_code, _svg = ch1.get_qr_code()
ac2.secure_join(qr_code)
ac1.remove()
logging.info("ac2 now has pending bobstate but ac1 is shutoff")
# we meanwhile expect ac3/ac2 verification started in the beginning to have completed
assert ac3.get_contact_by_addr(ac2.get_config("addr")).get_snapshot().is_verified
assert ac2.get_contact_by_addr(ac3.get_config("addr")).get_snapshot().is_verified
logging.info("ac3: create a verified group VG with ac2")
vg = ac3.create_group("ac3-created", protect=True)
vg.add_contact(ac3.get_contact_by_addr(ac2.get_config("addr")))
# ensure ac2 receives message in VG
vg.send_text("hello")
while 1:
msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
if msg.text == "hello":
assert msg.chat.get_basic_snapshot().is_protected
break
logging.info("ac3: create a join-code for group VG and let ac4 join, check that ac2 got it")
qr_code, _svg = vg.get_qr_code()
ac4.secure_join(qr_code)
ac3.wait_for_securejoin_inviter_success()
while 1:
ev = ac2.wait_for_event()
if "added by unrelated SecureJoin" in str(ev):
return
def test_qr_new_group_unblocked(acfactory):
"""Regression test for a bug introduced 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("Group for joining", protect=True)
qr_code, _svg = ac1_chat.get_qr_code()
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
ac1_new_chat = ac1.create_group("Another group")
ac1_new_chat.add_contact(ac1.get_contact_by_addr(ac2.get_config("addr")))
# Receive "Member added" message.
ac2.wait_for_incoming_msg_event()
ac1_new_chat.send_text("Hello!")
ac2_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert ac2_msg.text == "Hello!"
assert ac2_msg.chat.get_basic_snapshot().is_contact_request
def test_aeap_flow_verified(acfactory):
"""Test that a new address is added to a contact when it changes its address."""
ac1, ac2, ac1new = acfactory.get_online_accounts(3)
logging.info("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group("hello", protect=True)
assert chat.get_basic_snapshot().is_protected
qr_code, _svg = chat.get_qr_code()
logging.info("ac2: start QR-code based join-group protocol")
ac2.secure_join(qr_code)
ac1.wait_for_securejoin_inviter_success()
logging.info("sending first message")
msg_out = chat.send_text("old address").get_snapshot()
logging.info("receiving first message")
ac2.wait_for_incoming_msg_event() # member added message
msg_in_1 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert msg_in_1.text == msg_out.text
logging.info("changing email account")
ac1.set_config("addr", ac1new.get_config("addr"))
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
ac1.stop_io()
ac1.configure()
ac1.start_io()
logging.info("sending second message")
msg_out = chat.send_text("changed address").get_snapshot()
logging.info("receiving second message")
msg_in_2 = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
msg_in_2_snapshot = msg_in_2.get_snapshot()
assert msg_in_2_snapshot.text == msg_out.text
assert msg_in_2_snapshot.chat.id == msg_in_1.chat.id
assert msg_in_2.get_sender_contact().get_snapshot().address == ac1new.get_config("addr")
assert len(msg_in_2_snapshot.chat.get_contacts()) == 2
assert ac1new.get_config("addr") in [
contact.get_snapshot().address for contact in msg_in_2_snapshot.chat.get_contacts()
]
def test_gossip_verification(acfactory) -> None:
alice, bob, carol = acfactory.get_online_accounts(3)
# Bob verifies Alice.
qr_code, _svg = alice.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
# Bob verifies Carol.
qr_code, _svg = carol.get_qr_code()
bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
bob_contact_alice = bob.create_contact(alice.get_config("addr"), "Alice")
bob_contact_carol = bob.create_contact(carol.get_config("addr"), "Carol")
carol_contact_alice = carol.create_contact(alice.get_config("addr"), "Alice")
logging.info("Bob creates an Autocrypt group")
bob_group_chat = bob.create_group("Autocrypt Group")
assert not bob_group_chat.get_basic_snapshot().is_protected
bob_group_chat.add_contact(bob_contact_alice)
bob_group_chat.add_contact(bob_contact_carol)
bob_group_chat.send_message(text="Hello Autocrypt group")
snapshot = carol.get_message_by_id(carol.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hello Autocrypt group"
assert snapshot.show_padlock
# Autocrypt group does not propagate verification.
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
assert not carol_contact_alice_snapshot.is_verified
logging.info("Bob creates a Securejoin group")
bob_group_chat = bob.create_group("Securejoin Group", protect=True)
assert bob_group_chat.get_basic_snapshot().is_protected
bob_group_chat.add_contact(bob_contact_alice)
bob_group_chat.add_contact(bob_contact_carol)
bob_group_chat.send_message(text="Hello Securejoin group")
snapshot = carol.get_message_by_id(carol.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hello Securejoin group"
assert snapshot.show_padlock
# Securejoin propagates verification.
carol_contact_alice_snapshot = carol_contact_alice.get_snapshot()
assert carol_contact_alice_snapshot.is_verified
def test_securejoin_after_contact_resetup(acfactory) -> None:
"""
Regression test for a bug that prevented joining verified group with a QR code
if the group is already created and contains
a contact with inconsistent (Autocrypt and verified keys exist but don't match) key state.
"""
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
# ac3 creates protected group with ac1.
ac3_chat = ac3.create_group("Verified group", protect=True)
# ac1 joins ac3 group.
ac3_qr_code, _svg = ac3_chat.get_qr_code()
ac1.secure_join(ac3_qr_code)
ac1.wait_for_securejoin_joiner_success()
# ac1 waits for member added message and creates a QR code.
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
ac1_qr_code, _svg = snapshot.chat.get_qr_code()
# ac2 verifies ac1
qr_code, _svg = ac1.get_qr_code()
ac2.secure_join(qr_code)
ac2.wait_for_securejoin_joiner_success()
# ac1 is verified for ac2.
ac2_contact_ac1 = ac2.create_contact(ac1.get_config("addr"), "")
assert ac2_contact_ac1.get_snapshot().is_verified
# ac1 resetups the account.
ac1 = acfactory.resetup_account(ac1)
# ac1 sends a message to ac2.
ac1_contact_ac2 = ac1.create_contact(ac2.get_config("addr"), "")
ac1_chat_ac2 = ac1_contact_ac2.create_chat()
ac1_chat_ac2.send_text("Hello!")
# ac2 receives a message.
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hello!"
# ac1 is no longer verified for ac2 as new Autocrypt key is not the same as old verified key.
assert not ac2_contact_ac1.get_snapshot().is_verified
# ac1 goes offline.
ac1.remove()
# Scanning a QR code results in creating an unprotected group with an inviter.
# In this case inviter is ac1 which has an inconsistent key state.
# Normally inviter becomes verified as a result of Securejoin protocol
# and then the group chat becomes verified when "Member added" is received,
# but in this case ac1 is offline and this Securejoin process will never finish.
logging.info("ac2 scans ac1 QR code, this is not expected to finish")
ac2.secure_join(ac1_qr_code)
logging.info("ac2 scans ac3 QR code")
ac2.secure_join(ac3_qr_code)
logging.info("ac2 waits for joiner success")
ac2.wait_for_securejoin_joiner_success()
# Wait for member added.
logging.info("ac2 waits for member added message")
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.is_info
ac2_chat = snapshot.chat
assert ac2_chat.get_basic_snapshot().is_protected
assert len(ac2_chat.get_contacts()) == 3
# ac1 is still "not verified" for ac2 due to inconsistent state.
assert not ac2_contact_ac1.get_snapshot().is_verified
def test_withdraw_securejoin_qr(acfactory):
alice, bob = acfactory.get_online_accounts(2)
logging.info("Alice creates a verified group")
alice_chat = alice.create_group("Verified group", protect=True)
assert alice_chat.get_basic_snapshot().is_protected
logging.info("Bob joins verified group")
qr_code, _svg = alice_chat.get_qr_code()
bob_chat = bob.secure_join(qr_code)
bob.wait_for_securejoin_joiner_success()
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Member Me ({}) added by {}.".format(bob.get_config("addr"), alice.get_config("addr"))
assert snapshot.chat.get_basic_snapshot().is_protected
bob_chat.leave()
snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Group left by {}.".format(bob.get_config("addr"))
logging.info("Alice withdraws QR code.")
qr = alice.check_qr(qr_code)
assert qr["kind"] == "withdrawVerifyGroup"
alice.set_config_from_qr(qr_code)
logging.info("Bob scans withdrawn QR code.")
bob_chat = bob.secure_join(qr_code)
logging.info("Bob scanned withdrawn QR code")
while True:
event = alice.wait_for_event()
if event.kind == EventType.MSGS_CHANGED and event.chat_id != 0:
break
snapshot = alice.get_message_by_id(event.msg_id).get_snapshot()
assert snapshot.text == "Cannot establish guaranteed end-to-end encryption with {}".format(bob.get_config("addr"))

View File

@@ -1,38 +1,31 @@
import concurrent.futures import asyncio
import json
import logging
import os
import subprocess
import time
from unittest.mock import MagicMock from unittest.mock import MagicMock
import pytest import pytest
from deltachat_rpc_client import Contact, EventType, Message, events from deltachat_rpc_client import EventType, events
from deltachat_rpc_client.const import DownloadState, MessageState
from deltachat_rpc_client.direct_imap import DirectImap
from deltachat_rpc_client.rpc import JsonRpcError from deltachat_rpc_client.rpc import JsonRpcError
def test_system_info(rpc) -> None: @pytest.mark.asyncio()
system_info = rpc.get_system_info() async def test_system_info(rpc) -> None:
system_info = await rpc.get_system_info()
assert "arch" in system_info assert "arch" in system_info
assert "deltachat_core_version" in system_info assert "deltachat_core_version" in system_info
def test_sleep(rpc) -> None: @pytest.mark.asyncio()
async def test_sleep(rpc) -> None:
"""Test that long-running task does not block short-running task from completion.""" """Test that long-running task does not block short-running task from completion."""
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor: sleep_5_task = asyncio.create_task(rpc.sleep(5.0))
sleep_5_future = executor.submit(rpc.sleep, 5.0) sleep_3_task = asyncio.create_task(rpc.sleep(3.0))
sleep_3_future = executor.submit(rpc.sleep, 3.0) done, pending = await asyncio.wait([sleep_5_task, sleep_3_task], return_when=asyncio.FIRST_COMPLETED)
done, pending = concurrent.futures.wait( assert sleep_3_task in done
[sleep_5_future, sleep_3_future], assert sleep_5_task in pending
return_when=concurrent.futures.FIRST_COMPLETED, sleep_5_task.cancel()
)
assert sleep_3_future in done
assert sleep_5_future in pending
def test_email_address_validity(rpc) -> None: @pytest.mark.asyncio()
async def test_email_address_validity(rpc) -> None:
valid_addresses = [ valid_addresses = [
"email@example.com", "email@example.com",
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail", "36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
@@ -40,16 +33,17 @@ def test_email_address_validity(rpc) -> None:
invalid_addresses = ["email@", "example.com", "emai221"] invalid_addresses = ["email@", "example.com", "emai221"]
for addr in valid_addresses: for addr in valid_addresses:
assert rpc.check_email_validity(addr) assert await rpc.check_email_validity(addr)
for addr in invalid_addresses: for addr in invalid_addresses:
assert not rpc.check_email_validity(addr) assert not await rpc.check_email_validity(addr)
def test_acfactory(acfactory) -> None: @pytest.mark.asyncio()
account = acfactory.new_configured_account() async def test_acfactory(acfactory) -> None:
account = await acfactory.new_configured_account()
while True: while True:
event = account.wait_for_event() event = await account.wait_for_event()
if event.kind == EventType.CONFIGURE_PROGRESS: if event.type == EventType.CONFIGURE_PROGRESS:
assert event.progress != 0 # Progress 0 indicates error. assert event.progress != 0 # Progress 0 indicates error.
if event.progress == 1000: # Success if event.progress == 1000: # Success
break break
@@ -58,235 +52,232 @@ def test_acfactory(acfactory) -> None:
print("Successful configuration") print("Successful configuration")
def test_configure_starttls(acfactory) -> None: @pytest.mark.asyncio()
account = acfactory.new_preconfigured_account() async def test_configure_starttls(acfactory) -> None:
account = await acfactory.new_preconfigured_account()
# Use STARTTLS # Use STARTTLS
account.set_config("mail_security", "2") await account.set_config("mail_security", "2")
account.set_config("send_security", "2") await account.set_config("send_security", "2")
account.configure() await account.configure()
assert account.is_configured() assert await account.is_configured()
def test_account(acfactory) -> None: @pytest.mark.asyncio()
alice, bob = acfactory.get_online_accounts(2) async def test_account(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr") bob_addr = await bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob") alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat() alice_chat_bob = await alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!") await alice_chat_bob.send_text("Hello!")
while True: while True:
event = bob.wait_for_event() event = await bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG: if event.type == EventType.INCOMING_MSG:
chat_id = event.chat_id chat_id = event.chat_id
msg_id = event.msg_id msg_id = event.msg_id
break break
message = bob.get_message_by_id(msg_id) message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot() snapshot = await message.get_snapshot()
assert snapshot.chat_id == chat_id assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!" assert snapshot.text == "Hello!"
bob.mark_seen_messages([message]) await bob.mark_seen_messages([message])
assert alice != bob assert alice != bob
assert repr(alice) assert repr(alice)
assert alice.get_info().level assert (await alice.get_info()).level
assert alice.get_size() assert await alice.get_size()
assert alice.is_configured() assert await alice.is_configured()
assert not alice.get_avatar() assert not await alice.get_avatar()
assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob assert await alice.get_contact_by_addr(bob_addr) == alice_contact_bob
assert alice.get_contacts() assert await alice.get_contacts()
assert alice.get_contacts(snapshot=True) assert await alice.get_contacts(snapshot=True)
assert alice.self_contact assert alice.self_contact
assert alice.get_chatlist() assert await alice.get_chatlist()
assert alice.get_chatlist(snapshot=True) assert await alice.get_chatlist(snapshot=True)
assert alice.get_qr_code() assert await alice.get_qr_code()
assert alice.get_fresh_messages() await alice.get_fresh_messages()
assert alice.get_next_messages() await alice.get_fresh_messages_in_arrival_order()
# Test sending empty message. group = await alice.create_group("test group")
assert len(bob.wait_next_messages()) == 0 await group.add_contact(alice_contact_bob)
alice_chat_bob.send_text("") group_msg = await group.send_message(text="hello")
messages = bob.wait_next_messages()
assert len(messages) == 1
message = messages[0]
snapshot = message.get_snapshot()
assert snapshot.text == ""
bob.mark_seen_messages([message])
group = alice.create_group("test group")
group.add_contact(alice_contact_bob)
group_msg = group.send_message(text="hello")
assert group_msg == alice.get_message_by_id(group_msg.id) assert group_msg == alice.get_message_by_id(group_msg.id)
assert group == alice.get_chat_by_id(group.id) assert group == alice.get_chat_by_id(group.id)
alice.delete_messages([group_msg]) await alice.delete_messages([group_msg])
alice.set_config("selfstatus", "test") await alice.set_config("selfstatus", "test")
assert alice.get_config("selfstatus") == "test" assert await alice.get_config("selfstatus") == "test"
alice.update_config(selfstatus="test2") await alice.update_config(selfstatus="test2")
assert alice.get_config("selfstatus") == "test2" assert await alice.get_config("selfstatus") == "test2"
assert not alice.get_blocked_contacts() assert not await alice.get_blocked_contacts()
alice_contact_bob.block() await alice_contact_bob.block()
blocked_contacts = alice.get_blocked_contacts() blocked_contacts = await alice.get_blocked_contacts()
assert blocked_contacts assert blocked_contacts
assert blocked_contacts[0].contact == alice_contact_bob assert blocked_contacts[0].contact == alice_contact_bob
bob.remove() await bob.remove()
alice.stop_io() await alice.stop_io()
def test_chat(acfactory) -> None: @pytest.mark.asyncio()
alice, bob = acfactory.get_online_accounts(2) async def test_chat(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr") bob_addr = await bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob") alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat() alice_chat_bob = await alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!") await alice_chat_bob.send_text("Hello!")
event = bob.wait_for_incoming_msg_event() while True:
chat_id = event.chat_id event = await bob.wait_for_event()
msg_id = event.msg_id if event.type == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = bob.get_message_by_id(msg_id) message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot() snapshot = await message.get_snapshot()
assert snapshot.chat_id == chat_id assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!" assert snapshot.text == "Hello!"
bob_chat_alice = bob.get_chat_by_id(chat_id) bob_chat_alice = bob.get_chat_by_id(chat_id)
assert alice_chat_bob != bob_chat_alice assert alice_chat_bob != bob_chat_alice
assert repr(alice_chat_bob) assert repr(alice_chat_bob)
alice_chat_bob.delete() await alice_chat_bob.delete()
assert not bob_chat_alice.can_send() await bob_chat_alice.accept()
bob_chat_alice.accept() await bob_chat_alice.block()
assert bob_chat_alice.can_send() bob_chat_alice = await snapshot.sender.create_chat()
bob_chat_alice.block() await bob_chat_alice.mute()
bob_chat_alice = snapshot.sender.create_chat() await bob_chat_alice.unmute()
bob_chat_alice.mute() await bob_chat_alice.pin()
bob_chat_alice.unmute() await bob_chat_alice.unpin()
bob_chat_alice.pin() await bob_chat_alice.archive()
bob_chat_alice.unpin() await bob_chat_alice.unarchive()
bob_chat_alice.archive()
bob_chat_alice.unarchive()
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
bob_chat_alice.set_name("test") await bob_chat_alice.set_name("test")
bob_chat_alice.set_ephemeral_timer(300) await bob_chat_alice.set_ephemeral_timer(300)
bob_chat_alice.get_encryption_info() await bob_chat_alice.get_encryption_info()
group = alice.create_group("test group") group = await alice.create_group("test group")
group.add_contact(alice_contact_bob) await group.add_contact(alice_contact_bob)
group.get_qr_code() await group.get_qr_code()
snapshot = group.get_basic_snapshot() snapshot = await group.get_basic_snapshot()
assert snapshot.name == "test group" assert snapshot.name == "test group"
group.set_name("new name") await group.set_name("new name")
snapshot = group.get_full_snapshot() snapshot = await group.get_full_snapshot()
assert snapshot.name == "new name" assert snapshot.name == "new name"
msg = group.send_message(text="hi") msg = await group.send_message(text="hi")
assert (msg.get_snapshot()).text == "hi" assert (await msg.get_snapshot()).text == "hi"
group.forward_messages([msg]) await group.forward_messages([msg])
group.set_draft(text="test draft") await group.set_draft(text="test draft")
draft = group.get_draft() draft = await group.get_draft()
assert draft.text == "test draft" assert draft.text == "test draft"
group.remove_draft() await group.remove_draft()
assert not group.get_draft() assert not await group.get_draft()
assert group.get_messages() assert await group.get_messages()
group.get_fresh_message_count() await group.get_fresh_message_count()
group.mark_noticed() await group.mark_noticed()
assert group.get_contacts() assert await group.get_contacts()
group.remove_contact(alice_chat_bob) await group.remove_contact(alice_chat_bob)
group.get_locations() await group.get_locations()
def test_contact(acfactory) -> None: @pytest.mark.asyncio()
alice, bob = acfactory.get_online_accounts(2) async def test_contact(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr") bob_addr = await bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob") alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id) assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
assert repr(alice_contact_bob) assert repr(alice_contact_bob)
alice_contact_bob.block() await alice_contact_bob.block()
alice_contact_bob.unblock() await alice_contact_bob.unblock()
alice_contact_bob.set_name("new name") await alice_contact_bob.set_name("new name")
alice_contact_bob.get_encryption_info() await alice_contact_bob.get_encryption_info()
snapshot = alice_contact_bob.get_snapshot() snapshot = await alice_contact_bob.get_snapshot()
assert snapshot.address == bob_addr assert snapshot.address == bob_addr
alice_contact_bob.create_chat() await alice_contact_bob.create_chat()
def test_message(acfactory) -> None: @pytest.mark.asyncio()
alice, bob = acfactory.get_online_accounts(2) async def test_message(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr") bob_addr = await bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob") alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat() alice_chat_bob = await alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!") await alice_chat_bob.send_text("Hello!")
event = bob.wait_for_incoming_msg_event() while True:
chat_id = event.chat_id event = await bob.wait_for_event()
msg_id = event.msg_id if event.type == EventType.INCOMING_MSG:
chat_id = event.chat_id
msg_id = event.msg_id
break
message = bob.get_message_by_id(msg_id) message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot() snapshot = await message.get_snapshot()
assert snapshot.chat_id == chat_id assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!" assert snapshot.text == "Hello!"
assert not snapshot.is_bot assert not snapshot.is_bot
assert repr(message) assert repr(message)
with pytest.raises(JsonRpcError): # chat is not accepted with pytest.raises(JsonRpcError): # chat is not accepted
snapshot.chat.send_text("hi") await snapshot.chat.send_text("hi")
snapshot.chat.accept() await snapshot.chat.accept()
snapshot.chat.send_text("hi") await snapshot.chat.send_text("hi")
message.mark_seen() await message.mark_seen()
message.send_reaction("😎") await message.send_reaction("😎")
reactions = message.get_reactions()
assert reactions
snapshot = message.get_snapshot()
assert reactions == snapshot.reactions
def test_is_bot(acfactory) -> None: @pytest.mark.asyncio()
async def test_is_bot(acfactory) -> None:
"""Test that we can recognize messages submitted by bots.""" """Test that we can recognize messages submitted by bots."""
alice, bob = acfactory.get_online_accounts(2) alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr") bob_addr = await bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob") alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat() alice_chat_bob = await alice_contact_bob.create_chat()
# Alice becomes a bot. # Alice becomes a bot.
alice.set_config("bot", "1") await alice.set_config("bot", "1")
alice_chat_bob.send_text("Hello!") await alice_chat_bob.send_text("Hello!")
while True: while True:
event = bob.wait_for_event() event = await bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG: if event.type == EventType.INCOMING_MSG:
msg_id = event.msg_id msg_id = event.msg_id
message = bob.get_message_by_id(msg_id) message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot() snapshot = await message.get_snapshot()
assert snapshot.chat_id == event.chat_id assert snapshot.chat_id == event.chat_id
assert snapshot.text == "Hello!" assert snapshot.text == "Hello!"
assert snapshot.is_bot assert snapshot.is_bot
break break
def test_bot(acfactory) -> None: @pytest.mark.asyncio()
async def test_bot(acfactory) -> None:
mock = MagicMock() mock = MagicMock()
user = (acfactory.get_online_accounts(1))[0] user = (await acfactory.get_online_accounts(1))[0]
bot = acfactory.new_configured_bot() bot = await acfactory.new_configured_bot()
bot2 = acfactory.new_configured_bot() bot2 = await acfactory.new_configured_bot()
assert bot.is_configured() assert await bot.is_configured()
assert bot.account.get_config("bot") == "1" assert await bot.account.get_config("bot") == "1"
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG) hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
bot.add_hook(*hook) bot.add_hook(*hook)
event = acfactory.process_message(from_account=user, to_client=bot, text="Hello!") event = await acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
snapshot = bot.account.get_message_by_id(event.msg_id).get_snapshot() snapshot = await bot.account.get_message_by_id(event.msg_id).get_snapshot()
assert not snapshot.is_bot assert not snapshot.is_bot
mock.hook.assert_called_once_with(event.msg_id) mock.hook.assert_called_once_with(event.msg_id)
bot.remove_hook(*hook) bot.remove_hook(*hook)
@@ -298,318 +289,17 @@ def test_bot(acfactory) -> None:
hook = track, events.NewMessage(r"hello") hook = track, events.NewMessage(r"hello")
bot.add_hook(*hook) bot.add_hook(*hook)
bot.add_hook(track, events.NewMessage(command="/help")) bot.add_hook(track, events.NewMessage(command="/help"))
event = acfactory.process_message(from_account=user, to_client=bot, text="hello") event = await acfactory.process_message(from_account=user, to_client=bot, text="hello")
mock.hook.assert_called_with(event.msg_id) mock.hook.assert_called_with(event.msg_id)
event = acfactory.process_message(from_account=user, to_client=bot, text="hello!") event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!")
mock.hook.assert_called_with(event.msg_id) mock.hook.assert_called_with(event.msg_id)
acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello") await acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots
acfactory.process_message(from_account=user, to_client=bot, text="hey!") await acfactory.process_message(from_account=user, to_client=bot, text="hey!")
assert len(mock.hook.mock_calls) == 2 assert len(mock.hook.mock_calls) == 2
bot.remove_hook(*hook) bot.remove_hook(*hook)
mock.hook.reset_mock() mock.hook.reset_mock()
acfactory.process_message(from_account=user, to_client=bot, text="hello") await acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = acfactory.process_message(from_account=user, to_client=bot, text="/help") event = await acfactory.process_message(from_account=user, to_client=bot, text="/help")
mock.hook.assert_called_once_with(event.msg_id) mock.hook.assert_called_once_with(event.msg_id)
def test_wait_next_messages(acfactory) -> None:
alice = acfactory.new_configured_account()
# Create a bot account so it does not receive device messages in the beginning.
bot = acfactory.new_preconfigured_account()
bot.set_config("bot", "1")
bot.configure()
# There are no old messages and the call returns immediately.
assert not bot.wait_next_messages()
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
# Bot starts waiting for messages.
next_messages_task = executor.submit(bot.wait_next_messages)
bot_addr = bot.get_config("addr")
alice_contact_bot = alice.create_contact(bot_addr, "Bot")
alice_chat_bot = alice_contact_bot.create_chat()
alice_chat_bot.send_text("Hello!")
next_messages = next_messages_task.result()
assert len(next_messages) == 1
snapshot = next_messages[0].get_snapshot()
assert snapshot.text == "Hello!"
def test_import_export_backup(acfactory, tmp_path) -> None:
alice = acfactory.new_configured_account()
alice.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
alice2 = acfactory.get_unconfigured_account()
alice2.import_backup(files[0])
assert alice2.manager.get_system_info()
def test_import_export_keys(acfactory, tmp_path) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello Bob!")
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hello Bob!"
# Alice resetups account, but keeps the key.
alice_keys_path = tmp_path / "alice_keys"
alice_keys_path.mkdir()
alice.export_self_keys(alice_keys_path)
alice = acfactory.resetup_account(alice)
alice.import_self_keys(alice_keys_path)
snapshot.chat.accept()
snapshot.chat.send_text("Hello Alice!")
snapshot = alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "Hello Alice!"
assert snapshot.show_padlock
def test_openrpc_command_line() -> None:
"""Test that "deltachat-rpc-server --openrpc" command returns an OpenRPC specification."""
out = subprocess.run(["deltachat-rpc-server", "--openrpc"], capture_output=True, check=True).stdout
openrpc = json.loads(out)
assert "openrpc" in openrpc
assert "methods" in openrpc
def test_provider_info(rpc) -> None:
account_id = rpc.add_account()
provider_info = rpc.get_provider_info(account_id, "example.org")
assert provider_info["id"] == "example.com"
provider_info = rpc.get_provider_info(account_id, "uep7oiw4ahtaizuloith.org")
assert provider_info is None
# Test MX record resolution.
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info["id"] == "gmail"
# Disable MX record resolution.
rpc.set_config(account_id, "socks5_enabled", "1")
provider_info = rpc.get_provider_info(account_id, "github.com")
assert provider_info is None
def test_mdn_doesnt_break_autocrypt(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
# Bob creates chat manually so chat with Alice is accepted.
alice_chat_bob = alice_contact_bob.create_chat()
# Alice sends a message to Bob.
alice_chat_bob.send_text("Hello Bob!")
event = bob.wait_for_incoming_msg_event()
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
# Bob sends a message to Alice.
bob_chat_alice = snapshot.chat
bob_chat_alice.accept()
bob_chat_alice.send_text("Hello Alice!")
event = alice.wait_for_incoming_msg_event()
msg_id = event.msg_id
message = alice.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert snapshot.show_padlock
# Alice reads Bob's message.
message.mark_seen()
while True:
event = bob.wait_for_event()
if event.kind == EventType.MSG_READ:
break
# Bob sends a message to Alice, it should also be encrypted.
bob_chat_alice.send_text("Hi Alice!")
event = alice.wait_for_incoming_msg_event()
msg_id = event.msg_id
message = alice.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
assert snapshot.show_padlock
def test_reaction_to_partially_fetched_msg(acfactory, tmp_path):
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
messages are received out of order".
If the Inbox contains X small messages followed by Y large messages followed by Z small
messages, Delta Chat first downloaded a batch of X+Z messages, and then a batch of Y messages.
This bug was discovered by @Simon-Laux while testing reactions PR #3644 and can be reproduced
with online test as follows:
- Bob enables download limit and goes offline.
- Alice sends a large message to Bob and reacts to this message with a thumbs-up.
- Bob goes online
- Bob first processes a reaction message and throws it away because there is no corresponding
message, then processes a partially downloaded message.
- As a result, Bob does not see a reaction
"""
download_limit = 300000
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_addr = ac1.get_config("addr")
chat = ac1.create_chat(ac2)
ac2.set_config("download_limit", str(download_limit))
ac2.stop_io()
logging.info("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)))
for m in msgs:
m.wait_until_delivered()
logging.info("sending a reaction to the large message from ac1 to ac2")
# TODO: Find the reason of an occasional message reordering on the server (so that the reaction
# has a lower UID than the previous message). W/a is to sleep for some time to let the reaction
# have a later INTERNALDATE.
time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
msgs.append(msgs[-1].send_reaction(react_str))
msgs[-1].wait_until_delivered()
ac2.start_io()
logging.info("wait for ac2 to receive a reaction")
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
assert msg2.get_sender_contact().get_snapshot().address == ac1_addr
assert msg2.get_snapshot().download_state == DownloadState.AVAILABLE
reactions = msg2.get_reactions()
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
assert len(contacts) == 1
assert contacts[0].get_snapshot().address == ac1_addr
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
def test_reactions_for_a_reordering_move(acfactory):
"""When a batch of messages is moved from Inbox to DeltaChat folder with a single MOVE command,
their UIDs may be reordered (e.g. Gmail is known for that) which led to that messages were
processed by receive_imf in the wrong order, and, particularly, reactions were processed before
messages they refer to and thus dropped.
"""
(ac1,) = acfactory.get_online_accounts(1)
ac2 = acfactory.new_preconfigured_account()
ac2.configure()
ac2.set_config("mvbox_move", "1")
ac2.bring_online()
chat1 = acfactory.get_accepted_chat(ac1, ac2)
ac2.stop_io()
logging.info("sending message + reaction from ac1 to ac2")
msg1 = chat1.send_text("hi")
msg1.wait_until_delivered()
# It's is sad, but messages must differ in their INTERNALDATEs to be processed in the correct
# order by DC, and most (if not all) mail servers provide only seconds precision.
time.sleep(1.1)
react_str = "\N{THUMBS UP SIGN}"
msg1.send_reaction(react_str).wait_until_delivered()
logging.info("moving messages to ac2's DeltaChat folder in the reverse order")
ac2_direct_imap = DirectImap(ac2)
ac2_direct_imap.connect()
for uid in sorted([m.uid for m in ac2_direct_imap.get_all_messages()], reverse=True):
ac2_direct_imap.conn.move(uid, "DeltaChat")
logging.info("receiving messages by ac2")
ac2.start_io()
msg2 = Message(ac2, ac2.wait_for_reactions_changed().msg_id)
assert msg2.get_snapshot().text == msg1.get_snapshot().text
reactions = msg2.get_reactions()
contacts = [Contact(ac2, int(i)) for i in reactions.reactions_by_contact]
assert len(contacts) == 1
assert contacts[0].get_snapshot().address == ac1.get_config("addr")
assert list(reactions.reactions_by_contact.values())[0] == [react_str]
@pytest.mark.parametrize("n_accounts", [3, 2])
def test_download_limit_chat_assignment(acfactory, tmp_path, n_accounts):
download_limit = 300000
alice, *others = acfactory.get_online_accounts(n_accounts)
bob = others[0]
alice_group = alice.create_group("test group")
for account in others:
chat = account.create_chat(alice)
chat.send_text("Hello Alice!")
assert alice.get_message_by_id(alice.wait_for_incoming_msg_event().msg_id).get_snapshot().text == "Hello Alice!"
contact_addr = account.get_config("addr")
contact = alice.create_contact(contact_addr, "")
alice_group.add_contact(contact)
if n_accounts == 2:
bob_chat_alice = bob.create_chat(alice)
bob.set_config("download_limit", str(download_limit))
alice_group.send_text("hi")
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "hi"
bob_group = snapshot.chat
path = tmp_path / "large"
path.write_bytes(os.urandom(download_limit + 1))
for i in range(10):
logging.info("Sending message %s", i)
alice_group.send_file(str(path))
snapshot = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.download_state == DownloadState.AVAILABLE
if n_accounts > 2:
assert snapshot.chat == bob_group
else:
# Group contains only Alice and Bob,
# so partially downloaded messages are
# hard to distinguish from private replies to group messages.
#
# Message may be a private reply, so we assign it to 1:1 chat with Alice.
assert snapshot.chat == bob_chat_alice
def test_markseen_contact_request(acfactory, tmp_path):
"""
Test that seen status is synchronized for contact request messages
even though read receipt is not sent.
"""
alice, bob = acfactory.get_online_accounts(2)
# Bob sets up a second device.
bob.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
bob2 = acfactory.get_unconfigured_account()
bob2.import_backup(files[0])
bob2.start_io()
alice_chat_bob = alice.create_chat(bob)
alice_chat_bob.send_text("Hello Bob!")
message = bob.get_message_by_id(bob.wait_for_incoming_msg_event().msg_id)
message2 = bob2.get_message_by_id(bob2.wait_for_incoming_msg_event().msg_id)
assert message2.get_snapshot().state == MessageState.IN_FRESH
message.mark_seen()
while True:
event = bob2.wait_for_event()
if event.kind == EventType.MSGS_NOTICED:
break
assert message2.get_snapshot().state == MessageState.IN_SEEN

View File

@@ -1,22 +1,24 @@
import pytest
from deltachat_rpc_client import EventType from deltachat_rpc_client import EventType
def test_webxdc(acfactory) -> None: @pytest.mark.asyncio()
alice, bob = acfactory.get_online_accounts(2) async def test_webxdc(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr") bob_addr = await bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob") alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat() alice_chat_bob = await alice_contact_bob.create_chat()
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc") await alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
while True: while True:
event = bob.wait_for_event() event = await bob.wait_for_event()
if event.kind == EventType.INCOMING_MSG: if event.type == EventType.INCOMING_MSG:
bob_chat_alice = bob.get_chat_by_id(event.chat_id) bob_chat_alice = bob.get_chat_by_id(event.chat_id)
message = bob.get_message_by_id(event.msg_id) message = bob.get_message_by_id(event.msg_id)
break break
webxdc_info = message.get_webxdc_info() webxdc_info = await message.get_webxdc_info()
assert webxdc_info == { assert webxdc_info == {
"document": None, "document": None,
"icon": "icon.png", "icon": "icon.png",
@@ -26,32 +28,20 @@ def test_webxdc(acfactory) -> None:
"summary": None, "summary": None,
} }
status_updates = message.get_webxdc_status_updates() status_updates = await message.get_webxdc_status_updates()
assert status_updates == [] assert status_updates == []
bob_chat_alice.accept() await bob_chat_alice.accept()
message.send_webxdc_status_update({"payload": 42}, "") await message.send_webxdc_status_update({"payload": 42}, "")
message.send_webxdc_status_update({"payload": "Second update"}, "description") await message.send_webxdc_status_update({"payload": "Second update"}, "description")
status_updates = message.get_webxdc_status_updates() status_updates = await message.get_webxdc_status_updates()
assert status_updates == [ assert status_updates == [
{"payload": 42, "serial": 1, "max_serial": 2}, {"payload": 42, "serial": 1, "max_serial": 2},
{"payload": "Second update", "serial": 2, "max_serial": 2}, {"payload": "Second update", "serial": 2, "max_serial": 2},
] ]
status_updates = message.get_webxdc_status_updates(1) status_updates = await message.get_webxdc_status_updates(1)
assert status_updates == [ assert status_updates == [
{"payload": "Second update", "serial": 2, "max_serial": 2}, {"payload": "Second update", "serial": 2, "max_serial": 2},
] ]
def test_webxdc_insert_lots_of_updates(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
message = alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
for i in range(2000):
message.send_webxdc_status_update({"payload": str(i)}, "description")

View File

@@ -1,123 +0,0 @@
#!/usr/bin/env python3
"""
Testing webxdc iroh connectivity
If you want to debug iroh at rust-trace/log level set
RUST_LOG=iroh_net=trace,iroh_gossip=trace
"""
import pytest
import time
import os
import sys
import logging
import random
import itertools
import sys
from deltachat_rpc_client import DeltaChat, EventType, SpecialContactId
@pytest.fixture()
def path_to_webxdc():
return "../test-data/webxdc/chess.xdc"
def test_realtime_sequentially(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection sequentially."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.create_chat(ac2)
ac2.create_chat(ac1)
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping0")
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "ping0"
def log(msg):
print()
print("*" * 80 + "\n" + msg + "\n", file=sys.stderr)
print()
# share a webxdc app between ac1 and ac2
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
ac2_webxdc_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
snapshot = ac2_webxdc_msg.get_snapshot()
assert snapshot.text == "play"
# send iroh announcements sequentially
log("sending ac1 -> ac2 realtime advertisement and additional message")
ac1._rpc.send_webxdc_realtime_advertisement(ac1.id, ac1_webxdc_msg.id)
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
log("waiting for incoming message on ac2")
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "ping1"
log("sending ac2 -> ac1 realtime advertisement and additional message")
ac2._rpc.send_webxdc_realtime_advertisement(ac2.id, ac2_webxdc_msg.id)
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
log("waiting for incoming message on ac1")
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "ping2"
log("sending realtime data ac1 -> ac2")
ac1._rpc.send_webxdc_realtime_data(ac1.id, ac1_webxdc_msg.id, [13, 15, 17])
log("ac2: waiting for realtime data")
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
assert event.data == [13, 15, 17]
break
def test_realtime_simultaneously(acfactory, path_to_webxdc):
"""Test two peers trying to establish connection simultaneously."""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.create_chat(ac2)
ac2.create_chat(ac1)
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping0")
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "ping0"
def log(msg):
print()
print("*" * 80 + "\n" + msg + "\n", file=sys.stderr)
print()
# share a webxdc app between ac1 and ac2
ac1_webxdc_msg = acfactory.send_message(from_account=ac1, to_account=ac2, text="play", file=path_to_webxdc)
ac2_webxdc_msg = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id)
snapshot = ac2_webxdc_msg.get_snapshot()
assert snapshot.text == "play"
# send iroh announcements simultaneously
log("sending ac1 -> ac2 realtime advertisement and additional message")
ac1._rpc.send_webxdc_realtime_advertisement(ac1.id, ac1_webxdc_msg.id)
acfactory.send_message(from_account=ac1, to_account=ac2, text="ping1")
log("sending ac2 -> ac1 realtime advertisement and additional message")
ac2._rpc.send_webxdc_realtime_advertisement(ac2.id, ac2_webxdc_msg.id)
acfactory.send_message(from_account=ac2, to_account=ac1, text="ping2")
# Ensure that advertisements have been received.
log("waiting for incoming message on ac2")
snapshot = ac2.get_message_by_id(ac2.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "ping1"
log("waiting for incoming message on ac1")
snapshot = ac1.get_message_by_id(ac1.wait_for_incoming_msg_event().msg_id).get_snapshot()
assert snapshot.text == "ping2"
log("sending realtime data ac1 -> ac2")
ac1._rpc.send_webxdc_realtime_data(ac1.id, ac1_webxdc_msg.id, [13, 15, 17])
log("ac2: waiting for realtime data")
while 1:
event = ac2.wait_for_event()
if event.kind == EventType.WEBXDC_REALTIME_DATA:
assert event.data == [13, 15, 17]
break

View File

@@ -6,27 +6,24 @@ envlist =
[testenv] [testenv]
commands = commands =
pytest -n6 {posargs} pytest --exitfirst {posargs}
setenv = setenv =
# Avoid stack overflow when Rust core is built without optimizations. # Avoid stack overflow when Rust core is built without optimizations.
RUST_MIN_STACK=8388608 RUST_MIN_STACK=8388608
passenv = passenv =
CHATMAIL_DOMAIN DCC_NEW_TMP_EMAIL
deps = deps =
pytest pytest
pytest-timeout pytest-asyncio
pytest-xdist aiohttp
aiodns
[testenv:lint] [testenv:lint]
skipsdist = True skipsdist = True
skip_install = True skip_install = True
deps = deps =
ruff ruff
black
commands = commands =
ruff format --quiet --diff src/ examples/ tests/ black --quiet --check --diff src/ examples/ tests/
ruff check src/ examples/ tests/ ruff src/ examples/ tests/
[pytest]
timeout = 300
log_cli = true
log_level = debug

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "deltachat-rpc-server" name = "deltachat-rpc-server"
version = "1.138.5" version = "1.111.0"
description = "DeltaChat JSON-RPC server" description = "DeltaChat JSON-RPC server"
edition = "2021" edition = "2021"
readme = "README.md" readme = "README.md"
@@ -9,20 +9,20 @@ license = "MPL-2.0"
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"] keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
categories = ["cryptography", "std", "email"] categories = ["cryptography", "std", "email"]
[[bin]]
name = "deltachat-rpc-server"
[dependencies] [dependencies]
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false } deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
deltachat = { path = "..", default-features = false }
anyhow = "1" anyhow = "1"
env_logger = { version = "0.11.3" } env_logger = { version = "0.10.0" }
futures-lite = "2.3.0" futures-lite = "1.12.0"
log = "0.4" log = "0.4"
serde_json = "1" serde_json = "1.0.91"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.37.0", features = ["io-std"] } tokio = { version = "1.25.0", features = ["io-std"] }
tokio-util = "0.7.9" yerpc = { version = "0.4.0", features = ["anyhow_expose"] }
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
[features] [features]
default = ["vendored"] default = ["vendored"]

View File

@@ -30,8 +30,5 @@ deltachat-rpc-server
The common use case for this program is to create bindings to use Delta Chat core from programming The common use case for this program is to create bindings to use Delta Chat core from programming
languages other than Rust, for example: languages other than Rust, for example:
1. Python: https://pypi.org/project/deltachat-rpc-client/ 1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/ 2. Go: https://github.com/deltachat/deltachat-rpc-client-go/
Run `deltachat-rpc-server --version` to check the version of the server.
Run `deltachat-rpc-server --openrpc` to get [OpenRPC](https://open-rpc.org/) specification of the provided JSON-RPC API.

View File

@@ -1,3 +0,0 @@
platform_package
*.tgz
package-lock.json

View File

@@ -1,3 +0,0 @@
platform_package/*
scripts/
*.tgz

View File

@@ -1,77 +0,0 @@
## npm package for deltachat-rpc-server
This is the successor of `deltachat-node`,
it does not use NAPI bindings but instead uses stdio executables
to let you talk to core over jsonrpc over stdio.
This simplifies cross-compilation and even reduces binary size (no CFFI layer and no NAPI layer).
## Usage
> The **minimum** nodejs version for this package is `20.11`
```
npm i @deltachat/stdio-rpc-server @deltachat/jsonrpc-client
```
```js
import { startDeltaChat } from "@deltachat/stdio-rpc-server";
import { C } from "@deltachat/jsonrpc-client";
async function main() {
const dc = await startDeltaChat("deltachat-data");
console.log(await dc.rpc.getSystemInfo());
}
```
For a more complete example refer to https://github.com/deltachat-bot/echo/pull/69/files (TODO change link when pr is merged).
## How to use on an unsupported platform
<!-- todo instructions, will uses an env var for pointing to `deltachat-rpc-server` binary -->
<!-- todo copy parts from https://github.com/deltachat/deltachat-desktop/blob/7045c6f549e4b9d5caa0709d5bd314bbd9fd53db/docs/UPDATE_CORE.md -->
## How does it work when you install it
NPM automatically installs platform dependent optional dependencies when `os` and `cpu` fields are set correctly.
references:
- https://napi.rs/docs/deep-dive/release#3-the-native-addon-for-different-platforms-is-distributed-through-different-npm-packages, [webarchive version](https://web.archive.org/web/20240309234250/https://napi.rs/docs/deep-dive/release#3-the-native-addon-for-different-platforms-is-distributed-through-different-npm-packages)
- https://docs.npmjs.com/cli/v6/configuring-npm/package-json#cpu
- https://docs.npmjs.com/cli/v6/configuring-npm/package-json#os
When you import this package it searches for the rpc server in the following locations and order:
1. `DELTA_CHAT_RPC_SERVER` environment variable
2. in PATH
- unless `DELTA_CHAT_SKIP_PATH=1` is specified
- searches in .cargo/bin directory first
- but there an additional version check is performed
3. prebuilds in npm packages
## How do you built this package in CI
- To build platform packages, run the `build_platform_package.py` script:
```
python3 build_platform_package.py <cargo-target>
# example
python3 build_platform_package.py x86_64-apple-darwin
```
- Then pass it as an artifact to the last CI action that publishes the main package.
- upload all packages from `deltachat-rpc-server/npm-package/platform_package`.
- then publish `deltachat-rpc-server/npm-package`,
- this will run `update_optional_dependencies_and_version.js` (in the `prepack` script),
which puts all platform packages into `optionalDependencies` and updates the `version` in `package.json`
## How to build a version you can use localy on your host machine for development
You can not install the npm packet from the previous section locally, unless you have a local npm registry set up where you upload it too. This is why we have seperate scripts for making it work for local installation.
- If you just need your host platform run `python scripts/make_local_dev_version.py`
- note: this clears the `platform_package` folder
- (advanced) If you need more than one platform for local install you can just run `node scripts/update_optional_dependencies_and_version.js` after building multiple plaftorms with `build_platform_package.py`
## Thanks to nlnet
The initial work on this package was funded by nlnet as part of the [Delta Tauri](https://nlnet.nl/project/DeltaTauri/) Project.

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