Compare commits

..

28 Commits

Author SHA1 Message Date
Sebastian Klähn
bf52a77f98 typos 2023-01-30 18:47:07 +01:00
Sebastian Klähn
d850813ef0 Revert "fix test by expecting CHAT_MODIFIED another time"
This reverts commit 57ba2387e8.
2023-01-30 17:53:56 +01:00
Sebastian Klähn
7f4cebc08b fmt 2023-01-30 17:53:13 +01:00
Sebastian Klähn
d422ab8105 add tests 2023-01-30 17:50:38 +01:00
Sebastian Klähn
6529cfff03 properly handle mua users 2023-01-30 13:06:32 +01:00
Sebastian Klähn
061fd1dd2d move function to better place 2023-01-30 12:30:51 +01:00
septias
41f45bd680 fmt 2023-01-25 18:02:08 +01:00
septias
18dce04608 improvements 2023-01-25 17:55:47 +01:00
Sebastian Klähn
fe11198dbc Merge branch 'master' into fix3782 2023-01-23 11:10:41 +01:00
holger krekel
57ba2387e8 fix test by expecting CHAT_MODIFIED another time 2023-01-16 15:27:23 +01:00
Sebastian Klähn
8e0626e995 Merge branch 'master' into fix3782 2023-01-05 18:55:47 +01:00
Sebastian Klähn
7273c917a6 remove logs 2022-12-27 18:08:20 +01:00
Sebastian Klähn
d0871d3bd7 fmt + changelog 2022-12-27 17:41:21 +01:00
Sebastian Klähn
c8fe830c33 Revert "Fix test_synchronize_member_list_on_group_rejoin"
This reverts commit 0215046c76.
2022-12-27 17:24:24 +01:00
Sebastian Klähn
12dbf41116 Merge branch 'fix3782' of https://github.com/deltachat/deltachat-core-rust into fix3782 2022-12-27 17:23:15 +01:00
Sebastian Klähn
f7bf31dbea recteate memberlist on groupjoin 2022-12-27 17:23:04 +01:00
link2xt
0215046c76 Fix test_synchronize_member_list_on_group_rejoin 2022-12-27 16:07:29 +00:00
Sebastian Klähn
b5fc9aedc5 Update src/mimeparser.rs
Co-authored-by: Hocuri <hocuri@gmx.de>
2022-12-27 14:58:06 +01:00
Sebastian Klähn
0250b6f421 Update src/chat.rs
Co-authored-by: Hocuri <hocuri@gmx.de>
2022-12-27 14:58:06 +01:00
Septias
7415eadb78 fix suggested changes 2022-12-27 14:58:06 +01:00
Sebastian Klähn
8d077b964c Update src/chat.rs
Co-authored-by: Hocuri <hocuri@gmx.de>
2022-12-27 14:57:25 +01:00
Sebastian Klähn
99923fd816 Update src/chat.rs
Co-authored-by: Hocuri <hocuri@gmx.de>
2022-12-27 14:57:25 +01:00
Sebastian Klähn
f3614536d9 debug 2022-12-27 14:57:25 +01:00
Sebastian Klähn
a9a485d19e recreate test 2022-12-27 14:57:25 +01:00
Sebastian Klähn
32d54c8b93 clippy fix 2022-12-27 14:57:09 +01:00
Sebastian Klähn
f9a5cf9b11 use correct method & handle message deletions correctly 2022-12-27 14:56:24 +01:00
Sebastian Klähn
71ada54703 documentation 2022-12-27 14:56:24 +01:00
Sebastian Klähn
81695d6b80 don't always build new contact list 2022-12-27 14:56:24 +01:00
191 changed files with 5365 additions and 10571 deletions

28
.github/mergeable.yml vendored
View File

@@ -5,22 +5,22 @@ mergeable:
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"]
- 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"
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,55 +1,48 @@
name: Rust CI
# Cancel previously started workflow runs
# when the branch is updated.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
pull_request:
push:
branches:
- master
- staging
- trying
env:
RUSTFLAGS: -Dwarnings
jobs:
lint:
name: Rustfmt and Clippy
fmt:
name: Rustfmt
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.68.0
steps:
- uses: actions/checkout@v3
- name: Install rustfmt and clippy
run: rustup toolchain install $RUSTUP_TOOLCHAIN --profile minimal --component rustfmt --component clippy
- uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
override: true
- run: rustup component add rustfmt
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- name: Run rustfmt
run: cargo fmt --all -- --check
- name: Run clippy
run: scripts/clippy.sh
- run: cargo fmt --all -- --check
cargo_deny:
name: cargo deny
run_clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: EmbarkStudios/cargo-deny-action@v1
- uses: actions-rs/toolchain@v1
with:
arguments: --all-features --workspace
command: check
command-arguments: "-Dwarnings"
provider_database:
name: Check provider database
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Check provider database
run: scripts/update-provider-database.sh
toolchain: stable
components: clippy
override: true
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --workspace --tests --examples --benches --features repl -- -D warnings
docs:
name: Rust doc comments
@@ -59,6 +52,13 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v3
- name: Install rust stable toolchain
uses: actions-rs/toolchain@v1
with:
toolchain: stable
profile: minimal
components: rust-docs
override: true
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- name: Rustdoc
@@ -72,89 +72,91 @@ jobs:
include:
# Currently used Rust version.
- os: ubuntu-latest
rust: 1.68.0
rust: 1.64.0
python: 3.9
- os: windows-latest
rust: 1.68.0
rust: 1.64.0
python: false # Python bindings compilation on Windows is not supported.
# Minimum Supported Rust Version = 1.64.0
# Minimum Supported Rust Version = 1.63.0
#
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
# built.
- os: ubuntu-latest
rust: 1.64.0
rust: 1.63.0
python: 3.7
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@master
- uses: actions/checkout@master
- name: Install Rust ${{ matrix.rust }}
run: rustup toolchain install --profile minimal ${{ matrix.rust }}
- run: rustup override set ${{ matrix.rust }}
- name: Install ${{ matrix.rust }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.rust }}
override: true
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- name: Cache rust cargo artifacts
uses: swatinem/rust-cache@v2
- name: Check
run: cargo check --workspace --bins --examples --tests --benches
- name: check
run: cargo check --all --bins --examples --tests --features repl --benches
- name: Tests
run: cargo test --workspace
- name: tests
run: cargo test --all
- name: Test cargo vendor
run: cargo vendor
- name: test cargo vendor
run: cargo vendor
- name: Install python
if: ${{ matrix.python }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
- name: install python
if: ${{ matrix.python }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python }}
- name: Install tox
if: ${{ matrix.python }}
run: pip install tox
- name: install tox
if: ${{ matrix.python }}
run: pip install tox
- name: Build C library
if: ${{ matrix.python }}
run: cargo build -p deltachat_ffi --features jsonrpc
- name: build C library
if: ${{ matrix.python }}
run: cargo build -p deltachat_ffi --features jsonrpc
- name: Run python tests
if: ${{ matrix.python }}
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
run: tox -e lint,mypy,doc,py3
- name: run python tests
if: ${{ matrix.python }}
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
DCC_RS_TARGET: debug
DCC_RS_DEV: ${{ github.workspace }}
working-directory: python
run: tox -e lint,mypy,doc,py3
- name: Build deltachat-rpc-server
if: ${{ matrix.python }}
run: cargo build -p deltachat-rpc-server
- name: build deltachat-rpc-server
if: ${{ matrix.python }}
run: cargo build -p deltachat-rpc-server
- name: Add deltachat-rpc-server to path
if: ${{ matrix.python }}
run: echo ${{ github.workspace }}/target/debug >> $GITHUB_PATH
- name: add deltachat-rpc-server to path
if: ${{ matrix.python }}
run: echo ${{ github.workspace }}/target/debug >> $GITHUB_PATH
- name: Run deltachat-rpc-client tests
if: ${{ matrix.python }}
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
working-directory: deltachat-rpc-client
run: tox -e py3,lint
- name: run deltachat-rpc-client tests
if: ${{ matrix.python }}
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
working-directory: deltachat-rpc-client
run: tox -e py3,lint
- name: Install pypy
if: ${{ matrix.python }}
uses: actions/setup-python@v4
with:
python-version: "pypy${{ matrix.python }}"
- 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
- 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,126 +0,0 @@
# Manually triggered action to build deltachat-rpc-server binaries.
name: Build deltachat-rpc-server binaries
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
on:
workflow_dispatch:
release:
types: [published]
jobs:
# Build a version statically linked against musl libc
# to avoid problems with glibc version incompatibility.
build_linux:
name: Cross-compile deltachat-rpc-server for x86_64, aarch64 and armv7 Linux
runs-on: ubuntu-22.04
steps:
- uses: actions/checkout@v3
- name: Build
run: sh scripts/zig-rpc-server.sh
- name: Upload x86_64 binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-x86_64
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
build_windows:
name: Build deltachat-rpc-server for Windows
strategy:
fail-fast: false
matrix:
include:
- 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:
- uses: actions/checkout@v3
- name: Setup rust target
run: rustup target add ${{ matrix.target }}
- name: Build
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.target }} --features vendored
- name: Upload binary
uses: actions/upload-artifact@v3
with:
name: deltachat-rpc-server-${{ matrix.artifact }}
path: target/${{ matrix.target}}/release/${{ matrix.path }}
if-no-files-found: error
publish:
name: Upload binaries to the release
needs: ["build_linux", "build_windows"]
permissions:
contents: write
runs-on: "ubuntu-latest"
steps:
- name: Download deltachat-rpc-server-x86_64
uses: "actions/download-artifact@v3"
with:
name: "deltachat-rpc-server-x86_64"
path: "dist/deltachat-rpc-server-x86_64"
- name: Download deltachat-rpc-server-aarch64
uses: "actions/download-artifact@v3"
with:
name: "deltachat-rpc-server-aarch64"
path: "dist/deltachat-rpc-server-aarch64"
- name: Download deltachat-rpc-server-armv7
uses: "actions/download-artifact@v3"
with:
name: "deltachat-rpc-server-armv7"
path: "dist/deltachat-rpc-server-armv7"
- name: Download deltachat-rpc-server-win32.exe
uses: "actions/download-artifact@v3"
with:
name: "deltachat-rpc-server-win32.exe"
path: "dist/deltachat-rpc-server-win32.exe"
- name: Download deltachat-rpc-server-win64.exe
uses: "actions/download-artifact@v3"
with:
name: "deltachat-rpc-server-win64.exe"
path: "dist/deltachat-rpc-server-win64.exe"
- name: List downloaded artifacts
run: ls -l dist/
- name: Upload binaries to the GitHub release
env:
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
run: |
gh release upload ${{ github.ref_name }} \
--repo ${{ github.repository }} \
dist/deltachat-rpc-server-*

View File

@@ -1,28 +1,29 @@
name: "jsonrpc js client build"
name: 'jsonrpc js client build'
on:
pull_request:
push:
tags:
- "*"
- "!py-*"
- '*'
- '!py-*'
jobs:
pack-module:
name: "Package @deltachat/jsonrpc-client and upload to download.delta.chat"
runs-on: ubuntu-20.04
name: 'Package @deltachat/jsonrpc-client and upload to download.delta.chat'
runs-on: ubuntu-18.04
steps:
- name: Install tree
- name: install tree
run: sudo apt install tree
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16"
- name: Get tag
node-version: '16'
- name: get tag
id: tag
uses: dawidd6/action-get-tag@v1
continue-on-error: true
- name: Get Pull Request ID
- name: Get Pullrequest ID
id: prepare
run: |
tag=${{ steps.tag.outputs.tag }}
@@ -37,11 +38,11 @@ jobs:
npm --version
node --version
echo $DELTACHAT_JSONRPC_TAR_GZ
- name: Install dependencies without running scripts
- name: install dependencies without running scripts
run: |
cd deltachat-jsonrpc/typescript
npm install --ignore-scripts
- name: Package
- name: package
shell: bash
run: |
cd deltachat-jsonrpc/typescript
@@ -64,13 +65,13 @@ jobs:
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
- name: "Post links to details"
if: steps.upload-preview.outcome == 'success'
run: node ./node/scripts/postLinksToDetails.js
env:
URL: preview/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MSG_CONTEXT: Download the deltachat-jsonrpc-client.tgz
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 }}

View File

@@ -35,10 +35,6 @@ jobs:
npm run test
env:
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
- name: make sure websocket server version still builds
run: |
cd deltachat-jsonrpc
cargo build --bin deltachat-jsonrpc-server --features webserver
- name: Run linter
run: |
cd deltachat-jsonrpc/typescript

View File

@@ -7,25 +7,26 @@ on:
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/"
- name: Get Pullrequest 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

@@ -9,26 +9,26 @@ jobs:
generate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v3
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
- name: Use Node.js 16.x
uses: actions/setup-node@v3
with:
node-version: 16.x
- name: npm install and generate documentation
run: |
cd node
npm i --ignore-scripts
npx typedoc
mv docs js
- name: npm install and generate documentation
run: |
cd node
npm i --ignore-scripts
npx typedoc
mv docs js
- name: Upload
uses: horochx/deploy-via-scp@v1.0.1
with:
user: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}
host: "delta.chat"
port: 22
local: "node/js"
remote: "/var/www/html/"
- name: Upload
uses: horochx/deploy-via-scp@v1.0.1
with:
user: ${{ secrets.USERNAME }}
key: ${{ secrets.KEY }}
host: "delta.chat"
port: 22
local: "node/js"
remote: "/var/www/html/"

View File

@@ -1,24 +1,25 @@
name: "node.js build"
name: 'node.js build'
on:
pull_request:
push:
tags:
- "*"
- "!py-*"
- '*'
- '!py-*'
jobs:
prebuild:
name: Prebuild
name: 'prebuild'
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-20.04, macos-latest, windows-latest]
os: [ubuntu-18.04, macos-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16"
node-version: '16'
- name: System info
run: |
rustc -vV
@@ -64,21 +65,21 @@ jobs:
pack-module:
needs: prebuild
name: Package deltachat-node and upload to download.delta.chat
runs-on: ubuntu-20.04
name: 'Package deltachat-node and upload to download.delta.chat'
runs-on: ubuntu-18.04
steps:
- name: Install tree
- name: install tree
run: sudo apt install tree
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v2
with:
node-version: "16"
- name: Get tag
node-version: '16'
- name: get tag
id: tag
uses: dawidd6/action-get-tag@v1
continue-on-error: true
- name: Get Pull Request ID
- name: Get Pullrequest ID
id: prepare
run: |
tag=${{ steps.tag.outputs.tag }}
@@ -96,43 +97,43 @@ jobs:
npm --version
node --version
echo $DELTACHAT_NODE_TAR_GZ
- name: Download Ubuntu prebuild
- name: Download ubuntu prebuild
uses: actions/download-artifact@v1
with:
name: ubuntu-20.04
- name: Download macOS prebuild
name: ubuntu-18.04
- name: Download macos prebuild
uses: actions/download-artifact@v1
with:
name: macos-latest
- name: Download Windows prebuild
- name: Download windows prebuild
uses: actions/download-artifact@v1
with:
name: windows-latest
- shell: bash
run: |
mkdir node/prebuilds
tar -xvzf ubuntu-20.04/ubuntu-20.04.tar.gz -C node/prebuilds
tar -xvzf ubuntu-18.04/ubuntu-18.04.tar.gz -C node/prebuilds
tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
tree node/prebuilds
rm -rf ubuntu-20.04 macos-latest windows-latest
- name: Install dependencies without running scripts
rm -rf ubuntu-18.04 macos-latest windows-latest
- name: install dependencies without running scripts
run: |
npm install --ignore-scripts
- name: Build constants
- name: build constants
run: |
npm run build:core:constants
- name: Build TypeScript part
- name: build typescript part
run: |
npm run build:bindings:ts
- name: Package
- name: package
shell: bash
run: |
mv node/README.md README.md
npm pack .
ls -lah
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
- name: Upload prebuild
- name: Upload Prebuild
uses: actions/upload-artifact@v3
with:
name: deltachat-node.tgz
@@ -147,12 +148,12 @@ jobs:
chmod 600 __TEMP_INPUT_KEY_FILE
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r $DELTACHAT_NODE_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
continue-on-error: true
- name: Post links to details
- name: "Post links to details"
if: steps.upload-preview.outcome == 'success'
run: node ./node/scripts/postLinksToDetails.js
env:
URL: preview/${{ env.DELTACHAT_NODE_TAR_GZ }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
URL: preview/${{ env.DELTACHAT_NODE_TAR_GZ }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Upload to download.delta.chat/node/
- name: Upload deltachat-node build to download.delta.chat/node/
if: ${{ steps.tag.outputs.tag }}

View File

@@ -1,30 +1,25 @@
name: "node.js tests"
# Cancel previously started workflow runs
# when the branch is updated.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
name: 'node.js tests'
on:
pull_request:
push:
branches:
- master
- staging
- trying
jobs:
tests:
name: Tests
name: 'tests'
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-18.04, macos-latest, windows-latest]
steps:
- name: Checkout
uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: "16"
node-version: '16'
- name: System info
run: |
rustc -vV
@@ -64,7 +59,7 @@ jobs:
npm run test
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'
- name: Run tests on Windows, except lint
timeout-minutes: 10
if: runner.os == 'Windows'
@@ -73,4 +68,4 @@ jobs:
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

@@ -11,13 +11,19 @@ jobs:
name: Build REPL example
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v3
- name: Build
run: cargo build -p deltachat-repl --features vendored
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: 1.66.0
override: true
- name: Upload binary
uses: actions/upload-artifact@v3
with:
name: repl.exe
path: "target/debug/deltachat-repl.exe"
- name: build
run: cargo build --example repl --features repl,vendored
- name: Upload binary
uses: actions/upload-artifact@v3
with:
name: repl.exe
path: 'target/debug/examples/repl.exe'

View File

@@ -8,18 +8,20 @@ on:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat --no-deps --document-private-items
- name: Upload to rs.delta.chat
uses: up9cloud/action-rsync@v1.3
env:
USER: ${{ secrets.USERNAME }}
KEY: ${{ secrets.KEY }}
HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/rs/"
- uses: actions/checkout@v3
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat --no-deps
- name: Upload to rs.delta.chat
uses: up9cloud/action-rsync@v1.3
env:
USER: ${{ secrets.USERNAME }}
KEY: ${{ secrets.KEY }}
HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/rs/"

View File

@@ -8,18 +8,20 @@ on:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat_ffi --no-deps
- name: Upload to cffi.delta.chat
uses: up9cloud/action-rsync@v1.3
env:
USER: ${{ secrets.USERNAME }}
KEY: ${{ secrets.KEY }}
HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/cffi/"
- uses: actions/checkout@v3
- name: Build the documentation with cargo
run: |
cargo doc --package deltachat_ffi --no-deps
- name: Upload to cffi.delta.chat
uses: up9cloud/action-rsync@v1.3
env:
USER: ${{ secrets.USERNAME }}
KEY: ${{ secrets.KEY }}
HOST: "delta.chat"
SOURCE: "target/doc"
TARGET: "/var/www/html/cffi/"

View File

@@ -1,133 +1,14 @@
# Changelog
## [Unreleased]
### Changes
- "full message view" not needed because of footers that go to contact status #4151
- Pick up system's light/dark mode in generated message HTML #4150
- Support non-persistent configuration with DELTACHAT_* env
- Print deltachat-repl errors with causes. #4166
- Increase MSRV to 1.64. #4167
- Core takes care of stopping and re-starting IO itself where needed,
e.g. during backup creation. It is no longer needed to call
dc_stop_io(). dc_start_io() can now be called at any time without
harm. #4138
- More accurate maybe_add_bcc_self device message text #4175
### Fixes
- Fix segmentation fault if `dc_context_unref()` is called during
background process spawned by `dc_configure()` or `dc_imex()`
or `dc_jsonrpc_instance_t` is unreferenced
during handling the JSON-RPC request. #4153
- Delete expired messages using multiple SQL requests. #4158
- Do not emit "Failed to run incremental vacuum" warnings on success. #4160
- Ability to send backup over network and QR code to setup second device #4007
- Disable buffering during STARTTLS setup. #4190
## [1.111.0] - 2023-03-05
### Changes
- Make smeared timestamp generation non-async. #4075
- Set minimum TLS version to 1.2. #4096
- Run `cargo-deny` in CI. #4101
- Check provider database with CI. #4099
- Switch to DEFERRED transactions #4100
### Fixes
- Do not block async task executor while decrypting the messages. #4079
- Housekeeping: delete the blobs backup dir #4123
### API-Changes
- jsonrpc: add more advanced API to send a message. #4097
- jsonrpc: add get webxdc blob API `getWebxdcBlob` #4070
## 1.110.0
### Changes
- use transaction in `Contact::add_or_lookup()` #4059
- Organize the connection pool as a stack rather than a queue to ensure that
connection page cache is reused more often.
This speeds up tests by 28%, real usage will have lower speedup. #4065
- Use transaction in `update_blocked_mailinglist_contacts`. #4058
- Remove `Sql.get_conn()` interface in favor of `.call()` and `.transaction()`. #4055
- Updated provider database.
- Disable DKIM-Checks again #4076
- Switch from "X.Y.Z" and "py-X.Y.Z" to "vX.Y.Z" tags. #4089
- mimeparser: handle headers from the signed part of unencrypted signed message #4013
### Fixes
- Start SQL transactions with IMMEDIATE behaviour rather than default DEFERRED one. #4063
- Fix a problem with Gmail where (auto-)deleted messages would get archived instead of deleted.
Move them to the Trash folder for Gmail which auto-deletes trashed messages in 30 days #3972
- Clear config cache after backup import. This bug sometimes resulted in the import to seemingly work at first. #4067
- Update timestamps in `param` columns with transactions. #4083
### API-Changes
## 1.109.0
### Changes
- deltachat-rpc-client: use `dataclass` for `Account`, `Chat`, `Contact` and `Message` #4042
### Fixes
- deltachat-rpc-server: do not block stdin while processing the request. #4041
deltachat-rpc-server now reads the next request as soon as previous request handler is spawned.
- Enable `auto_vacuum` on all SQL connections. #2955
- Replace `r2d2` connection pool with an own implementation. #4050 #4053 #4043 #4061
This change improves reliability
by closing all database connections immediately when the context is closed.
### API-Changes
- Remove `MimeMessage::from_bytes()` public interface. #4033
- BREAKING Types: jsonrpc: `get_messages` now returns a map with `MessageLoadResult` instead of failing completely if one of the requested messages could not be loaded. #4038
- Add `dc_msg_set_subject()`. C-FFI #4057
- Mark python bindings as supporting typing according to PEP 561 #4045
## 1.108.0
### Changes
- Use read/write timeouts instead of per-command timeouts for SMTP #3985
- Cache DNS results for SMTP connections #3985
- Prefer TLS over STARTTLS during autoconfiguration #4021
- Use SOCKS5 configuration for HTTP requests #4017
- Show non-deltachat emails by default for new installations #4019
- Re-enabled SMTP pipelining after disabling it in #4006
### Fixes
- Fix Securejoin for multiple devices on a joining side #3982
- python: handle NULL value returned from `dc_get_msg()` #4020
Account.`get_message_by_id` may return `None` in this case.
### API-Changes
- Remove bitflags from `get_chat_msgs()` interface #4022
C interface is not changed.
Rust and JSON-RPC API have `flags` integer argument
replaced with two boolean flags `info_only` and `add_daymarker`.
- jsonrpc: add API to check if the message is sent by a bot #3877
## 1.107.1
### Changes
- Log server security (TLS/STARTTLS/plain) type #4005
### Fixes
- Disable SMTP pipelining #4006
## 1.107.0
## Unreleased
### Changes
- Pipeline SMTP commands #3924
- Cache DNS results for IMAP connections #3970
- Cache DNS results #3970
### Fixes
- Securejoin: Fix adding and handling Autocrypt-Gossip headers #3914
- fix verifier-by addr was empty string instead of None #3961
- fix verifier-by addr was empty string intead of None #3961
- Emit DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK when the number of archived chats with
unread messages increases #3959
- Fix Peerstate comparison #3962
@@ -139,8 +20,6 @@
- jsonrpc: add verified-by information to `Contact`-Object
- Remove `attach_selfavatar` config #3951
### Changes
- add debug logging support for webxdcs #3296
## 1.106.0
@@ -178,13 +57,13 @@
### Fixes
- Do not add an error if the message is encrypted but not signed #3860
- Do not strip leading spaces from message lines #3867
- Don't always rebuild group member lists #3872
- Fix uncaught exception in JSON-RPC tests #3884
- Fix STARTTLS connection and add a test for it #3907
- Trigger reconnection when failing to fetch existing messages #3911
- Do not retry fetching existing messages after failure, prevents infinite reconnection loop #3913
- Ensure format=flowed formatting is always reversible on the receiver side #3880
## 1.104.0
### Changes
@@ -228,7 +107,7 @@
- jsonrpc: Add async Python client #3734
### Fixes
- Make sure malformed messages will never block receiving further messages anymore #3771
- Make sure malformed messsages will never block receiving further messages anymore #3771
- strip leading/trailing whitespace from "Chat-Group-Name{,-Changed}:" headers content #3650
- Assume all Thunderbird users prefer encryption #3774
- refactor peerstate handling to ensure no duplicate peerstates #3776
@@ -380,7 +259,7 @@
- `importBackup()`
- `getMessageHtml()` #3671
- `miscGetStickerFolder` and `miscGetStickers` #3672
- breaking: jsonrpc: remove function `messageListGetMessageIds()`, it is replaced by `getMessageIds()` and `getMessageListItems()` the latter returns a new `MessageListItem` type, which is the now preferred way of using the message list.
- breaking: jsonrpc: remove function `messageListGetMessageIds()`, it is replaced by `getMessageIds()` and `getMessageListItems()` the latter returns a new `MessageListItem` type, which is the now prefered way of using the message list.
- jsonrpc: add type: #3641, #3645
- `MessageSearchResult`
- `Location`
@@ -406,7 +285,7 @@
- jsonrpc js client:
- Change package name from `deltachat-jsonrpc-client` to `@deltachat/jsonrpc-client`
- remove relative file dependency to it from `deltachat-node` (because it did not work anyway and broke the nix build of desktop)
- ci: add github ci action to upload it to our download server automatically on release
- ci: add github ci action to upload it to our download server automaticaly on realease
## 1.95.0
@@ -483,7 +362,7 @@
- Auto accept contact requests if `Config::Bot` is set for a client #3567
- Don't prepend the subject to chat messages in mailinglists
- fix `set_core_version.py` script to also update version in `deltachat-jsonrpc/typescript/package.json` #3585
- Reject webxdc-updates from contacts who are not group members #3568
- Reject webxcd-updates from contacts who are not group members #3568
## 1.93.0
@@ -706,7 +585,7 @@
### Fixes
- node: throw error when getting context with an invalid account id
- node: throw error when instantiating a wrapper class on `null` (Context, Message, Chat, ChatList and so on)
- node: throw error when instanciating a wrapper class on `null` (Context, Message, Chat, ChatList and so on)
- use same contact-color if email address differ only in upper-/lowercase #3327
- repair encrypted mails "mixed up" by Google Workspace "Append footer" function #3315
@@ -794,7 +673,7 @@
- hopefully fix a bug where outgoing messages appear twice with Amazon SES #3077
- do not delete messages without Message-IDs as duplicates #3095
- assign replies from a different email address to the correct chat #3119
- assign outgoing private replies to the correct chat #3177
- assing outgoing private replies to the correct chat #3177
- start ephemeral timer when seen status is synchronized via IMAP #3122
- do not create empty contact requests with "setup changed" messages;
instead, send a "setup changed" message into all chats we share with the peer #3187
@@ -847,7 +726,7 @@
- don't watch Sent folder by default #3025
- use webxdc app name in chatlist/quotes/replies etc. #3027
- make it possible to cancel message sending by removing the message #3034,
this was previously removed in 1.71.0 #2939
this was previosuly removed in 1.71.0 #2939
- synchronize Seen flags only on watched folders to speed up
folder scanning #3041
- remove direct dependency on `byteorder` crate #3031
@@ -2069,7 +1948,7 @@
- #1043 avoid potential crashes in malformed From/Chat-Disposition... headers
- #1045 #1041 #1038 #1035 #1034 #1029 #1025 various cleanups and doc
improvements
improvments
## 1.0.0-beta.16
@@ -2148,7 +2027,7 @@
- trigger reconnect more often on imap error states. Should fix an
issue observed when trying to empty a folder. @hpk42
- un-split qr tests: we fixed qr-securejoin protocol flakiness
- un-split qr tests: we fixed qr-securejoin protocol flakyness
last weeks. @hpk42
## 1.0.0-beta.10
@@ -2186,7 +2065,7 @@
- fix moving self-sent messages, thanks @r10s, @flub, @hpk42
- fix flakiness/sometimes-failing verified/join-protocols,
- fix flakyness/sometimes-failing verified/join-protocols,
thanks @flub, @r10s, @hpk42
- fix reply-to-encrypted message to keep encryption
@@ -2205,7 +2084,7 @@
- fixes imap-protocol parsing bugs that lead to infinitely
repeated crashing while trying to receive messages with
a subject that contained non-utf8. thanks @link2xt
a subjec that contained non-utf8. thanks @link2xt
- fixed logic to find encryption subkey -- previously
delta chat would use the primary key for encryption
@@ -2313,6 +2192,3 @@
For a full list of changes, please see our closed Pull Requests:
https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[unreleased]: https://github.com/deltachat/deltachat-core-rust/compare/v1.111.0...HEAD
[1.111.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.110.0...v1.111.0

2620
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,9 @@
[package]
name = "deltachat"
version = "1.111.0"
version = "1.106.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.64"
rust-version = "1.63"
[profile.dev]
debug = 0
@@ -13,67 +13,54 @@ opt-level = 1
[profile.test]
opt-level = 0
# Always optimize dependencies.
# This does not apply to crates in the workspace.
# <https://doc.rust-lang.org/cargo/reference/profiles.html#overrides>
[profile.dev.package."*"]
opt-level = "z"
[profile.release]
lto = true
panic = 'abort'
opt-level = "z"
[patch.crates-io]
default-net = { git = "https://github.com/dignifiedquire/default-net.git", branch="feat-android" }
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
quinn-proto = { git = "https://github.com/quinn-rs/quinn", branch="main" }
[dependencies]
deltachat_derive = { path = "./deltachat_derive" }
format-flowed = { path = "./format-flowed" }
ratelimit = { path = "./deltachat-ratelimit" }
ansi_term = { version = "0.12.1", optional = true }
anyhow = "1"
async-channel = "1.8.0"
async-imap = { git = "https://github.com/async-email/async-imap", branch = "master", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.9", default-features = false, features = ["deflate"] }
async-native-tls = { version = "0.4", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.6", default-features = false, features = ["smtp-transport", "socks5", "runtime-tokio"] }
trust-dns-resolver = "0.22"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
backtrace = "0.3"
base64 = "0.21"
base64 = "0.20"
bitflags = "1.3"
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
dirs = { version = "4", optional=true }
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
escaper = "0.1"
fast-socks5 = "0.8"
futures = "0.3"
futures-lite = "1.12.0"
hex = "0.4.0"
humansize = "2"
image = { version = "0.24.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
# iroh = { version = "0.3.0", default-features = false }
iroh = { git = 'https://github.com/n0-computer/iroh', branch = "flub/ticket-multiple-addrs" }
kamadak-exif = "0.5"
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
libc = "0.2"
log = {version = "0.4.16", optional = true }
mailparse = "0.14"
native-tls = "0.2"
num_cpus = "1.15"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.17.0"
percent-encoding = "2.2"
parking_lot = "0.12"
pgp = { version = "0.9", default-features = false }
pretty_env_logger = { version = "0.4", optional = true }
qrcodegen = "1.7.0"
quick-xml = "0.27"
r2d2 = "0.8"
r2d2_sqlite = "0.20"
rand = "0.8"
regex = "1.7"
reqwest = { version = "0.11.14", features = ["json"] }
rusqlite = { version = "0.28", features = ["sqlcipher"] }
rusqlite = { version = "0.27", features = ["sqlcipher"] }
rust-hsluv = "0.1"
rustyline = { version = "10", optional = true }
sanitize-filename = "0.4"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
@@ -82,17 +69,21 @@ sha2 = "0.10"
smallvec = "1"
strum = "0.24"
strum_macros = "0.24"
tagger = "4.3.4"
textwrap = "0.16.0"
thiserror = "1"
tokio-io-timeout = "1.2.0"
tokio-stream = { version = "0.1.11", features = ["fs"] }
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
toml = "0.7"
trust-dns-resolver = "0.22"
toml = "0.5"
url = "2"
uuid = { version = "1", features = ["serde", "v4"] }
fast-socks5 = "0.8"
humansize = "2"
qrcodegen = "1.7.0"
tagger = "4.3.4"
textwrap = "0.16.0"
async-channel = "1.8.0"
futures-lite = "1.12.0"
tokio-stream = { version = "0.1.11", features = ["fs"] }
tokio-io-timeout = "1.2.0"
reqwest = { version = "0.11.13", features = ["json"] }
async_zip = { version = "0.0.9", default-features = false, features = ["deflate"] }
[dev-dependencies]
ansi_term = "0.12.0"
@@ -102,7 +93,6 @@ log = "0.4"
pretty_env_logger = "0.4"
proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = "3"
testdir = "0.7.2"
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
[workspace]
@@ -111,14 +101,18 @@ members = [
"deltachat_derive",
"deltachat-jsonrpc",
"deltachat-rpc-server",
"deltachat-ratelimit",
"deltachat-repl",
"format-flowed",
]
[[example]]
name = "simple"
path = "examples/simple.rs"
required-features = ["repl"]
[[example]]
name = "repl"
path = "examples/repl/main.rs"
required-features = ["repl"]
[[bench]]
@@ -145,15 +139,14 @@ harness = false
name = "get_chatlist"
harness = false
[[bench]]
name = "send_events"
harness = false
[features]
default = ["vendored"]
internals = []
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
vendored = [
"async-native-tls/vendored",
"async-smtp/native-tls-vendored",
"rusqlite/bundled-sqlcipher-vendored-openssl",
"reqwest/native-tls-vendored"
]
nightly = ["pgp/nightly"]

View File

@@ -19,19 +19,10 @@ $ curl https://sh.rustup.rs -sSf | sh
Compile and run Delta Chat Core command line utility, using `cargo`:
```
$ RUST_LOG=deltachat_repl=info cargo run -p deltachat-repl -- ~/deltachat-db
$ RUST_LOG=repl=info cargo run --example repl --features repl -- ~/deltachat-db
```
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
Optionally, install `deltachat-repl` binary with
```
$ cargo install --path deltachat-repl/
```
and run as
```
$ deltachat-repl ~/deltachat-db
```
Configure your account (if not already configured):
```
@@ -113,7 +104,7 @@ $ cargo build -p deltachat_ffi --release
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
- `RUST_LOG=deltachat_repl=info,async_imap=trace,async_smtp=trace`: enable IMAP and
- `RUST_LOG=repl=info,async_imap=trace,async_smtp=trace`: enable IMAP and
SMTP tracing in addition to info messages.
### Expensive tests
@@ -170,9 +161,7 @@ Language bindings are available for:
- over cffi (legacy): \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
- over jsonrpc built with napi.rs: \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
- **Go**
- over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
- over cffi[^1]: \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
- **Go**[^1] \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
- **Free Pascal**[^1] \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
- **Java** and **Swift** (contained in the Android/iOS repos)

View File

@@ -14,7 +14,7 @@ async fn address_book_benchmark(n: u32, read_count: u32) {
.unwrap();
let book = (0..n)
.map(|i| format!("Name {i}\naddr{i}@example.org\n"))
.map(|i| format!("Name {}\naddr{}@example.org\n", i, i))
.collect::<Vec<String>>()
.join("");

View File

@@ -14,7 +14,7 @@ async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
.unwrap();
for c in chats.iter().take(10) {
black_box(chat::get_chat_msgs(&context, *c).await.ok());
black_box(chat::get_chat_msgs(&context, *c, 0).await.ok());
}
}

View File

@@ -1,47 +0,0 @@
use criterion::{criterion_group, criterion_main, Criterion};
use deltachat::context::Context;
use deltachat::stock_str::StockStrings;
use deltachat::{info, Event, EventType, Events};
use tempfile::tempdir;
async fn send_events_benchmark(context: &Context) {
let emitter = context.get_event_emitter();
for _i in 0..1_000_000 {
info!(context, "interesting event...");
}
info!(context, "DONE");
loop {
match emitter.recv().await.unwrap() {
Event {
typ: EventType::Info(info),
..
} if info.contains("DONE") => {
break;
}
_ => {}
}
}
}
fn criterion_benchmark(c: &mut Criterion) {
let dir = tempdir().unwrap();
let dbfile = dir.path().join("db.sqlite");
let rt = tokio::runtime::Runtime::new().unwrap();
let context = rt.block_on(async {
Context::new(&dbfile, 100, Events::new(), StockStrings::new())
.await
.expect("failed to create context")
});
let executor = tokio::runtime::Runtime::new().unwrap();
c.bench_function("Sending 1.000.000 events", |b| {
b.to_async(&executor)
.iter(|| send_events_benchmark(&context))
});
}
criterion_group!(benches, criterion_benchmark);
criterion_main!(benches);

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.111.0"
version = "1.106.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"
@@ -17,7 +17,7 @@ crate-type = ["cdylib", "staticlib"]
deltachat = { path = "../", default-features = false }
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
libc = "0.2"
human-panic = { version = "1", default-features = false }
human-panic = "1"
num-traits = "0.2"
serde_json = "1.0"
tokio = { version = "1", features = ["rt-multi-thread"] }
@@ -29,5 +29,6 @@ once_cell = "1.17.0"
[features]
default = ["vendored"]
vendored = ["deltachat/vendored"]
jsonrpc = ["dep:deltachat-jsonrpc"]
nightly = ["deltachat/nightly"]
jsonrpc = ["deltachat-jsonrpc"]

View File

@@ -1,24 +1,14 @@
:root {
--accent: hsl(0 0% 85%);
}
@media (prefers-color-scheme: dark) {
:root {
--accent: hsl(0 0% 25%);
}
}
/* the code snippet frame, defaults to white which tends to get badly readable in combination with explaining text around */
div.fragment {
background-color: var(--accent);
background-color: #e0e0e0;
border: 0;
padding: 1em;
border-radius: 6px;
}
code {
background-color: var(--accent);
background-color: #e0e0e0;
padding-left: .5em;
padding-right: .5em;
border-radius: 6px;

View File

@@ -24,7 +24,6 @@ typedef struct _dc_provider dc_provider_t;
typedef struct _dc_event dc_event_t;
typedef struct _dc_event_emitter dc_event_emitter_t;
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
typedef struct _dc_backup_provider dc_backup_provider_t;
// Alias for backwards compatibility, use dc_event_emitter_t instead.
typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
@@ -613,7 +612,7 @@ int dc_all_work_done (dc_context_t* context);
* While dc_configure() returns immediately,
* the started configuration-job may take a while.
*
* During configuration, #DC_EVENT_CONFIGURE_PROGRESS events are emitted;
* During configuration, #DC_EVENT_CONFIGURE_PROGRESS events are emmited;
* they indicate a successful configuration as well as errors
* and may be used to create a progress bar.
*
@@ -870,7 +869,7 @@ uint32_t dc_get_chat_id_by_contact_id (dc_context_t* context, uint32_t co
* @param context The context object as returned from dc_context_new().
* @param chat_id The chat ID to send the message to.
* @param msg The message object to send to the chat defined by the chat ID.
* On success, msg_id and state of the object are set up,
* On succcess, msg_id and state of the object are set up,
* The function does not take ownership of the object,
* so you have to free it using dc_msg_unref() as usual.
* @return The ID of the message that is being prepared.
@@ -881,7 +880,7 @@ uint32_t dc_prepare_msg (dc_context_t* context, uint32_t ch
/**
* Send a message defined by a dc_msg_t object to a chat.
*
* Sends the event #DC_EVENT_MSGS_CHANGED on success.
* Sends the event #DC_EVENT_MSGS_CHANGED on succcess.
* However, this does not imply, the message really reached the recipient -
* sending may be delayed e.g. due to network problems. However, from your
* view, you're done with the message. Sooner or later it will find its way.
@@ -910,7 +909,7 @@ uint32_t dc_prepare_msg (dc_context_t* context, uint32_t ch
* @param chat_id The chat ID to send the message to.
* If dc_prepare_msg() was called before, this parameter can be 0.
* @param msg The message object to send to the chat defined by the chat ID.
* On success, msg_id of the object is set up,
* On succcess, msg_id of the object is set up,
* The function does not take ownership of the object,
* so you have to free it using dc_msg_unref() as usual.
* @return The ID of the message that is about to be sent. 0 in case of errors.
@@ -927,7 +926,7 @@ uint32_t dc_send_msg (dc_context_t* context, uint32_t ch
* @param chat_id The chat ID to send the message to.
* If dc_prepare_msg() was called before, this parameter can be 0.
* @param msg The message object to send to the chat defined by the chat ID.
* On success, msg_id of the object is set up,
* On succcess, msg_id of the object is set up,
* The function does not take ownership of the object,
* so you have to free it using dc_msg_unref() as usual.
* @return The ID of the message that is about to be sent. 0 in case of errors.
@@ -938,7 +937,7 @@ uint32_t dc_send_msg_sync (dc_context_t* context, uint32
/**
* Send a simple text message a given chat.
*
* Sends the event #DC_EVENT_MSGS_CHANGED on success.
* Sends the event #DC_EVENT_MSGS_CHANGED on succcess.
* However, this does not imply, the message really reached the recipient -
* sending may be delayed e.g. due to network problems. However, from your
* view, you're done with the message. Sooner or later it will find its way.
@@ -1147,7 +1146,7 @@ void dc_set_draft (dc_context_t* context, uint32_t ch
* // not now and not when this code is executed again
* dc_add_device_msg(context, "update-123", NULL);
* } else {
* // welcome message was not added now, this is an older installation,
* // welcome message was not added now, this is an oder installation,
* // add a changelog
* dc_add_device_msg(context, "update-123", changelog_msg);
* }
@@ -1160,7 +1159,7 @@ uint32_t dc_add_device_msg (dc_context_t* context, const char*
/**
* Check if a device-message with a given label was ever added.
* Device-messages can be added with dc_add_device_msg().
* Device-messages can be added dc_add_device_msg().
*
* @memberof dc_context_t
* @param context The context object.
@@ -2101,7 +2100,8 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
/**
* Import/export things.
*
* During backup import/export IO must not be started,
* if needed stop IO using dc_accounts_stop_io() or dc_stop_io() first.
* What to do is defined by the _what_ parameter which may be one of the following:
*
* - **DC_IMEX_EXPORT_BACKUP** (11) - Export a backup to the directory given as `param1`
@@ -2295,7 +2295,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
#define DC_QR_FPR_MISMATCH 220 // id=contact
#define DC_QR_FPR_WITHOUT_ADDR 230 // test1=formatted fingerprint
#define DC_QR_ACCOUNT 250 // text1=domain
#define DC_QR_BACKUP 251
#define DC_QR_WEBRTC_INSTANCE 260 // text1=domain, text2=instance pattern
#define DC_QR_ADDR 320 // id=contact
#define DC_QR_TEXT 330 // text1=text
@@ -2321,7 +2320,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
* ask whether to verify the contact;
* if so, start the protocol with dc_join_securejoin().
*
* - DC_QR_ASK_VERIFYGROUP with dc_lot_t::text1=Group name:
* - DC_QR_ASK_VERIFYGROUP withdc_lot_t::text1=Group name:
* ask whether to join the group;
* if so, start the protocol with dc_join_securejoin().
*
@@ -2341,10 +2340,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
* ask the user if they want to create an account on the given domain,
* if so, call dc_set_config_from_qr() and then dc_configure().
*
* - DC_QR_BACKUP:
* ask the user if they want to set up a new device.
* If so, pass the qr-code to dc_receive_backup().
*
* - DC_QR_WEBRTC_INSTANCE with dc_lot_t::text1=domain:
* ask the user if they want to use the given service for video chats;
* if so, call dc_set_config_from_qr().
@@ -2635,117 +2630,6 @@ char* dc_get_last_error (dc_context_t* context);
void dc_str_unref (char* str);
/**
* @class dc_backup_provider_t
*
* Set up another device.
*/
/**
* Creates an object for sending a backup to another device.
*
* The backup is sent to through a peer-to-peer channel which is bootstrapped
* by a QR-code. The backup contains the entire state of the account
* including credentials. This can be used to setup a new device.
*
* This is a blocking call as some preparations are made like e.g. exporting
* the database. Once this function returns, the backup is being offered to
* remote devices. To wait until one device received the backup, use
* dc_backup_provider_wait(). Alternatively abort the operation using
* dc_stop_ongoing_process().
*
* During execution of the job #DC_EVENT_IMEX_PROGRESS is sent out to indicate
* state and progress.
*
* @memberof dc_backup_provider_t
* @param context The context.
* @return Opaque object for sending the backup.
* On errors, NULL is returned and dc_get_last_error() returns an error that
* should be shown to the user.
*/
dc_backup_provider_t* dc_backup_provider_new (dc_context_t* context);
/**
* Returns the QR code text that will offer the backup to other devices.
*
* The QR code contains a ticket which will validate the backup and provide
* authentication for both the provider and the recipient.
*
* The scanning device should call the scanned text to dc_check_qr(). If
* dc_check_qr() returns DC_QR_BACKUP, the backup transfer can be started using
* dc_get_backup().
*
* @memberof dc_backup_provider_t
* @param backup_provider The backup provider object as created by
* dc_backup_provider_new().
* @return The text that should be put in the QR code.
* On errors an empty string is returned, NULL is never returned.
* the returned string must be released using dc_str_unref() after usage.
*/
char* dc_backup_provider_get_qr (const dc_backup_provider_t* backup_provider);
/**
* Returns the QR code SVG image that will offer the backup to other devices.
*
* This works like dc_backup_provider_qr() but returns the text of a rendered
* SVG image containing the QR code.
*
* @memberof dc_backup_provider_t
* @param backup_provider The backup provider object as created by
* dc_backup_provider_new().
* @return The QR code rendered as SVG.
* On errors an empty string is returned, NULL is never returned.
* the returned string must be released using dc_str_unref() after usage.
*/
char* dc_backup_provider_get_qr_svg (const dc_backup_provider_t* backup_provider);
/**
* Waits for the sending to finish.
*
* This is a blocking call and should only be called once.
*
* @memberof dc_backup_provider_t
* @param backup_provider The backup provider object as created by
* dc_backup_provider_new(). If NULL is given nothing is done.
*/
void dc_backup_provider_wait (dc_backup_provider_t* backup_provider);
/**
* Frees a dc_backup_provider_t object.
*
* @memberof dc_backup_provider_t
* @param backup_provider The backup provider object as created by
* dc_backup_provider_new().
*/
void dc_backup_provider_unref (dc_backup_provider_t* backup_provider);
/**
* Gets a backup offered by a dc_backup_provider_t object on another device.
*
* This function is called on a device that scanned the QR code offered by
* dc_backup_sender_qr() or dc_backup_sender_qr_svg(). Typically this is a
* different device than that which provides the backup.
*
* This call will block while the backup is being transferred and only
* complete on success or failure. Use dc_stop_ongoing_process() to abort it
* early.
*
* During execution of the job #DC_EVENT_IMEX_PROGRESS is sent out to indicate
* state and progress. The process is finished when the event emits either 0
* or 1000, 0 means it failed and 1000 means it succeeded. These events are
* for showing progress and informational only, success and failure is also
* shown in the return code of this function.
*
* @memberof dc_context_t
* @param context The context.
* @param qr The qr code text, dc_check_qr() must have returned DC_QR_BACKUP
* on this text.
* @return 0=failure, 1=success.
*/
int dc_receive_backup (dc_context_t* context, const char* qr);
/**
* @class dc_accounts_t
*
@@ -3300,7 +3184,7 @@ dc_lot_t* dc_chatlist_get_summary (const dc_chatlist_t* chatlist, siz
* it takes the chat ID and the message ID as returned by dc_chatlist_get_chat_id() and dc_chatlist_get_msg_id()
* as arguments. The chatlist object itself is not needed directly.
*
* This maybe useful if you convert the complete object into a different representation
* This maybe useful if you convert the complete object into a different represenation
* as done e.g. in the node-bindings.
* If you have access to the chatlist object in some way, using this function is not recommended,
* use dc_chatlist_get_summary() in this case instead.
@@ -4443,18 +4327,6 @@ void dc_msg_set_text (dc_msg_t* msg, const char* text);
void dc_msg_set_html (dc_msg_t* msg, const char* html);
/**
* Sets the email's subject. If it's empty, a default subject
* will be used (e.g. `Message from Alice` or `Re: <last subject>`).
* This does not alter any information in the database.
*
* @memberof dc_msg_t
* @param msg The message object.
* @param subject The new subject.
*/
void dc_msg_set_subject (dc_msg_t* msg, const char* subject);
/**
* Set different sender name for a message.
* This overrides the name set by the dc_set_config()-option `displayname`.
@@ -5799,7 +5671,7 @@ void dc_event_unref(dc_event_t* event);
#define DC_EVENT_INCOMING_MSG 2005
/**
* 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,
* instead of cluttering the user with many notifications.
* For each of the msg_ids, an additional #DC_EVENT_INCOMING_MSG event was emitted before.
@@ -6457,7 +6329,7 @@ void dc_event_unref(dc_event_t* event);
///
/// Used in status messages.
///
/// @deprecated 2022-09-10
/// @deperecated 2022-09-10
#define DC_STR_EPHEMERAL_MINUTE 77
/// "Message deletion timer is set to 1 hour."
@@ -6739,7 +6611,7 @@ void dc_event_unref(dc_event_t* event);
/// "You changed your email address from %1$s to %2$s.
/// If you now send a message to a group, contacts there will automatically
/// replace the old with your new address.\n\n It's highly advised to set up
/// replace the old with your new address.\n\nIt's highly advised to set up
/// your old email provider to forward all emails to your new email address.
/// Otherwise you might miss messages of contacts who did not get your new
/// address yet." + the link to the AEAP blog post

View File

@@ -23,15 +23,14 @@ use std::sync::Arc;
use std::time::{Duration, SystemTime};
use anyhow::Context as _;
use deltachat::chat::{ChatId, ChatVisibility, MessageListOptions, MuteDuration, ProtectionStatus};
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::context::Context;
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::imex::BackupProvider;
use deltachat::key::DcKey;
use deltachat::message::MsgId;
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
use deltachat::qr_code_generator::get_securejoin_qr_svg;
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
use deltachat::stock_str::StockMessage;
use deltachat::stock_str::StockStrings;
@@ -61,8 +60,7 @@ use self::string::*;
// this avoids panics if the ui just forgets to handle a case
// - finally, this behaviour matches the old core-c API and UIs already depend on it
const DC_GCM_ADDDAYMARKER: u32 = 0x01;
const DC_GCM_INFO_ONLY: u32 = 0x02;
// TODO: constants
// dc_context_t
@@ -117,7 +115,7 @@ pub unsafe extern "C" fn dc_context_new(
match ctx {
Ok(ctx) => Box::into_raw(Box::new(ctx)),
Err(err) => {
eprintln!("failed to create context: {err:#}");
eprintln!("failed to create context: {:#}", err);
ptr::null_mut()
}
}
@@ -141,7 +139,7 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
)) {
Ok(context) => Box::into_raw(Box::new(context)),
Err(err) => {
eprintln!("failed to create context: {err:#}");
eprintln!("failed to create context: {:#}", err);
ptr::null_mut()
}
}
@@ -216,7 +214,7 @@ pub unsafe extern "C" fn dc_set_config(
if key.starts_with("ui.") {
ctx.set_ui_config(&key, value.as_deref())
.await
.with_context(|| format!("Can't set {key} to {value:?}"))
.with_context(|| format!("Can't set {} to {:?}", key, value))
.log_err(ctx, "dc_set_config() failed")
.is_ok() as libc::c_int
} else {
@@ -224,7 +222,7 @@ pub unsafe extern "C" fn dc_set_config(
Ok(key) => ctx
.set_config(key, value.as_deref())
.await
.with_context(|| format!("Can't set {key} to {value:?}"))
.with_context(|| format!("Can't set {} to {:?}", key, value))
.log_err(ctx, "dc_set_config() failed")
.is_ok() as libc::c_int,
Err(_) => {
@@ -293,12 +291,12 @@ pub unsafe extern "C" fn dc_set_stock_translation(
Some(id) => match ctx.set_stock_translation(id, msg).await {
Ok(()) => 1,
Err(err) => {
warn!(ctx, "set_stock_translation failed: {err:#}");
warn!(ctx, "set_stock_translation failed: {}", err);
0
}
},
None => {
warn!(ctx, "invalid stock message id {stock_id}");
warn!(ctx, "invalid stock message id {}", stock_id);
0
}
}
@@ -321,7 +319,7 @@ pub unsafe extern "C" fn dc_set_config_from_qr(
match qr::set_config_from_qr(ctx, &qr).await {
Ok(()) => 1,
Err(err) => {
error!(ctx, "Failed to create account from QR code: {err:#}");
error!(ctx, "Failed to create account from QR code: {}", err);
0
}
}
@@ -339,7 +337,7 @@ pub unsafe extern "C" fn dc_get_info(context: *const dc_context_t) -> *mut libc:
match ctx.get_info().await {
Ok(info) => render_info(info).unwrap_or_default().strdup(),
Err(err) => {
warn!(ctx, "failed to get info: {err:#}");
warn!(ctx, "failed to get info: {}", err);
"".strdup()
}
}
@@ -351,7 +349,7 @@ fn render_info(
) -> std::result::Result<String, std::fmt::Error> {
let mut res = String::new();
for (key, value) in &info {
writeln!(&mut res, "{key}={value}")?;
writeln!(&mut res, "{}={}", key, value)?;
}
Ok(res)
@@ -380,7 +378,7 @@ pub unsafe extern "C" fn dc_get_connectivity_html(
match ctx.get_connectivity_html().await {
Ok(html) => html.strdup(),
Err(err) => {
error!(ctx, "Failed to get connectivity html: {err:#}");
error!(ctx, "Failed to get connectivity html: {}", err);
"".strdup()
}
}
@@ -422,10 +420,6 @@ pub unsafe extern "C" fn dc_get_oauth2_url(
})
}
fn spawn_configure(ctx: Context) {
spawn(async move { ctx.configure().await.log_err(&ctx, "Configure failed") });
}
#[no_mangle]
pub unsafe extern "C" fn dc_configure(context: *mut dc_context_t) {
if context.is_null() {
@@ -434,7 +428,8 @@ pub unsafe extern "C" fn dc_configure(context: *mut dc_context_t) {
}
let ctx = &*context;
spawn_configure(ctx.clone());
spawn(async move { ctx.configure().await.log_err(ctx, "Configure failed") });
}
#[no_mangle]
@@ -1141,7 +1136,7 @@ pub unsafe extern "C" fn dc_get_draft(context: *mut dc_context_t, chat_id: u32)
}
Ok(None) => ptr::null_mut(),
Err(err) => {
error!(ctx, "Failed to get draft for chat #{chat_id}: {err:#}");
error!(ctx, "Failed to get draft for chat #{}: {}", chat_id, err);
ptr::null_mut()
}
}
@@ -1161,21 +1156,12 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
}
let ctx = &*context;
let info_only = (flags & DC_GCM_INFO_ONLY) != 0;
let add_daymarker = (flags & DC_GCM_ADDDAYMARKER) != 0;
block_on(async move {
Box::into_raw(Box::new(
chat::get_chat_msgs_ex(
ctx,
ChatId::new(chat_id),
MessageListOptions {
info_only,
add_daymarker,
},
)
.await
.unwrap_or_log_default(ctx, "failed to get chat msgs")
.into(),
chat::get_chat_msgs(ctx, ChatId::new(chat_id), flags)
.await
.unwrap_or_log_default(ctx, "failed to get chat msgs")
.into(),
))
})
}
@@ -1299,11 +1285,11 @@ pub unsafe extern "C" fn dc_get_chat_media(
} else {
Some(ChatId::new(chat_id))
};
let msg_type = from_prim(msg_type).expect(&format!("invalid msg_type = {msg_type}"));
let msg_type = from_prim(msg_type).expect(&format!("invalid msg_type = {}", msg_type));
let or_msg_type2 =
from_prim(or_msg_type2).expect(&format!("incorrect or_msg_type2 = {or_msg_type2}"));
from_prim(or_msg_type2).expect(&format!("incorrect or_msg_type2 = {}", or_msg_type2));
let or_msg_type3 =
from_prim(or_msg_type3).expect(&format!("incorrect or_msg_type3 = {or_msg_type3}"));
from_prim(or_msg_type3).expect(&format!("incorrect or_msg_type3 = {}", or_msg_type3));
block_on(async move {
Box::into_raw(Box::new(
@@ -1335,11 +1321,11 @@ pub unsafe extern "C" fn dc_get_next_media(
};
let ctx = &*context;
let msg_type = from_prim(msg_type).expect(&format!("invalid msg_type = {msg_type}"));
let msg_type = from_prim(msg_type).expect(&format!("invalid msg_type = {}", msg_type));
let or_msg_type2 =
from_prim(or_msg_type2).expect(&format!("incorrect or_msg_type2 = {or_msg_type2}"));
from_prim(or_msg_type2).expect(&format!("incorrect or_msg_type2 = {}", or_msg_type2));
let or_msg_type3 =
from_prim(or_msg_type3).expect(&format!("incorrect or_msg_type3 = {or_msg_type3}"));
from_prim(or_msg_type3).expect(&format!("incorrect or_msg_type3 = {}", or_msg_type3));
block_on(async move {
chat::get_next_media(
@@ -1731,7 +1717,7 @@ pub unsafe extern "C" fn dc_get_chat_encrinfo(
.await
.map(|s| s.strdup())
.unwrap_or_else(|e| {
error!(ctx, "{e:#}");
error!(ctx, "{}", e);
ptr::null_mut()
})
})
@@ -1894,7 +1880,7 @@ pub unsafe extern "C" fn dc_resend_msgs(
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
if let Err(err) = block_on(chat::resend_msgs(ctx, &msg_ids)) {
error!(ctx, "Resending failed: {err:#}");
error!(ctx, "Resending failed: {}", err);
0
} else {
1
@@ -1935,11 +1921,14 @@ pub unsafe extern "C" fn dc_get_msg(context: *mut dc_context_t, msg_id: u32) ->
// C-core API returns empty messages, do the same
warn!(
ctx,
"dc_get_msg called with special msg_id={msg_id}, returning empty msg"
"dc_get_msg called with special msg_id={}, returning empty msg", msg_id
);
message::Message::default()
} else {
error!(ctx, "dc_get_msg could not retrieve msg_id {msg_id}: {e:#}");
error!(
ctx,
"dc_get_msg could not retrieve msg_id {}: {}", msg_id, e
);
return ptr::null_mut();
}
}
@@ -2004,10 +1993,12 @@ pub unsafe extern "C" fn dc_create_contact(
let ctx = &*context;
let name = to_string_lossy(name);
block_on(Contact::create(ctx, &name, &to_string_lossy(addr)))
.log_err(ctx, "Cannot create contact")
.map(|id| id.to_u32())
.unwrap_or(0)
block_on(async move {
Contact::create(ctx, &name, &to_string_lossy(addr))
.await
.map(|id| id.to_u32())
.unwrap_or(0)
})
}
#[no_mangle]
@@ -2132,7 +2123,7 @@ pub unsafe extern "C" fn dc_get_contact_encrinfo(
.await
.map(|s| s.strdup())
.unwrap_or_else(|e| {
error!(ctx, "{e:#}");
error!(ctx, "{}", e);
ptr::null_mut()
})
})
@@ -2154,7 +2145,7 @@ pub unsafe extern "C" fn dc_delete_contact(
match Contact::delete(ctx, contact_id).await {
Ok(_) => 1,
Err(err) => {
error!(ctx, "cannot delete contact: {err:#}");
error!(ctx, "cannot delete contact: {}", err);
0
}
}
@@ -2180,14 +2171,6 @@ pub unsafe extern "C" fn dc_get_contact(
})
}
fn spawn_imex(ctx: Context, what: imex::ImexMode, param1: String, passphrase: Option<String>) {
spawn(async move {
imex::imex(&ctx, what, param1.as_ref(), passphrase)
.await
.log_err(&ctx, "IMEX failed")
});
}
#[no_mangle]
pub unsafe extern "C" fn dc_imex(
context: *mut dc_context_t,
@@ -2202,7 +2185,7 @@ pub unsafe extern "C" fn dc_imex(
let what = match imex::ImexMode::from_i32(what_raw) {
Some(what) => what,
None => {
eprintln!("ignoring invalid argument {what_raw} to dc_imex");
eprintln!("ignoring invalid argument {} to dc_imex", what_raw);
return;
}
};
@@ -2211,7 +2194,11 @@ pub unsafe extern "C" fn dc_imex(
let ctx = &*context;
if let Some(param1) = to_opt_string_lossy(param1) {
spawn_imex(ctx.clone(), what, param1, passphrase);
spawn(async move {
imex::imex(ctx, what, param1.as_ref(), passphrase)
.await
.log_err(ctx, "IMEX failed")
});
} else {
eprintln!("dc_imex called without a valid directory");
}
@@ -2234,7 +2221,7 @@ pub unsafe extern "C" fn dc_imex_has_backup(
Err(err) => {
// do not bubble up error to the user,
// the ui will expect that the file does not exist or cannot be accessed
warn!(ctx, "dc_imex_has_backup: {err:#}");
warn!(ctx, "dc_imex_has_backup: {}", err);
ptr::null_mut()
}
}
@@ -2253,7 +2240,7 @@ pub unsafe extern "C" fn dc_initiate_key_transfer(context: *mut dc_context_t) ->
match imex::initiate_key_transfer(ctx).await {
Ok(res) => res.strdup(),
Err(err) => {
error!(ctx, "dc_initiate_key_transfer(): {err:#}");
error!(ctx, "dc_initiate_key_transfer(): {}", err);
ptr::null_mut()
}
}
@@ -2278,7 +2265,7 @@ pub unsafe extern "C" fn dc_continue_key_transfer(
{
Ok(()) => 1,
Err(err) => {
warn!(ctx, "dc_continue_key_transfer: {err:#}");
warn!(ctx, "dc_continue_key_transfer: {}", err);
0
}
}
@@ -2666,7 +2653,7 @@ pub unsafe fn dc_array_is_independent(
///
/// This is the structure behind [dc_chatlist_t] which is the opaque
/// structure representing a chatlist in the FFI API. It exists
/// because the FFI API has a reference from the message to the
/// because the FFI API has a refernce from the message to the
/// context, but the Rust API does not, so the FFI layer needs to glue
/// these together.
pub struct ChatlistWrapper {
@@ -2709,7 +2696,7 @@ pub unsafe extern "C" fn dc_chatlist_get_chat_id(
match ffi_list.list.get_chat_id(index) {
Ok(chat_id) => chat_id.to_u32(),
Err(err) => {
warn!(ctx, "get_chat_id failed: {err:#}");
warn!(ctx, "get_chat_id failed: {}", err);
0
}
}
@@ -2729,7 +2716,7 @@ pub unsafe extern "C" fn dc_chatlist_get_msg_id(
match ffi_list.list.get_msg_id(index) {
Ok(msg_id) => msg_id.map_or(0, |msg_id| msg_id.to_u32()),
Err(err) => {
warn!(ctx, "get_msg_id failed: {err:#}");
warn!(ctx, "get_msg_id failed: {}", err);
0
}
}
@@ -2810,7 +2797,7 @@ pub unsafe extern "C" fn dc_chatlist_get_context(
///
/// This is the structure behind [dc_chat_t] which is the opaque
/// structure representing a chat in the FFI API. It exists
/// because the FFI API has a reference from the message to the
/// because the FFI API has a refernce from the message to the
/// context, but the Rust API does not, so the FFI layer needs to glue
/// these together.
pub struct ChatWrapper {
@@ -2888,7 +2875,7 @@ pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut
Ok(Some(p)) => p.to_string_lossy().strdup(),
Ok(None) => ptr::null_mut(),
Err(err) => {
error!(ctx, "failed to get profile image: {err:#}");
error!(ctx, "failed to get profile image: {:?}", err);
ptr::null_mut()
}
}
@@ -3040,7 +3027,7 @@ pub unsafe extern "C" fn dc_chat_get_info_json(
let chat = match chat::Chat::load_from_db(ctx, ChatId::new(chat_id)).await {
Ok(chat) => chat,
Err(err) => {
error!(ctx, "dc_get_chat_info_json() failed to load chat: {err:#}");
error!(ctx, "dc_get_chat_info_json() failed to load chat: {}", err);
return "".strdup();
}
};
@@ -3049,7 +3036,7 @@ pub unsafe extern "C" fn dc_chat_get_info_json(
Err(err) => {
error!(
ctx,
"dc_get_chat_info_json() failed to get chat info: {err:#}"
"dc_get_chat_info_json() failed to get chat info: {}", err
);
return "".strdup();
}
@@ -3066,7 +3053,7 @@ pub unsafe extern "C" fn dc_chat_get_info_json(
///
/// This is the structure behind [dc_msg_t] which is the opaque
/// structure representing a message in the FFI API. It exists
/// because the FFI API has a reference from the message to the
/// because the FFI API has a refernce from the message to the
/// context, but the Rust API does not, so the FFI layer needs to glue
/// these together.
pub struct MessageWrapper {
@@ -3086,7 +3073,7 @@ pub unsafe extern "C" fn dc_msg_new(
return ptr::null_mut();
}
let context = &*context;
let viewtype = from_prim(viewtype).expect(&format!("invalid viewtype = {viewtype}"));
let viewtype = from_prim(viewtype).expect(&format!("invalid viewtype = {}", viewtype));
let msg = MessageWrapper {
context,
message: message::Message::new(viewtype),
@@ -3269,7 +3256,7 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_blob(
ptr as *mut libc::c_char
}
Err(err) => {
eprintln!("failed read blob from archive: {err}");
eprintln!("failed read blob from archive: {}", err);
ptr::null_mut()
}
}
@@ -3288,7 +3275,7 @@ pub unsafe extern "C" fn dc_msg_get_webxdc_info(msg: *mut dc_msg_t) -> *mut libc
let info = match ffi_msg.message.get_webxdc_info(ctx).await {
Ok(info) => info,
Err(err) => {
error!(ctx, "dc_msg_get_webxdc_info() failed to get info: {err:#}");
error!(ctx, "dc_msg_get_webxdc_info() failed to get info: {}", err);
return "".strdup();
}
};
@@ -3601,16 +3588,6 @@ pub unsafe extern "C" fn dc_msg_set_html(msg: *mut dc_msg_t, html: *const libc::
ffi_msg.message.set_html(to_opt_string_lossy(html))
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_set_subject(msg: *mut dc_msg_t, subject: *const libc::c_char) {
if msg.is_null() {
eprintln!("ignoring careless call to dc_msg_get_subject()");
return;
}
let ffi_msg = &mut *msg;
ffi_msg.message.set_subject(to_string_lossy(subject));
}
#[no_mangle]
pub unsafe extern "C" fn dc_msg_set_override_sender_name(
msg: *mut dc_msg_t,
@@ -3819,7 +3796,7 @@ pub unsafe extern "C" fn dc_msg_force_plaintext(msg: *mut dc_msg_t) {
///
/// This is the structure behind [dc_contact_t] which is the opaque
/// structure representing a contact in the FFI API. It exists
/// because the FFI API has a reference from the message to the
/// because the FFI API has a refernce from the message to the
/// context, but the Rust API does not, so the FFI layer needs to glue
/// these together.
pub struct ContactWrapper {
@@ -4143,116 +4120,6 @@ pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
libc::free(s as *mut _)
}
pub struct BackupProviderWrapper {
context: *const dc_context_t,
provider: BackupProvider,
}
pub type dc_backup_provider_t = BackupProviderWrapper;
#[no_mangle]
pub unsafe extern "C" fn dc_backup_provider_new(
context: *mut dc_context_t,
) -> *mut dc_backup_provider_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_backup_provider_new()");
return ptr::null_mut();
}
let ctx = &*context;
block_on(BackupProvider::prepare(ctx))
.map(|provider| BackupProviderWrapper {
context: ctx,
provider,
})
.map(|ffi_provider| Box::into_raw(Box::new(ffi_provider)))
.log_err(ctx, "BackupProvider failed")
.context("BackupProvider failed")
.set_last_error(ctx)
.unwrap_or(ptr::null_mut())
}
#[no_mangle]
pub unsafe extern "C" fn dc_backup_provider_get_qr(
provider: *const dc_backup_provider_t,
) -> *mut libc::c_char {
if provider.is_null() {
eprintln!("ignoring careless call to dc_backup_provider_qr");
return "".strdup();
}
let ffi_provider = &*provider;
let ctx = &*ffi_provider.context;
deltachat::qr::format_backup(&ffi_provider.provider.qr())
.log_err(ctx, "BackupProvider get_qr failed")
.context("BackupProvider get_qr failed")
.set_last_error(ctx)
.unwrap_or_default()
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_backup_provider_get_qr_svg(
provider: *const dc_backup_provider_t,
) -> *mut libc::c_char {
if provider.is_null() {
eprintln!("ignoring careless call to dc_backup_provider_qr_svg()");
return "".strdup();
}
let ffi_provider = &*provider;
let ctx = &*ffi_provider.context;
let provider = &ffi_provider.provider;
block_on(generate_backup_qr(ctx, &provider.qr()))
.log_err(ctx, "BackupProvider get_qr_svg failed")
.context("BackupProvider get_qr_svg failed")
.set_last_error(ctx)
.unwrap_or_default()
.strdup()
}
#[no_mangle]
pub unsafe extern "C" fn dc_backup_provider_wait(provider: *mut dc_backup_provider_t) {
if provider.is_null() {
eprintln!("ignoring careless call to dc_backup_provider_wait()");
return;
}
let ffi_provider = &mut *provider;
let ctx = &*ffi_provider.context;
let provider = &mut ffi_provider.provider;
block_on(provider)
.log_err(ctx, "Failed to await BackupProvider")
.context("Failed to await BackupProvider")
.set_last_error(ctx)
.ok();
}
#[no_mangle]
pub unsafe extern "C" fn dc_backup_provider_unref(provider: *mut dc_backup_provider_t) {
drop(Box::from_raw(provider));
}
#[no_mangle]
pub unsafe extern "C" fn dc_receive_backup(
context: *mut dc_context_t,
qr: *const libc::c_char,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_receive_backup()");
return 0;
}
let ctx = &*context;
let qr_text = to_string_lossy(qr);
let qr = match block_on(qr::check_qr(ctx, &qr_text)).log_err(ctx, "Invalid QR code") {
Ok(qr) => qr,
Err(_) => return 0,
};
spawn(async move {
imex::get_backup(ctx, qr)
.await
.log_err(ctx, "Get backup failed")
.ok();
});
1
}
trait ResultExt<T, E> {
/// Like `log_err()`, but:
/// - returns the default value instead of an Err value.
@@ -4266,63 +4133,13 @@ impl<T: Default, E: std::fmt::Display> ResultExt<T, E> for Result<T, E> {
match self {
Ok(t) => t,
Err(err) => {
error!(context, "{message}: {err:#}");
error!(context, "{}: {}", message, err);
Default::default()
}
}
}
}
trait ResultLastError<T, E>
where
E: std::fmt::Display,
{
/// Sets this `Err` value using [`Context::set_last_error`].
///
/// Normally each FFI-API *should* call this if it handles an error from the Rust API:
/// errors which need to be reported to users in response to an API call need to be
/// propagated up the Rust API and at the FFI boundary need to be stored into the "last
/// error" so the FFI users can retrieve an appropriate error message on failure. Often
/// you will want to combine this with a call to [`LogExt::log_err`].
///
/// Since historically calls to the `deltachat::log::error!()` macro were (and sometimes
/// still are) shown as error toasts to the user, this macro also calls
/// [`Context::set_last_error`]. It is preferable however to rely on normal error
/// propagation in Rust code however and only use this `ResultExt::set_last_error` call
/// in the FFI layer.
///
/// # Example
///
/// Fully handling an error in the FFI code looks like this currently:
///
/// ```no_compile
/// some_dc_rust_api_call_returning_result()
/// .log_err(&context, "My API call failed")
/// .context("My API call failed")
/// .set_last_error(&context)
/// .unwrap_or_default()
/// ```
///
/// As shows it is a shame the `.log_err()` call currently needs a message instead of
/// relying on an implicit call to the [`anyhow::Context`] call if needed. This stems
/// from a time before we fully embraced anyhow. Some day we'll also fix that.
///
/// [`Context::set_last_error`]: context::Context::set_last_error
fn set_last_error(self, context: &context::Context) -> Result<T, E>;
}
impl<T, E> ResultLastError<T, E> for Result<T, E>
where
E: std::fmt::Display,
{
fn set_last_error(self, context: &context::Context) -> Result<T, E> {
if let Err(ref err) = self {
context.set_last_error(&format!("{err:#}"));
}
self
}
}
trait ResultNullableExt<T> {
fn into_raw(self) -> *mut T;
}
@@ -4492,7 +4309,7 @@ pub unsafe extern "C" fn dc_accounts_new(
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
Err(err) => {
// We are using Anyhow's .context() and to show the inner error, too, we need the {:#}:
eprintln!("failed to create accounts: {err:#}");
eprintln!("failed to create accounts: {:#}", err);
ptr::null_mut()
}
}
@@ -4560,7 +4377,8 @@ pub unsafe extern "C" fn dc_accounts_select_account(
Ok(()) => 1,
Err(err) => {
accounts.emit_event(EventType::Error(format!(
"Failed to select account: {err:#}"
"Failed to select account: {:#}",
err
)));
0
}
@@ -4582,7 +4400,10 @@ pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -
match accounts.add_account().await {
Ok(id) => id,
Err(err) => {
accounts.emit_event(EventType::Error(format!("Failed to add account: {err:#}")));
accounts.emit_event(EventType::Error(format!(
"Failed to add account: {:#}",
err
)));
0
}
}
@@ -4603,7 +4424,10 @@ pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *mut dc_accoun
match accounts.add_closed_account().await {
Ok(id) => id,
Err(err) => {
accounts.emit_event(EventType::Error(format!("Failed to add account: {err:#}")));
accounts.emit_event(EventType::Error(format!(
"Failed to add account: {:#}",
err
)));
0
}
}
@@ -4628,7 +4452,8 @@ pub unsafe extern "C" fn dc_accounts_remove_account(
Ok(()) => 1,
Err(err) => {
accounts.emit_event(EventType::Error(format!(
"Failed to remove account: {err:#}"
"Failed to remove account: {:#}",
err
)));
0
}
@@ -4658,7 +4483,8 @@ pub unsafe extern "C" fn dc_accounts_migrate_account(
Ok(id) => id,
Err(err) => {
accounts.emit_event(EventType::Error(format!(
"Failed to migrate account: {err:#}"
"Failed to migrate account: {:#}",
err
)));
0
}
@@ -4772,22 +4598,33 @@ mod jsonrpc {
return ptr::null_mut();
}
let account_manager = &*account_manager;
let events = block_on(account_manager.read()).get_event_emitter();
let cmd_api = deltachat_jsonrpc::api::CommandApi::from_arc(account_manager.inner.clone());
let cmd_api =
deltachat_jsonrpc::api::CommandApi::from_arc((*account_manager).inner.clone());
let (request_handle, receiver) = RpcClient::new();
let handle = RpcSession::new(request_handle.clone(), cmd_api);
let request_handle2 = request_handle.clone();
let handle = RpcSession::new(request_handle, cmd_api);
let event_thread = spawn(async move {
while let Some(event) = events.recv().await {
let event = event_to_json_rpc_notification(event);
request_handle
.send_notification("event", Some(event))
.await?;
let events = block_on({
async {
let am = (*account_manager).inner.clone();
let ev = am.read().await.get_event_emitter();
drop(am);
ev
}
});
let event_thread = spawn({
async move {
while let Some(event) = events.recv().await {
let event = event_to_json_rpc_notification(event);
request_handle2
.send_notification("event", Some(event))
.await?;
}
let res: Result<(), anyhow::Error> = Ok(());
res
}
let res: Result<(), anyhow::Error> = Ok(());
res
});
let instance = dc_jsonrpc_instance_t {
@@ -4809,12 +4646,6 @@ mod jsonrpc {
drop(Box::from_raw(jsonrpc_instance));
}
fn spawn_handle_jsonrpc_request(handle: RpcSession<CommandApi>, request: String) {
spawn(async move {
handle.handle_incoming(&request).await;
});
}
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_request(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
@@ -4825,9 +4656,12 @@ mod jsonrpc {
return;
}
let handle = &(*jsonrpc_instance).handle;
let api = &*jsonrpc_instance;
let handle = &api.handle;
let request = to_string_lossy(request);
spawn_handle_jsonrpc_request(handle.clone(), request);
spawn(async move {
handle.handle_incoming(&request).await;
});
}
#[no_mangle]

View File

@@ -14,8 +14,6 @@ use crate::summary::{Summary, SummaryPrefix};
/// eg. by chatlist.get_summary() or dc_msg_get_summary().
///
/// *Lot* is used in the meaning *heap* here.
// The QR code grew too large. So be it.
#[allow(clippy::large_enum_variant)]
#[derive(Debug)]
pub enum Lot {
Summary(Summary),
@@ -24,15 +22,20 @@ pub enum Lot {
}
#[repr(u8)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Meaning {
#[default]
None = 0,
Text1Draft = 1,
Text1Username = 2,
Text1Self = 3,
}
impl Default for Meaning {
fn default() -> Self {
Meaning::None
}
}
impl Lot {
pub fn get_text1(&self) -> Option<&str> {
match self {
@@ -49,7 +52,6 @@ impl Lot {
Qr::FprMismatch { .. } => None,
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
Qr::Account { domain } => Some(domain),
Qr::Backup { .. } => None,
Qr::WebrtcInstance { domain, .. } => Some(domain),
Qr::Addr { draft, .. } => draft.as_deref(),
Qr::Url { url } => Some(url),
@@ -101,7 +103,6 @@ impl Lot {
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
Qr::Account { .. } => LotState::QrAccount,
Qr::Backup { .. } => LotState::QrBackup,
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
Qr::Addr { .. } => LotState::QrAddr,
Qr::Url { .. } => LotState::QrUrl,
@@ -126,7 +127,6 @@ impl Lot {
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
Qr::FprWithoutAddr { .. } => Default::default(),
Qr::Account { .. } => Default::default(),
Qr::Backup { .. } => Default::default(),
Qr::WebrtcInstance { .. } => Default::default(),
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
Qr::Url { .. } => Default::default(),
@@ -151,9 +151,9 @@ impl Lot {
}
#[repr(u32)]
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LotState {
#[default]
// Default
Undefined = 0,
// Qr States
@@ -175,8 +175,6 @@ pub enum LotState {
/// text1=domain
QrAccount = 250,
QrBackup = 251,
/// text1=domain, text2=instance pattern
QrWebrtcInstance = 260,
@@ -217,6 +215,12 @@ pub enum LotState {
MsgOutMdnRcvd = 28,
}
impl Default for LotState {
fn default() -> Self {
LotState::Undefined
}
}
impl From<MessageState> for LotState {
fn from(s: MessageState) -> Self {
use MessageState::*;

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.111.0"
version = "1.106.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -19,24 +19,22 @@ serde = { version = "1.0", features = ["derive"] }
tempfile = "3.3.0"
log = "0.4"
async-channel = { version = "1.8.0" }
futures = { version = "0.3.26" }
futures = { version = "0.3.25" }
serde_json = "1.0.91"
yerpc = { version = "0.4.3", features = ["anyhow_expose"] }
yerpc = { version = "^0.3.1", features = ["anyhow_expose"] }
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
tokio = { version = "1.25.0" }
tokio = { version = "1.23.1" }
sanitize-filename = "0.4"
walkdir = "2.3.2"
base64 = "0.21"
# optional dependencies
axum = { version = "0.6.11", optional = true, features = ["ws"] }
axum = { version = "0.6.1", optional = true, features = ["ws"] }
env_logger = { version = "0.10.0", optional = true }
[dev-dependencies]
tokio = { version = "1.25.0", features = ["full", "rt-multi-thread"] }
tokio = { version = "1.23.1", features = ["full", "rt-multi-thread"] }
[features]
default = ["vendored"]
webserver = ["dep:env_logger", "dep:axum", "tokio/full", "yerpc/support-axum"]
vendored = ["deltachat/vendored"]
default = []
webserver = ["env_logger", "axum", "tokio/full", "yerpc/support-axum"]

View File

@@ -35,7 +35,7 @@ The server can be configured with environment variables:
|`DC_PORT`|`20808`|port to listen on|
|`DC_ACCOUNTS_PATH`|`./accounts`|path to storage directory|
If you are targeting other architectures (like KaiOS or Android), the webserver binary can be cross-compiled easily with [rust-cross](https://github.com/cross-rs/cross):
If you are targetting other architectures (like KaiOS or Android), the webserver binary can be cross-compiled easily with [rust-cross](https://github.com/cross-rs/cross):
```sh
cross build --features=webserver --target armv7-linux-androideabi --release

View File

@@ -4,12 +4,10 @@ use std::{collections::HashMap, str::FromStr};
use anyhow::{anyhow, bail, ensure, Context, Result};
pub use deltachat::accounts::Accounts;
use deltachat::qr::Qr;
use deltachat::{
chat::{
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
ProtectionStatus,
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, marknoticed_chat,
remove_contact_from_chat, Chat, ChatId, ChatItem, ProtectionStatus,
},
chatlist::Chatlist,
config::Config,
@@ -23,15 +21,14 @@ use deltachat::{
},
provider::get_provider_info,
qr,
qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg},
qr_code_generator::get_securejoin_qr_svg,
reaction::send_reaction,
securejoin,
stock_str::StockMessage,
webxdc::StatusUpdateSerial,
};
use sanitize_filename::is_sanitized;
use tokio::fs;
use tokio::sync::{watch, Mutex, RwLock};
use tokio::{fs, sync::RwLock};
use walkdir::WalkDir;
use yerpc::rpc;
@@ -43,12 +40,10 @@ use types::account::Account;
use types::chat::FullChat;
use types::chat_list::ChatListEntry;
use types::contact::ContactObject;
use types::message::MessageData;
use types::message::MessageObject;
use types::provider_info::ProviderInfo;
use types::webxdc::WebxdcMessageInfo;
use self::types::message::MessageLoadResult;
use self::types::{
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
location::JsonrpcLocation,
@@ -59,45 +54,21 @@ use self::types::{
use crate::api::types::chat_list::{get_chat_list_item_by_id, ChatListItemFetchResult};
use crate::api::types::qr::QrObject;
#[derive(Debug)]
struct AccountState {
/// The Qr code for current [`CommandApi::provide_backup`] call.
///
/// If there currently is a call to [`CommandApi::provide_backup`] this will be
/// `Pending` or `Ready`, otherwise `NoProvider`.
backup_provider_qr: watch::Sender<ProviderQr>,
}
impl Default for AccountState {
fn default() -> Self {
let (tx, _rx) = watch::channel(ProviderQr::NoProvider);
Self {
backup_provider_qr: tx,
}
}
}
#[derive(Clone, Debug)]
pub struct CommandApi {
pub(crate) accounts: Arc<RwLock<Accounts>>,
states: Arc<Mutex<BTreeMap<u32, AccountState>>>,
}
impl CommandApi {
pub fn new(accounts: Accounts) -> Self {
CommandApi {
accounts: Arc::new(RwLock::new(accounts)),
states: Arc::new(Mutex::new(BTreeMap::new())),
}
}
#[allow(dead_code)]
pub fn from_arc(accounts: Arc<RwLock<Accounts>>) -> Self {
CommandApi {
accounts,
states: Arc::new(Mutex::new(BTreeMap::new())),
}
CommandApi { accounts }
}
async fn get_context(&self, id: u32) -> Result<deltachat::context::Context> {
@@ -109,47 +80,10 @@ impl CommandApi {
.ok_or_else(|| anyhow!("account with id {} not found", id))?;
Ok(sc)
}
async fn with_state<F, T>(&self, id: u32, with_state: F) -> T
where
F: FnOnce(&AccountState) -> T,
{
let mut states = self.states.lock().await;
let state = states.entry(id).or_insert_with(Default::default);
with_state(state)
}
async fn inner_get_backup_qr(&self, account_id: u32) -> Result<Qr> {
let mut receiver = self
.with_state(account_id, |state| state.backup_provider_qr.subscribe())
.await;
let val: ProviderQr = receiver.borrow_and_update().clone();
match val {
ProviderQr::NoProvider => bail!("No backup being provided"),
ProviderQr::Pending => loop {
if receiver.changed().await.is_err() {
bail!("No backup being provided (account state dropped)");
}
let val: ProviderQr = receiver.borrow().clone();
match val {
ProviderQr::NoProvider => bail!("No backup being provided"),
ProviderQr::Pending => continue,
ProviderQr::Ready(qr) => break Ok(qr),
};
},
ProviderQr::Ready(qr) => Ok(qr),
}
}
}
#[rpc(all_positional, ts_outdir = "typescript/generated")]
impl CommandApi {
/// Test function.
async fn sleep(&self, delay: f64) {
tokio::time::sleep(std::time::Duration::from_secs_f64(delay)).await
}
// ---------------------------------------------
// Misc top level functions
// ---------------------------------------------
@@ -173,13 +107,7 @@ impl CommandApi {
}
async fn remove_account(&self, account_id: u32) -> Result<()> {
self.accounts
.write()
.await
.remove_account(account_id)
.await?;
self.states.lock().await.remove(&account_id);
Ok(())
self.accounts.write().await.remove_account(account_id).await
}
async fn get_all_account_ids(&self) -> Vec<u32> {
@@ -206,7 +134,7 @@ impl CommandApi {
if let Some(ctx) = context_option {
accounts.push(Account::from_context(&ctx, id).await?)
} else {
println!("account with id {id} doesn't exist anymore");
println!("account with id {} doesn't exist anymore", id);
}
}
Ok(accounts)
@@ -314,7 +242,7 @@ impl CommandApi {
for (key, value) in config.into_iter() {
set_config(&ctx, &key, value.as_deref())
.await
.with_context(|| format!("Can't set {key} to {value:?}"))?;
.with_context(|| format!("Can't set {} to {:?}", key, value))?;
}
Ok(())
}
@@ -531,7 +459,7 @@ impl CommandApi {
Ok(res) => res,
Err(err) => ChatListItemFetchResult::Error {
id: entry.0,
error: format!("{err:#}"),
error: format!("{:?}", err),
},
},
);
@@ -875,7 +803,7 @@ impl CommandApi {
let ctx = self.get_context(account_id).await?;
// TODO: implement this in core with an SQL query, that will be way faster
let messages = get_chat_msgs(&ctx, ChatId::new(chat_id)).await?;
let messages = get_chat_msgs(&ctx, ChatId::new(chat_id), 0).await?;
let mut first_unread_message_id = None;
for item in messages.into_iter().rev() {
if let ChatItem::Message { msg_id } = item {
@@ -950,23 +878,9 @@ impl CommandApi {
markseen_msgs(&ctx, msg_ids.into_iter().map(MsgId::new).collect()).await
}
async fn get_message_ids(
&self,
account_id: u32,
chat_id: u32,
info_only: bool,
add_daymarker: bool,
) -> Result<Vec<u32>> {
async fn get_message_ids(&self, account_id: u32, chat_id: u32, flags: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let msg = get_chat_msgs_ex(
&ctx,
ChatId::new(chat_id),
MessageListOptions {
info_only,
add_daymarker,
},
)
.await?;
let msg = get_chat_msgs(&ctx, ChatId::new(chat_id), flags).await?;
Ok(msg
.iter()
.map(|chat_item| -> u32 {
@@ -982,19 +896,10 @@ impl CommandApi {
&self,
account_id: u32,
chat_id: u32,
info_only: bool,
add_daymarker: bool,
flags: u32,
) -> Result<Vec<JSONRPCMessageListItem>> {
let ctx = self.get_context(account_id).await?;
let msg = get_chat_msgs_ex(
&ctx,
ChatId::new(chat_id),
MessageListOptions {
info_only,
add_daymarker,
},
)
.await?;
let msg = get_chat_msgs(&ctx, ChatId::new(chat_id), flags).await?;
Ok(msg
.iter()
.map(|chat_item| (*chat_item).into())
@@ -1011,27 +916,17 @@ impl CommandApi {
MsgId::new(message_id).get_html(&ctx).await
}
/// get multiple messages in one call,
/// if loading one message fails the error is stored in the result object in it's place.
///
/// this is the batch variant of [get_message]
async fn get_messages(
&self,
account_id: u32,
message_ids: Vec<u32>,
) -> Result<HashMap<u32, MessageLoadResult>> {
) -> Result<HashMap<u32, MessageObject>> {
let ctx = self.get_context(account_id).await?;
let mut messages: HashMap<u32, MessageLoadResult> = HashMap::new();
let mut messages: HashMap<u32, MessageObject> = HashMap::new();
for message_id in message_ids {
let message_result = MessageObject::from_message_id(&ctx, message_id).await;
messages.insert(
message_id,
match message_result {
Ok(message) => MessageLoadResult::Message(message),
Err(error) => MessageLoadResult::LoadingError {
error: format!("{error:#}"),
},
},
MessageObject::from_message_id(&ctx, message_id).await?,
);
}
Ok(messages)
@@ -1389,13 +1284,16 @@ impl CommandApi {
passphrase: Option<String>,
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
imex::imex(
ctx.stop_io().await;
let result = imex::imex(
&ctx,
imex::ImexMode::ExportBackup,
destination.as_ref(),
passphrase,
)
.await
.await;
ctx.start_io().await;
result
}
async fn import_backup(
@@ -1414,75 +1312,6 @@ impl CommandApi {
.await
}
/// Offers a backup for remote devices to retrieve.
///
/// Can be cancelled by stopping the ongoing process. Success or failure can be tracked
/// via the `ImexProgress` event which should either reach `1000` for success or `0` for
/// failure.
///
/// This **stops IO** while it is running.
///
/// Returns once a remote device has retrieved the backup, or is cancelled.
async fn provide_backup(&self, account_id: u32) -> Result<()> {
let ctx = self.get_context(account_id).await?;
self.with_state(account_id, |state| {
state.backup_provider_qr.send_replace(ProviderQr::Pending);
})
.await;
let provider = imex::BackupProvider::prepare(&ctx).await?;
self.with_state(account_id, |state| {
state
.backup_provider_qr
.send_replace(ProviderQr::Ready(provider.qr()));
})
.await;
provider.await
}
/// Returns the text of the QR code for the running [`CommandApi::provide_backup`].
///
/// This QR code text can be used in [`CommandApi::get_backup`] on a second device to
/// retrieve the backup and setup this second device.
///
/// This call will fail if there is currently no concurrent call to
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
/// ready.
async fn get_backup_qr(&self, account_id: u32) -> Result<String> {
let qr = self.inner_get_backup_qr(account_id).await?;
qr::format_backup(&qr)
}
/// Returns the rendered QR code for the running [`CommandApi::provide_backup`].
///
/// This QR code can be used in [`CommandApi::get_backup`] on a second device to
/// retrieve the backup and setup this second device.
///
/// This call will fail if there is currently no concurrent call to
/// [`CommandApi::provide_backup`]. This call may block if the QR code is not yet
/// ready.
///
/// Returns the QR code rendered as an SVG image.
async fn get_backup_qr_svg(&self, account_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let qr = self.inner_get_backup_qr(account_id).await?;
generate_backup_qr(&ctx, &qr).await
}
/// Gets a backup from a remote provider.
///
/// This retrieves the backup from a remote device over the network and imports it into
/// the current device.
///
/// Can be cancelled by stopping the ongoing process.
async fn get_backup(&self, account_id: u32, qr_text: String) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let qr = qr::check_qr(&ctx, &qr_text).await?;
imex::get_backup(&ctx, qr).await?;
Ok(())
}
// ---------------------------------------------
// connectivity
// ---------------------------------------------
@@ -1593,23 +1422,6 @@ impl CommandApi {
WebxdcMessageInfo::get_for_message(&ctx, MsgId::new(instance_msg_id)).await
}
/// Get blob encoded as base64 from a webxdc message
///
/// path is the path of the file within webxdc archive
async fn get_webxdc_blob(
&self,
account_id: u32,
instance_msg_id: u32,
path: String,
) -> Result<String> {
let ctx = self.get_context(account_id).await?;
let message = Message::load_from_db(&ctx, MsgId::new(instance_msg_id)).await?;
let blob = message.get_webxdc_blob(&ctx, &path).await?;
use base64::{engine::general_purpose, Engine as _};
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
}
/// Forward messages to another chat.
///
/// All types of messages can be forwarded,
@@ -1659,48 +1471,6 @@ impl CommandApi {
Ok(message_id.to_u32())
}
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut message = Message::new(if let Some(viewtype) = data.viewtype {
viewtype.into()
} else if data.file.is_some() {
Viewtype::File
} else {
Viewtype::Text
});
if data.text.is_some() {
message.set_text(data.text);
}
if data.html.is_some() {
message.set_html(data.html);
}
if data.override_sender_name.is_some() {
message.set_override_sender_name(data.override_sender_name);
}
if let Some(file) = data.file {
message.set_file(file, None);
}
if let Some((latitude, longitude)) = data.location {
message.set_location(latitude, longitude);
}
if let Some(id) = data.quoted_message_id {
message
.set_quote(
&ctx,
Some(
&Message::load_from_db(&ctx, MsgId::new(id))
.await
.context("message to quote could not be loaded")?,
),
)
.await?;
}
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut message)
.await?
.to_u32();
Ok(msg_id)
}
// ---------------------------------------------
// functions for the composer
// the composer is the message input field
@@ -1944,7 +1714,7 @@ async fn set_config(
ctx.set_ui_config(key, value).await?;
} else {
ctx.set_config(
Config::from_str(key).with_context(|| format!("unknown key {key:?}"))?,
Config::from_str(key).with_context(|| format!("unknown key {:?}", key))?,
value,
)
.await?;
@@ -1966,19 +1736,7 @@ async fn get_config(
if key.starts_with("ui.") {
ctx.get_ui_config(key).await
} else {
ctx.get_config(Config::from_str(key).with_context(|| format!("unknown key {key:?}"))?)
ctx.get_config(Config::from_str(key).with_context(|| format!("unknown key {:?}", key))?)
.await
}
}
/// Whether a QR code for a BackupProvider is currently available.
#[allow(clippy::large_enum_variant)]
#[derive(Clone, Debug)]
enum ProviderQr {
/// There is no provider, asking for a QR is an error.
NoProvider,
/// There is a provider, the QR code is pending.
Pending,
/// There is a provider and QR code.
Ready(Qr),
}

View File

@@ -19,13 +19,6 @@ use super::contact::ContactObject;
use super::reactions::JSONRPCReactions;
use super::webxdc::WebxdcMessageInfo;
#[derive(Serialize, TypeDef)]
#[serde(rename_all = "camelCase", tag = "variant")]
pub enum MessageLoadResult {
Message(MessageObject),
LoadingError { error: String },
}
#[derive(Serialize, TypeDef)]
#[serde(rename = "Message", rename_all = "camelCase")]
pub struct MessageObject {
@@ -55,10 +48,6 @@ pub struct MessageObject {
is_setupmessage: bool,
is_info: bool,
is_forwarded: bool,
/// True if the message was sent by a bot.
is_bot: bool,
/// when is_info is true this describes what type of system message it is
system_message_type: SystemMessageType,
@@ -193,7 +182,6 @@ impl MessageObject {
is_setupmessage: message.is_setupmessage(),
is_info: message.is_info(),
is_forwarded: message.is_forwarded(),
is_bot: message.is_bot(),
system_message_type: message.get_info_type().into(),
duration: message.get_duration(),
@@ -502,15 +490,3 @@ impl From<ChatItem> for JSONRPCMessageListItem {
}
}
}
#[derive(Deserialize, TypeDef)]
#[serde(rename_all = "camelCase")]
pub struct MessageData {
pub text: Option<String>,
pub html: Option<String>,
pub viewtype: Option<MessageViewtype>,
pub file: Option<String>,
pub location: Option<(f64, f64)>,
pub override_sender_name: Option<String>,
pub quoted_message_id: Option<u32>,
}

View File

@@ -10,7 +10,7 @@ pub mod reactions;
pub mod webxdc;
pub fn color_int_to_hex_string(color: u32) -> String {
format!("{color:#08x}").replace("0x", "#")
format!("{:#08x}", color).replace("0x", "#")
}
fn maybe_empty_string_to_option(string: String) -> Option<String> {

View File

@@ -8,7 +8,7 @@ use typescript_type_def::TypeDef;
pub struct ProviderInfo {
pub before_login_hint: 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 simlicity and because it gets converted into a number anyway, we use an u32 here.
}
impl ProviderInfo {

View File

@@ -32,9 +32,6 @@ pub enum QrObject {
Account {
domain: String,
},
Backup {
ticket: String,
},
WebrtcInstance {
domain: String,
instance_pattern: String,
@@ -129,9 +126,6 @@ impl From<Qr> for QrObject {
}
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
Qr::Account { domain } => QrObject::Account { domain },
Qr::Backup { ticket } => QrObject::Backup {
ticket: ticket.to_string(),
},
Qr::WebrtcInstance {
domain,
instance_pattern,

View File

@@ -37,7 +37,7 @@ mod tests {
let response = r#"{"jsonrpc":"2.0","id":1,"result":1}"#;
session.handle_incoming(request).await;
let result = receiver.next().await;
println!("{result:?}");
println!("{:?}", result);
assert_eq!(result, Some(response.to_owned()));
}
{
@@ -45,7 +45,7 @@ mod tests {
let response = r#"{"jsonrpc":"2.0","id":2,"result":[1]}"#;
session.handle_incoming(request).await;
let result = receiver.next().await;
println!("{result:?}");
println!("{:?}", result);
assert_eq!(result, Some(response.to_owned()));
}

View File

@@ -4,9 +4,3 @@ docs
coverage
yarn*
package-lock.json
.prettierignore
example.html
report_api_coverage.mjs
scripts
dist/example
dist/test

View File

@@ -68,22 +68,22 @@ async function run() {
null
);
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>`);
const messageIds = await client.rpc.getMessageIds(
selectedAccount,
chatId,
false,
false
0
);
const messages = await client.rpc.getMessages(
selectedAccount,
messageIds
);
for (const [_messageId, message] of Object.entries(messages)) {
if (message.variant === "message")
write($main, `<p>${message.text}</p>`);
else write($main, `<p>loading error: ${message.error}</p>`);
write($main, `<p>${message.text}</p>`);
}
}
}

View File

@@ -3,27 +3,24 @@ import { DeltaChat } from "../dist/deltachat.js";
run().catch(console.error);
async function run() {
const delta = new DeltaChat("ws://localhost:20808/ws");
const delta = new DeltaChat('ws://localhost:20808/ws');
delta.on("event", (event) => {
console.log("event", event.data);
});
const email = process.argv[2];
const password = process.argv[3];
if (!email || !password)
throw new Error(
"USAGE: node node-add-account.js <EMAILADDRESS> <PASSWORD>"
);
console.log(`creating account for ${email}`);
const id = await delta.rpc.addAccount();
console.log(`created account id ${id}`);
const email = process.argv[2]
const password = process.argv[3]
if (!email || !password) throw new Error('USAGE: node node-add-account.js <EMAILADDRESS> <PASSWORD>')
console.log(`creating acccount for ${email}`)
const id = await delta.rpc.addAccount()
console.log(`created account id ${id}`)
await delta.rpc.setConfig(id, "addr", email);
await delta.rpc.setConfig(id, "mail_pw", password);
console.log("configuration updated");
await delta.rpc.configure(id);
console.log("account configured!");
console.log('configuration updated')
await delta.rpc.configure(id)
console.log('account configured!')
const accounts = await delta.rpc.getAllAccounts();
console.log("accounts", accounts);
console.log("waiting for events...");
console.log("waiting for events...")
}

View File

@@ -10,5 +10,5 @@ async function run() {
const accounts = await delta.rpc.getAllAccounts();
console.log("accounts", accounts);
console.log("waiting for events...");
console.log("waiting for events...")
}

View File

@@ -3,7 +3,7 @@
"dependencies": {
"@deltachat/tiny-emitter": "3.0.0",
"isomorphic-ws": "^4.0.1",
"yerpc": "^0.4.3"
"yerpc": "^0.3.3"
},
"devDependencies": {
"@types/chai": "^4.2.21",
@@ -14,7 +14,7 @@
"c8": "^7.10.0",
"chai": "^4.3.4",
"chai-as-promised": "^7.1.1",
"esbuild": "^0.17.9",
"esbuild": "^0.14.11",
"http-server": "^14.1.1",
"mocha": "^9.1.1",
"node-fetch": "^2.6.1",
@@ -24,19 +24,12 @@
"typescript": "^4.5.5",
"ws": "^8.5.0"
},
"exports": {
".": {
"import": "./dist/deltachat.js",
"require": "./dist/deltachat.cjs"
}
},
"license": "MPL-2.0",
"main": "dist/deltachat.js",
"name": "@deltachat/jsonrpc-client",
"scripts": {
"build": "run-s generate-bindings extract-constants build:tsc build:bundle build:cjs",
"build": "run-s generate-bindings extract-constants build:tsc build:bundle",
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
"build:cjs": "esbuild --format=cjs --bundle --packages=external dist/deltachat.js --outfile=dist/deltachat.cjs",
"build:tsc": "tsc",
"docs": "typedoc --out docs deltachat.ts",
"example": "run-s build example:build example:start",
@@ -45,8 +38,8 @@
"example:start": "http-server .",
"extract-constants": "node ./scripts/generate-constants.js",
"generate-bindings": "cargo test",
"prettier:check": "prettier --check .",
"prettier:fix": "prettier --write .",
"prettier:check": "prettier --check **.ts",
"prettier:fix": "prettier --write **.ts",
"test": "run-s test:prepare test:run-coverage test:report-coverage",
"test:prepare": "cargo build --package deltachat-rpc-server --bin deltachat-rpc-server",
"test:report-coverage": "node report_api_coverage.mjs",
@@ -55,5 +48,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.111.0"
}
"version": "1.106.0"
}

View File

@@ -1,5 +1,5 @@
import { readFileSync } from "fs";
// only checks for the coverage of the api functions in bindings.ts for now
// only checks for the coverge of the api functions in bindings.ts for now
const generatedFile = "typescript/generated/client.ts";
const json = JSON.parse(readFileSync("./coverage/coverage-final.json"));
const jsonCoverage =

View File

@@ -43,7 +43,7 @@ export class BaseDeltaChat<
const method = request.method;
if (method === "event") {
const event = request.params! as DCWireEvent<Event>;
//@ts-ignore
//@ts-ignore
this.emit(event.event.type, event.contextId, event.event as any);
this.emit("ALL", event.contextId, event.event as any);
@@ -120,7 +120,7 @@ export class StdioTransport extends BaseTransport {
});
}
_send(message: any): void {
_send(message: RPC.Message): void {
const serialized = JSON.stringify(message);
this.input.write(serialized + "\n");
}

View File

@@ -4,7 +4,10 @@ import chaiAsPromised from "chai-as-promised";
chai.use(chaiAsPromised);
import { StdioDeltaChat as DeltaChat } from "../deltachat.js";
import { RpcServerHandle, startServer } from "./test_base.js";
import {
RpcServerHandle,
startServer,
} from "./test_base.js";
describe("basic tests", () => {
let serverHandle: RpcServerHandle;
@@ -12,9 +15,9 @@ describe("basic tests", () => {
before(async () => {
serverHandle = await startServer();
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout)
// dc.on("ALL", (event) => {
//console.log("event", event);
//console.log("event", event);
// });
});
@@ -53,7 +56,7 @@ describe("basic tests", () => {
]);
});
describe("account management", () => {
describe("account managment", () => {
it("should create account", async () => {
const res = await dc.rpc.addAccount();
assert((await dc.rpc.getAllAccountIds()).length === 1);
@@ -73,7 +76,7 @@ describe("basic tests", () => {
});
});
describe("contact management", function () {
describe("contact managment", function () {
let accountId: number;
before(async () => {
accountId = await dc.rpc.addAccount();
@@ -103,44 +106,38 @@ describe("basic tests", () => {
accountId = await dc.rpc.addAccount();
});
it("set and retrieve", async function () {
it("set and retrive", async function () {
await dc.rpc.setConfig(accountId, "addr", "valid@email");
assert((await dc.rpc.getConfig(accountId, "addr")) == "valid@email");
});
it("set invalid key should throw", async function () {
await expect(dc.rpc.setConfig(accountId, "invalid_key", "some value")).to
.be.eventually.rejected;
await expect(dc.rpc.setConfig(accountId, "invalid_key", "some value")).to.be
.eventually.rejected;
});
it("get invalid key should throw", async function () {
await expect(dc.rpc.getConfig(accountId, "invalid_key")).to.be.eventually
.rejected;
});
it("set and retrieve ui.*", async function () {
it("set and retrive ui.*", async function () {
await dc.rpc.setConfig(accountId, "ui.chat_bg", "color:red");
assert((await dc.rpc.getConfig(accountId, "ui.chat_bg")) == "color:red");
});
it("set and retrieve (batch)", async function () {
it("set and retrive (batch)", async function () {
const config = { addr: "valid@email", mail_pw: "1234" };
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(
accountId,
Object.keys(config)
);
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
expect(retrieved).to.deep.equal(config);
});
it("set and retrieve ui.* (batch)", async function () {
it("set and retrive ui.* (batch)", async function () {
const config = {
"ui.chat_bg": "color:green",
"ui.enter_key_sends": "true",
};
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(
accountId,
Object.keys(config)
);
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
expect(retrieved).to.deep.equal(config);
});
it("set and retrieve mixed(ui and core) (batch)", async function () {
it("set and retrive mixed(ui and core) (batch)", async function () {
const config = {
"ui.chat_bg": "color:yellow",
"ui.enter_key_sends": "false",
@@ -148,10 +145,7 @@ describe("basic tests", () => {
mail_pw: "123456",
};
await dc.rpc.batchSetConfig(accountId, config);
const retrieved = await dc.rpc.batchGetConfig(
accountId,
Object.keys(config)
);
const retrieved = await dc.rpc.batchGetConfig(accountId, Object.keys(config));
expect(retrieved).to.deep.equal(config);
});
});

View File

@@ -22,7 +22,7 @@ describe("online tests", function () {
process.exit(1);
}
console.log(
"Missing DCC_NEW_TMP_EMAIL environment variable!, skip integration tests"
"Missing DCC_NEW_TMP_EMAIL environment variable!, skip intergration tests"
);
this.skip();
}
@@ -36,7 +36,7 @@ describe("online tests", function () {
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
if (!account1 || !account1.email || !account1.password) {
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 intergration tests"
);
this.skip();
}
@@ -44,7 +44,7 @@ describe("online tests", function () {
account2 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
if (!account2 || !account2.email || !account2.password) {
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 intergration tests"
);
this.skip();
}
@@ -74,7 +74,7 @@ describe("online tests", function () {
accountsConfigured = true;
});
it("send and receive text message", async function () {
it("send and recieve text message", async function () {
if (!accountsConfigured) {
this.skip();
}
@@ -97,8 +97,7 @@ describe("online tests", function () {
const messageList = await dc.rpc.getMessageIds(
accountId2,
chatIdOnAccountB,
false,
false
0
);
expect(messageList).have.length(1);
@@ -106,7 +105,7 @@ describe("online tests", function () {
expect(message.text).equal("Hello");
});
it("send and receive text message roundtrip, encrypted on answer onwards", async function () {
it("send and recieve text message roundtrip, encrypted on answer onwards", async function () {
if (!accountsConfigured) {
this.skip();
}
@@ -134,8 +133,7 @@ describe("online tests", function () {
const messageList = await dc.rpc.getMessageIds(
accountId2,
chatIdOnAccountB,
false,
false
0
);
const message = await dc.rpc.getMessage(
accountId2,
@@ -152,7 +150,7 @@ describe("online tests", function () {
await eventPromise2;
const messageId = (
await dc.rpc.getMessageIds(accountId1, chatId, false, false)
await dc.rpc.getMessageIds(accountId1, chatId, 0)
).reverse()[0];
const message2 = await dc.rpc.getMessage(accountId1, messageId);
expect(message2.text).equal("super secret message");

View File

@@ -59,13 +59,13 @@ export async function startServer(): Promise<RpcServerHandle> {
export async function createTempUser(url: string) {
const response = await fetch(url, {
method: "POST",
method: "POST",
headers: {
"cache-control": "no-cache",
},
});
if (!response.ok) throw new Error("Received invalid response");
return response.json();
if (!response.ok) throw new Error('Received invalid response')
return response.json();
}
function getTargetDir(): Promise<string> {
@@ -89,3 +89,4 @@ function getTargetDir(): Promise<string> {
);
});
}

View File

@@ -1,8 +0,0 @@
[package]
name = "ratelimit"
version = "1.0.0"
description = "Token bucket implementation"
edition = "2021"
license = "MPL-2.0"
[dependencies]

View File

@@ -1,20 +0,0 @@
[package]
name = "deltachat-repl"
version = "1.111.0"
license = "MPL-2.0"
edition = "2021"
[dependencies]
ansi_term = "0.12.1"
anyhow = "1"
deltachat = { path = "..", features = ["internals"]}
dirs = "4"
log = "0.4.16"
pretty_env_logger = "0.4"
rusqlite = "0.28"
rustyline = "11"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
[features]
default = ["vendored"]
vendored = ["deltachat/vendored"]

View File

@@ -64,8 +64,7 @@ async def main():
bot = Bot(account, hooks)
if not await bot.is_configured():
# Save a reference to avoid garbage collection of the task.
_configure_task = asyncio.create_task(bot.configure(email=sys.argv[1], password=sys.argv[2]))
asyncio.create_task(bot.configure(email=sys.argv[1], password=sys.argv[2]))
await bot.run_forever()

View File

@@ -32,7 +32,7 @@ async def main():
async def process_messages():
for message in await account.get_fresh_messages_in_arrival_order():
snapshot = await message.get_snapshot()
if not snapshot.is_bot and not snapshot.is_info:
if not snapshot.is_info:
await snapshot.chat.send_text(snapshot.text)
await snapshot.message.mark_seen()

View File

@@ -13,6 +13,13 @@ dynamic = [
"version"
]
[tool.setuptools]
# We declare the package not-zip-safe so that our type hints are also available
# when checking client code that uses our (installed) package.
# Ref:
# https://mypy.readthedocs.io/en/stable/installed_packages.html?highlight=zip#using-installed-packages-with-mypy-pep-561
zip-safe = false
[tool.setuptools.package-data]
deltachat_rpc_client = [
"py.typed"
@@ -25,35 +32,7 @@ deltachat_rpc_client = [
line-length = 120
[tool.ruff]
select = [
"E", "W", # pycodestyle
"F", # Pyflakes
"N", # pep8-naming
"I", # isort
"ARG", # flake8-unused-arguments
"B", # flake8-bugbear
"C4", # flake8-comprehensions
"COM", # flake8-commas
"DTZ", # flake8-datetimez
"ICN", # flake8-import-conventions
"ISC", # flake8-implicit-str-concat
"PIE", # flake8-pie
"PT", # flake8-pytest-style
"RET", # flake8-return
"SIM", # flake8-simplify
"TCH", # flake8-type-checking
"TID", # flake8-tidy-imports
"YTT", # flake8-2020
"ERA", # eradicate
"PLC", # Pylint Convention
"PLE", # Pylint Error
"PLW", # Pylint Warning
"RUF006" # asyncio-dangling-task
]
select = ["E", "F", "W", "N", "YTT", "B", "C4", "ISC", "ICN", "PT", "RET", "SIM", "TID", "ARG", "DTZ", "ERA", "PLC", "PLE", "PLW", "PIE", "COM"]
line-length = 120
[tool.isort]

View File

@@ -27,7 +27,7 @@ def _to_attrdict(obj):
class AttrDict(dict):
"""Dictionary that allows accessing values using the "dot notation" as attributes."""
"""Dictionary that allows accessing values usin the "dot notation" as attributes."""
def __init__(self, *args, **kwargs) -> None:
super().__init__({_camel_to_snake(key): _to_attrdict(value) for key, value in dict(*args, **kwargs).items()})
@@ -104,8 +104,7 @@ async def _run_cli(
if not await client.is_configured():
assert args.email, "Account is not configured and email must be provided"
assert args.password, "Account is not configured and password must be provided"
# Save a reference to avoid garbage collection of the task.
_configure_task = asyncio.create_task(client.configure(email=args.email, password=args.password))
asyncio.create_task(client.configure(email=args.email, password=args.password))
await client.run_forever()

View File

@@ -1,4 +1,3 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
from ._utils import AttrDict
@@ -6,23 +5,34 @@ from .chat import Chat
from .const import ChatlistFlag, ContactFlag, SpecialContactId
from .contact import Contact
from .message import Message
from .rpc import Rpc
if TYPE_CHECKING:
from .deltachat import DeltaChat
from .rpc import Rpc
@dataclass
class Account:
"""Delta Chat account."""
manager: "DeltaChat"
id: int
def __init__(self, manager: "DeltaChat", account_id: int) -> None:
self.manager = manager
self.id = account_id
@property
def _rpc(self) -> "Rpc":
def _rpc(self) -> Rpc:
return self.manager.rpc
def __eq__(self, other) -> bool:
if not isinstance(other, Account):
return False
return self.id == other.id and self.manager == other.manager
def __ne__(self, other) -> bool:
return not self == other
def __repr__(self) -> str:
return f"<Account id={self.id}>"
async def wait_for_event(self) -> AttrDict:
"""Wait until the next event and return it."""
return AttrDict(await self._rpc.wait_for_event(self.id))
@@ -158,7 +168,7 @@ class Account:
:param contact: if a contact is specified only chats including this contact are returned.
:param archived_only: if True only archived chats are returned.
:param for_forwarding: if True the chat list is sorted with "Saved messages" at the top
and without "Device chat" and contact requests.
and withot "Device chat" and contact requests.
:param no_specials: if True archive link is not added to the list.
:param alldone_hint: if True the "all done hint" special chat will be added to the list
as needed.

View File

@@ -1,30 +1,39 @@
import calendar
from dataclasses import dataclass
from datetime import datetime
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
from ._utils import AttrDict
from .const import ChatVisibility, ViewType
from .const import ChatVisibility
from .contact import Contact
from .message import Message
from .rpc import Rpc
if TYPE_CHECKING:
from datetime import datetime
from .account import Account
from .rpc import Rpc
@dataclass
class Chat:
"""Chat object which manages members and through which you can send and retrieve messages."""
account: "Account"
id: int
def __init__(self, account: "Account", chat_id: int) -> None:
self.account = account
self.id = chat_id
@property
def _rpc(self) -> "Rpc":
def _rpc(self) -> Rpc:
return self.account._rpc
def __eq__(self, other) -> bool:
if not isinstance(other, Chat):
return False
return self.id == other.id and self.account == other.account
def __ne__(self, other) -> bool:
return not self == other
def __repr__(self) -> str:
return f"<Chat id={self.id} account={self.account.id}>"
async def delete(self) -> None:
"""Delete this chat and all its messages.
@@ -108,27 +117,15 @@ class Chat:
async def send_message(
self,
text: Optional[str] = None,
html: Optional[str] = None,
viewtype: Optional[ViewType] = None,
file: Optional[str] = None,
location: Optional[Tuple[float, float]] = None,
override_sender_name: Optional[str] = None,
quoted_msg: Optional[Union[int, Message]] = None,
) -> Message:
"""Send a message and return the resulting Message instance."""
if isinstance(quoted_msg, Message):
quoted_msg = quoted_msg.id
draft = {
"text": text,
"html": html,
"viewtype": viewtype,
"file": file,
"location": location,
"overrideSenderName": override_sender_name,
"quotedMsg": quoted_msg,
}
msg_id = await self._rpc.send_msg(self.account.id, self.id, draft)
msg_id, _ = await self._rpc.misc_send_msg(self.account.id, self.id, text, file, location, quoted_msg)
return Message(self.account, msg_id)
async def send_text(self, text: str) -> Message:
@@ -177,9 +174,9 @@ class Chat:
snapshot["message"] = Message(self.account, snapshot.id)
return snapshot
async def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
async def get_messages(self, flags: int = 0) -> List[Message]:
"""get the list of messages in this chat."""
msgs = await 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, flags)
return [Message(self.account, msg_id) for msg_id in msgs]
async def get_fresh_message_count(self) -> int:
@@ -194,23 +191,19 @@ class Chat:
"""Add contacts to this group."""
for cnt in contact:
if isinstance(cnt, str):
contact_id = (await self.account.create_contact(cnt)).id
cnt = (await self.account.create_contact(cnt)).id
elif not isinstance(cnt, int):
contact_id = cnt.id
else:
contact_id = cnt
await self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
cnt = cnt.id
await self._rpc.add_contact_to_chat(self.account.id, self.id, cnt)
async def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
"""Remove members from this group."""
for cnt in contact:
if isinstance(cnt, str):
contact_id = (await self.account.create_contact(cnt)).id
cnt = (await self.account.create_contact(cnt)).id
elif not isinstance(cnt, int):
contact_id = cnt.id
else:
contact_id = cnt
await self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
cnt = cnt.id
await self._rpc.remove_contact_from_chat(self.account.id, self.id, cnt)
async def get_contacts(self) -> List[Contact]:
"""Get the contacts belonging to this chat.
@@ -234,8 +227,8 @@ class Chat:
async def get_locations(
self,
contact: Optional[Contact] = None,
timestamp_from: Optional["datetime"] = None,
timestamp_to: Optional["datetime"] = None,
timestamp_from: Optional[datetime] = None,
timestamp_to: Optional[datetime] = None,
) -> List[AttrDict]:
"""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
@@ -246,9 +239,9 @@ class Chat:
locations = []
contacts: Dict[int, Contact] = {}
for loc in result:
location = AttrDict(loc)
location["chat"] = self
location["contact"] = contacts.setdefault(location.contact_id, Contact(self.account, location.contact_id))
location["message"] = Message(self.account, location.msg_id)
locations.append(location)
loc = AttrDict(loc)
loc["chat"] = self
loc["contact"] = contacts.setdefault(loc.contact_id, Contact(self.account, loc.contact_id))
loc["message"] = Message(self.account, loc.msg_id)
locations.append(loc)
return locations

View File

@@ -2,7 +2,6 @@
import inspect
import logging
from typing import (
TYPE_CHECKING,
Callable,
Coroutine,
Dict,
@@ -14,6 +13,8 @@ from typing import (
Union,
)
from deltachat_rpc_client.account import Account
from ._utils import (
AttrDict,
parse_system_add_remove,
@@ -30,16 +31,13 @@ from .events import (
RawEvent,
)
if TYPE_CHECKING:
from deltachat_rpc_client.account import Account
class Client:
"""Simple Delta Chat client that listen to events of a single account."""
def __init__(
self,
account: "Account",
account: Account,
hooks: Optional[Iterable[Tuple[Callable, Union[type, EventFilter]]]] = None,
logger: Optional[logging.Logger] = None,
) -> None:

View File

@@ -1,15 +1,13 @@
from dataclasses import dataclass
from typing import TYPE_CHECKING
from ._utils import AttrDict
from .rpc import Rpc
if TYPE_CHECKING:
from .account import Account
from .chat import Chat
from .rpc import Rpc
@dataclass
class Contact:
"""
Contact API.
@@ -17,11 +15,23 @@ class Contact:
Essentially a wrapper for RPC, account ID and a contact ID.
"""
account: "Account"
id: int
def __init__(self, account: "Account", contact_id: int) -> None:
self.account = account
self.id = contact_id
def __eq__(self, other) -> bool:
if not isinstance(other, Contact):
return False
return self.id == other.id and self.account == other.account
def __ne__(self, other) -> bool:
return not self == other
def __repr__(self) -> str:
return f"<Contact id={self.id} account={self.account.id}>"
@property
def _rpc(self) -> "Rpc":
def _rpc(self) -> Rpc:
return self.account._rpc
async def block(self) -> None:

View File

@@ -1,10 +1,8 @@
from typing import TYPE_CHECKING, Dict, List
from typing import Dict, List
from ._utils import AttrDict
from .account import Account
if TYPE_CHECKING:
from .rpc import Rpc
from .rpc import Rpc
class DeltaChat:
@@ -13,7 +11,7 @@ class DeltaChat:
This is the root of the object oriented API.
"""
def __init__(self, rpc: "Rpc") -> None:
def __init__(self, rpc: Rpc) -> None:
self.rpc = rpc
async def add_account(self) -> Account:

View File

@@ -2,13 +2,11 @@
import inspect
import re
from abc import ABC, abstractmethod
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
from typing import Callable, Iterable, Iterator, Optional, Set, Tuple, Union
from ._utils import AttrDict
from .const import EventType
if TYPE_CHECKING:
from ._utils import AttrDict
def _tuple_of(obj, type_: type) -> tuple:
if not obj:
@@ -82,7 +80,7 @@ class RawEvent(EventFilter):
return (self.types, self.func) == (other.types, other.func)
return False
async def filter(self, event: "AttrDict") -> bool:
async def filter(self, event: AttrDict) -> bool:
if self.types and event.type not in self.types:
return False
return await self._call_func(event)
@@ -98,9 +96,6 @@ class NewMessage(EventFilter):
content.
:param command: If set, only match messages with the given command (ex. /help).
Setting this property implies `is_info==False`.
:param is_bot: If set to True only match messages sent by bots, if set to None
match messages from bots and users. If omitted or set to False
only messages from users will be matched.
:param is_info: If set to True only match info/system messages, if set to False
only match messages that are not info/system messages. If omitted
info/system messages as well as normal messages will be matched.
@@ -118,12 +113,10 @@ class NewMessage(EventFilter):
re.Pattern,
] = None,
command: Optional[str] = None,
is_bot: Optional[bool] = False,
is_info: Optional[bool] = None,
func: Optional[Callable[["AttrDict"], bool]] = None,
func: Optional[Callable[[AttrDict], bool]] = None,
) -> None:
super().__init__(func=func)
self.is_bot = is_bot
self.is_info = is_info
if command is not None and not isinstance(command, str):
raise TypeError("Invalid command")
@@ -140,28 +133,19 @@ class NewMessage(EventFilter):
raise TypeError("Invalid pattern type")
def __hash__(self) -> int:
return hash((self.pattern, self.command, self.is_bot, self.is_info, self.func))
return hash((self.pattern, self.func))
def __eq__(self, other) -> bool:
if isinstance(other, NewMessage):
return (
self.pattern,
self.command,
self.is_bot,
self.is_info,
self.func,
) == (
return (self.pattern, self.command, self.is_info, self.func) == (
other.pattern,
other.command,
other.is_bot,
other.is_info,
other.func,
)
return False
async def filter(self, event: "AttrDict") -> bool:
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
return False
async def filter(self, event: AttrDict) -> bool:
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
return False
if self.command and self.command != event.command:
@@ -201,7 +185,7 @@ class MemberListChanged(EventFilter):
return (self.added, self.func) == (other.added, other.func)
return False
async def filter(self, event: "AttrDict") -> bool:
async def filter(self, event: AttrDict) -> bool:
if self.added is not None and self.added != event.member_added:
return False
return await self._call_func(event)
@@ -233,7 +217,7 @@ class GroupImageChanged(EventFilter):
return (self.deleted, self.func) == (other.deleted, other.func)
return False
async def filter(self, event: "AttrDict") -> bool:
async def filter(self, event: AttrDict) -> bool:
if self.deleted is not None and self.deleted != event.image_deleted:
return False
return await self._call_func(event)
@@ -258,7 +242,7 @@ class GroupNameChanged(EventFilter):
return self.func == other.func
return False
async def filter(self, event: "AttrDict") -> bool:
async def filter(self, event: AttrDict) -> bool:
return await self._call_func(event)

View File

@@ -1,24 +1,34 @@
import json
from dataclasses import dataclass
from typing import TYPE_CHECKING, Union
from ._utils import AttrDict
from .contact import Contact
from .rpc import Rpc
if TYPE_CHECKING:
from .account import Account
from .rpc import Rpc
@dataclass
class Message:
"""Delta Chat Message object."""
account: "Account"
id: int
def __init__(self, account: "Account", msg_id: int) -> None:
self.account = account
self.id = msg_id
def __eq__(self, other) -> bool:
if not isinstance(other, Message):
return False
return self.id == other.id and self.account == other.account
def __ne__(self, other) -> bool:
return not self == other
def __repr__(self) -> str:
return f"<Message id={self.id} account={self.account.id}>"
@property
def _rpc(self) -> "Rpc":
def _rpc(self) -> Rpc:
return self.account._rpc
async def send_reaction(self, *reaction: str):

View File

@@ -1,4 +1,3 @@
import asyncio
import json
import os
from typing import AsyncGenerator, List, Optional
@@ -52,13 +51,11 @@ class ACFactory:
await bot.configure(credentials["email"], credentials["password"])
return bot
async def get_online_account(self) -> Account:
account = await self.new_configured_account()
await account.start_io()
return account
async def get_online_accounts(self, num: int) -> List[Account]:
return await asyncio.gather(*[self.get_online_account() for _ in range(num)])
accounts = [await self.new_configured_account() for _ in range(num)]
for account in accounts:
await account.start_io()
return accounts
async def send_message(
self,

View File

@@ -1,7 +1,7 @@
import asyncio
from unittest.mock import MagicMock
import pytest
from deltachat_rpc_client import EventType, events
from deltachat_rpc_client.rpc import JsonRpcError
@@ -13,17 +13,6 @@ async def test_system_info(rpc) -> None:
assert "deltachat_core_version" in system_info
@pytest.mark.asyncio()
async def test_sleep(rpc) -> None:
"""Test that long-running task does not block short-running task from completion."""
sleep_5_task = asyncio.create_task(rpc.sleep(5.0))
sleep_3_task = asyncio.create_task(rpc.sleep(3.0))
done, pending = await asyncio.wait([sleep_5_task, sleep_3_task], return_when=asyncio.FIRST_COMPLETED)
assert sleep_3_task in done
assert sleep_5_task in pending
sleep_5_task.cancel()
@pytest.mark.asyncio()
async def test_email_address_validity(rpc) -> None:
valid_addresses = [
@@ -58,7 +47,6 @@ async def test_configure_starttls(acfactory) -> None:
# Use STARTTLS
await account.set_config("mail_security", "2")
await account.set_config("send_security", "2")
await account.configure()
assert await account.is_configured()
@@ -227,7 +215,6 @@ async def test_message(acfactory) -> None:
snapshot = await message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
assert not snapshot.is_bot
assert repr(message)
with pytest.raises(JsonRpcError): # chat is not accepted
@@ -239,46 +226,18 @@ async def test_message(acfactory) -> None:
await message.send_reaction("😎")
@pytest.mark.asyncio()
async def test_is_bot(acfactory) -> None:
"""Test that we can recognize messages submitted by bots."""
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat()
# Alice becomes a bot.
await alice.set_config("bot", "1")
await alice_chat_bob.send_text("Hello!")
while True:
event = await bob.wait_for_event()
if event.type == EventType.INCOMING_MSG:
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = await message.get_snapshot()
assert snapshot.chat_id == event.chat_id
assert snapshot.text == "Hello!"
assert snapshot.is_bot
break
@pytest.mark.asyncio()
async def test_bot(acfactory) -> None:
mock = MagicMock()
user = (await acfactory.get_online_accounts(1))[0]
bot = await acfactory.new_configured_bot()
bot2 = await acfactory.new_configured_bot()
assert await bot.is_configured()
assert await bot.account.get_config("bot") == "1"
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
hook = lambda e: mock.hook(e.msg_id), events.RawEvent(EventType.INCOMING_MSG)
bot.add_hook(*hook)
event = await acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
snapshot = await bot.account.get_message_by_id(event.msg_id).get_snapshot()
assert not snapshot.is_bot
mock.hook.assert_called_once_with(event.msg_id)
bot.remove_hook(*hook)
@@ -293,8 +252,6 @@ async def test_bot(acfactory) -> None:
mock.hook.assert_called_with(event.msg_id)
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!")
mock.hook.assert_called_with(event.msg_id)
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
await acfactory.process_message(from_account=user, to_client=bot, text="hey!")
assert len(mock.hook.mock_calls) == 2
bot.remove_hook(*hook)

View File

@@ -1,4 +1,5 @@
import pytest
from deltachat_rpc_client import EventType

View File

@@ -6,7 +6,7 @@ envlist =
[testenv]
commands =
pytest --exitfirst {posargs}
pytest {posargs}
setenv =
# Avoid stack overflow when Rust core is built without optimizations.
RUST_MIN_STACK=8388608
@@ -25,5 +25,5 @@ deps =
ruff
black
commands =
black --quiet --check --diff src/ examples/ tests/
black --check src/ examples/ tests/
ruff src/ examples/ tests/

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.111.0"
version = "1.106.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -13,7 +13,7 @@ categories = ["cryptography", "std", "email"]
name = "deltachat-rpc-server"
[dependencies]
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", default-features = false }
deltachat-jsonrpc = { path = "../deltachat-jsonrpc" }
anyhow = "1"
env_logger = { version = "0.10.0" }
@@ -21,9 +21,5 @@ futures-lite = "1.12.0"
log = "0.4"
serde_json = "1.0.91"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.25.0", features = ["io-std"] }
yerpc = { version = "0.4.0", features = ["anyhow_expose"] }
[features]
default = ["vendored"]
vendored = ["deltachat-jsonrpc/vendored"]
tokio = { version = "1.23.1", features = ["io-std"] }
yerpc = { version = "0.3.1", features = ["anyhow_expose"] }

View File

@@ -1,34 +0,0 @@
# Delta Chat RPC server
This program provides a [JSON-RPC 2.0](https://www.jsonrpc.org/specification) interface to DeltaChat
over standard I/O.
## Install
To download binary pre-builds check the [releases page](https://github.com/deltachat/deltachat-core-rust/releases).
Rename the downloaded binary to `deltachat-rpc-server` and add it to your `PATH`.
To install from source run:
```sh
cargo install --git https://github.com/deltachat/deltachat-core-rust/ deltachat-rpc-server
```
The `deltachat-rpc-server` executable will be installed into `$HOME/.cargo/bin` that should be available
in your `PATH`.
## Usage
To use just run `deltachat-rpc-server` command. The accounts folder will be created in the current
working directory unless `DC_ACCOUNTS_PATH` is set:
```sh
export DC_ACCOUNTS_PATH=$HOME/delta/
deltachat-rpc-server
```
The common use case for this program is to create bindings to use Delta Chat core from programming
languages other than Rust, for example:
1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/

View File

@@ -40,7 +40,7 @@ async fn main() -> Result<()> {
while let Some(message) = out_receiver.next().await {
let message = serde_json::to_string(&message)?;
log::trace!("RPC send {}", message);
println!("{message}");
println!("{}", message);
}
Ok(())
});
@@ -51,10 +51,7 @@ async fn main() -> Result<()> {
let mut lines = BufReader::new(stdin).lines();
while let Some(message) = lines.next_line().await? {
log::trace!("RPC recv {}", message);
let session = session.clone();
tokio::spawn(async move {
session.handle_incoming(&message).await;
});
session.handle_incoming(&message).await;
}
log::info!("EOF reached on stdin");
Ok(())

View File

@@ -5,7 +5,7 @@ use quote::quote;
use crate::proc_macro::TokenStream;
// For now, assume (not check) that these macros are applied to enum without
// For now, assume (not check) that these macroses are applied to enum without
// data. If this assumption is violated, compiler error will point to
// generated code, which is not very user-friendly.

View File

@@ -1,84 +0,0 @@
[advisories]
unmaintained = "allow"
ignore = [
"RUSTSEC-2020-0071",
# Only affects windows if using non-default allocator (and unmaintained).
"RUSTSEC-2021-0145",
]
[bans]
# Accept some duplicate versions, ideally we work towards this list
# becoming empty. Adding versions forces us to revisit this at least
# when upgrading.
skip = [
{ name = "base64", version = "<0.21" },
{ name = "block-buffer", version = "<0.10" },
{ name = "darling", version = "<0.14" },
{ name = "darling_core", version = "<0.14" },
{ name = "darling_macro", version = "<0.14" },
{ name = "digest", version = "<0.10" },
{ name = "env_logger", version = "<0.10" },
{ name = "getrandom", version = "<0.2" },
{ name = "hermit-abi", version = "<0.3" },
{ name = "humantime", version = "<2.1" },
{ name = "idna", version = "<0.3" },
{ name = "nom", version = "<7.1" },
{ name = "quick-error", version = "<2.0" },
{ name = "rand", version = "<0.8" },
{ name = "rand_chacha", version = "<0.3" },
{ name = "rand_core", version = "<0.6" },
{ name = "sha2", version = "<0.10" },
{ name = "time", version = "<0.3" },
{ name = "version_check", version = "<0.9" },
{ name = "wasi", version = "<0.11" },
{ name = "windows-sys", version = "<0.45" },
{ name = "windows_x86_64_msvc", version = "<0.42" },
{ name = "windows_x86_64_gnu", version = "<0.42" },
{ name = "windows_i686_msvc", version = "<0.42" },
{ name = "windows_i686_gnu", version = "<0.42" },
{ name = "windows_aarch64_msvc", version = "<0.42" },
{ name = "unicode-xid", version = "<0.2.4" },
{ name = "syn", version = "<1.0" },
{ name = "quote", version = "<1.0" },
{ name = "proc-macro2", version = "<1.0" },
{ name = "portable-atomic", version = "<1.0" },
{ name = "spin", version = "<0.9.6" },
{ name = "convert_case", version = "0.4.0" },
{ name = "clap_lex", version = "0.2.4" },
{ name = "clap", version = "3.2.23" },
]
[licenses]
allow = [
"0BSD",
"Apache-2.0",
"BSD-2-Clause",
"BSD-3-Clause",
"BSL-1.0", # Boost Software License 1.0
"CC0-1.0",
"ISC",
"MIT",
"MPL-2.0",
"OpenSSL",
"Unicode-DFS-2016",
"Zlib",
]
[[licenses.clarify]]
name = "ring"
expression = "MIT AND ISC AND OpenSSL"
license-files = [
{ path = "LICENSE", hash = 0xbd0eed23 },
]
[sources.allow-org]
# Organisations which we allow git sources from.
github = [
"async-email",
"deltachat",
"n0-computer",
"quinn-rs",
"dignifiedquire",
]

View File

@@ -66,13 +66,13 @@ Note that usually a mail is signed by a key that has a UID matching the from add
#### Upsides:
- With this approach, it's easy to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
- Faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.
- Faster transation: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.
### Alternatives and old discussions/plans:
- Change the contact instead of rewriting the group member lists. This seems to call for more trouble since we will end up with multiple contacts having the same email address.
- If needed, we could add a header a) indicating that the sender did an address transition or b) listing all the secondary (old) addresses. For now, there is no big enough benefit to warrant introducing another header and its processing on the receiver side (including all the necessary checks and handling of error cases). Instead, we only check for the `Chat-Version` header to prevent accidental transitions when an MUA user sends a message from another email address with the same key.
- If needed, we could add a header a) indicating that the sender did an address transition or b) listing all the secondary (old) addresses. For now, there is no big enough benefit to warrant introducing another header and its processing on the receiver side (including all the neccessary checks and handling of error cases). Instead, we only check for the `Chat-Version` header to prevent accidental transitions when an MUA user sends a message from another email address with the same key.
- The condition for a transition temporarily was:
@@ -107,7 +107,7 @@ The most obvious alternative would be to create a new contact with the new addre
#### Upsides:
- With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
- (Also, less important: Slightly faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
- (Also, less important: Slightly faster transation: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't wast that much development time.)
[full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161)
@@ -124,4 +124,4 @@ Other
Notes during implementing
========================
- As far as I understand the code, unencrypted messages are unsigned. So, the transition only works if both sides have the other side's key.
- As far as I understand the code, unencrypted messages are unsigned. So, the transition only works if both sides have the other side's key.

View File

@@ -62,5 +62,5 @@ Notes/Questions:
and design such that we can cover all imap flags.
- It might not be necessary to keep needs_send_mdn state in this table
- It might not be neccessary to keep needs_send_mdn state in this table
if this can be decided rather when we succeed with mark_seen/mark_delete.

View File

@@ -29,13 +29,13 @@ use tokio::fs;
/// Reset database tables.
/// Argument is a bitmask, executing single or multiple actions in one call.
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
/// e.g. bitmask 7 triggers actions definded with bits 1, 2 and 4.
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;", ())
.execute("DELETE FROM jobs;", paramsv![])
.await
.unwrap();
println!("(1) Jobs reset.");
@@ -43,7 +43,7 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 2 {
context
.sql()
.execute("DELETE FROM acpeerstates;", ())
.execute("DELETE FROM acpeerstates;", paramsv![])
.await
.unwrap();
println!("(2) Peerstates reset.");
@@ -51,7 +51,7 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 4 {
context
.sql()
.execute("DELETE FROM keypairs;", ())
.execute("DELETE FROM keypairs;", paramsv![])
.await
.unwrap();
println!("(4) Private keypairs reset.");
@@ -59,36 +59,36 @@ async fn reset_tables(context: &Context, bits: i32) {
if 0 != bits & 8 {
context
.sql()
.execute("DELETE FROM contacts WHERE id>9;", ())
.execute("DELETE FROM contacts WHERE id>9;", paramsv![])
.await
.unwrap();
context
.sql()
.execute("DELETE FROM chats WHERE id>9;", ())
.execute("DELETE FROM chats WHERE id>9;", paramsv![])
.await
.unwrap();
context
.sql()
.execute("DELETE FROM chats_contacts;", ())
.execute("DELETE FROM chats_contacts;", paramsv![])
.await
.unwrap();
context
.sql()
.execute("DELETE FROM msgs WHERE id>9;", ())
.execute("DELETE FROM msgs WHERE id>9;", paramsv![])
.await
.unwrap();
context
.sql()
.execute(
"DELETE FROM config WHERE keyname LIKE 'imap.%' OR keyname LIKE 'configured%';",
(),
paramsv![],
)
.await
.unwrap();
context.sql().config_cache().write().await.clear();
context
.sql()
.execute("DELETE FROM leftgrps;", ())
.execute("DELETE FROM leftgrps;", paramsv![])
.await
.unwrap();
println!("(8) Rest but server config reset.");
@@ -101,7 +101,7 @@ async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<
let data = read_file(context, filename).await?;
if let Err(err) = receive_imf(context, &data, false).await {
println!("receive_imf errored: {err:?}");
println!("receive_imf errored: {:?}", err);
}
Ok(())
}
@@ -148,7 +148,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
let name = name_f.to_string_lossy();
if name.ends_with(".eml") {
let path_plus_name = format!("{}/{}", &real_spec, name);
println!("Import: {path_plus_name}");
println!("Import: {}", path_plus_name);
if poke_eml_file(context, path_plus_name).await.is_ok() {
read_cnt += 1
}
@@ -168,7 +168,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
.await
.expect("invalid contact");
let contact_name = if let Some(name) = msg.get_override_sender_name() {
format!("~{name}")
format!("~{}", name)
} else {
contact.get_display_name().to_string()
};
@@ -223,7 +223,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
"[WEBXDC: {}, icon={}, document={}, summary={}, source_code_url={}]",
info.name, info.icon, info.document, info.summary, info.source_code_url
),
Err(err) => format!("[get_webxdc_info() failed: {err}]"),
Err(err) => format!("[get_webxdc_info() failed: {}]", err),
}
} else {
"".to_string()
@@ -336,8 +336,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
has-backup\n\
export-backup\n\
import-backup <backup-file>\n\
send-backup\n\
receive-backup <qr>\n\
export-keys\n\
import-keys\n\
export-setup\n\
@@ -437,9 +435,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
),
},
"initiate-key-transfer" => match initiate_key_transfer(&context).await {
Ok(setup_code) => {
println!("Setup code for the transferred setup message: {setup_code}",)
}
Ok(setup_code) => println!(
"Setup code for the transferred setup message: {}",
setup_code,
),
Err(err) => bail!("Failed to generate setup code: {}", err),
},
"get-setupcodebegin" => {
@@ -488,17 +487,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
)
.await?;
}
"send-backup" => {
let provider = BackupProvider::prepare(&context).await?;
let qr = provider.qr();
println!("QR code: {}", format_backup(&qr)?);
provider.await?;
}
"receive-backup" => {
ensure!(!arg1.is_empty(), "Argument <qr> is missing.");
let qr = check_qr(&context, arg1).await?;
deltachat::imex::get_backup(&context, qr).await?;
}
"export-keys" => {
let dir = dirs::home_dir().unwrap_or_default();
imex(&context, ImexMode::ExportSelfKeys, dir.as_ref(), None).await?;
@@ -540,7 +528,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(!arg1.is_empty(), "Argument <key> missing.");
let key = config::Config::from_str(arg1)?;
let val = context.get_config(key).await;
println!("{key}={val:?}");
println!("{}={:?}", key, val);
}
"info" => {
println!("{:#?}", context.get_info().await);
@@ -552,7 +540,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
match context.get_connectivity_html().await {
Ok(html) => {
fs::write(&file, html).await?;
println!("Report written to: {file:#?}");
println!("Report written to: {:#?}", file);
}
Err(err) => {
bail!("Failed to get connectivity html: {}", err);
@@ -625,7 +613,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"{}{}{} [{}]{}",
summary
.prefix
.map_or_else(String::new, |prefix| format!("{prefix}: ")),
.map_or_else(String::new, |prefix| format!("{}: ", prefix)),
summary.text,
statestr,
&timestr,
@@ -643,8 +631,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
if location::is_sending_locations_to_chat(&context, None).await? {
println!("Location streaming enabled.");
}
println!("{cnt} chats");
println!("{time_needed:?} to create this list");
println!("{} chats", cnt);
println!("{:?} to create this list", time_needed);
}
"chat" => {
if sel_chat.is_none() && arg1.is_empty() {
@@ -652,7 +640,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
if !arg1.is_empty() {
let id = ChatId::new(arg1.parse()?);
println!("Selecting chat {id}");
println!("Selecting chat {}", id);
sel_chat = Some(Chat::load_from_db(&context, id).await?);
*chat_id = id;
}
@@ -661,15 +649,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let sel_chat = sel_chat.as_ref().unwrap();
let time_start = std::time::SystemTime::now();
let msglist = chat::get_chat_msgs_ex(
&context,
sel_chat.get_id(),
chat::MessageListOptions {
info_only: false,
add_daymarker: true,
},
)
.await?;
let msglist =
chat::get_chat_msgs(&context, sel_chat.get_id(), DC_GCM_ADDDAYMARKER).await?;
let time_needed = time_start.elapsed().unwrap_or_default();
let msglist: Vec<MsgId> = msglist
@@ -705,7 +686,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
},
match sel_chat.get_profile_image(&context).await? {
Some(icon) => match icon.to_str() {
Some(icon) => format!(" Icon: {icon}"),
Some(icon) => format!(" Icon: {}", icon),
_ => " Icon: Err".to_string(),
},
_ => "".to_string(),
@@ -731,7 +712,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
println!(
"{time_needed:?} to create this list, {time_noticed_needed:?} to mark all messages as noticed."
"{:?} to create this list, {:?} to mark all messages as noticed.",
time_needed, time_noticed_needed
);
}
"createchat" => {
@@ -739,26 +721,26 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let contact_id = ContactId::new(arg1.parse()?);
let chat_id = ChatId::create_for_contact(&context, contact_id).await?;
println!("Single#{chat_id} created successfully.",);
println!("Single#{} created successfully.", chat_id,);
}
"creategroup" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =
chat::create_group_chat(&context, ProtectionStatus::Unprotected, arg1).await?;
println!("Group#{chat_id} created successfully.");
println!("Group#{} created successfully.", chat_id);
}
"createbroadcast" => {
let chat_id = chat::create_broadcast_list(&context).await?;
println!("Broadcast#{chat_id} created successfully.");
println!("Broadcast#{} created successfully.", chat_id);
}
"createprotected" => {
ensure!(!arg1.is_empty(), "Argument <name> missing.");
let chat_id =
chat::create_group_chat(&context, ProtectionStatus::Protected, arg1).await?;
println!("Group#{chat_id} created and protected successfully.");
println!("Group#{} created and protected successfully.", chat_id);
}
"addmember" => {
ensure!(sel_chat.is_some(), "No chat selected");
@@ -788,7 +770,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat::set_chat_name(
&context,
sel_chat.as_ref().unwrap().get_id(),
format!("{arg1} {arg2}").trim(),
format!("{} {}", arg1, arg2).trim(),
)
.await?;
@@ -882,7 +864,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
if continue_streaming {
println!("Success, streaming should be continued.");
} else {
println!("Success, streaming can be stopped.");
println!("Success, streaming can be stoppped.");
}
}
"dellocations" => {
@@ -892,7 +874,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(sel_chat.is_some(), "No chat selected.");
ensure!(!arg1.is_empty(), "No message text given.");
let msg = format!("{arg1} {arg2}");
let msg = format!("{} {}", arg1, arg2);
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?;
}
@@ -934,7 +916,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
}
"sendsyncmsg" => match context.send_sync_msg().await? {
Some(msg_id) => println!("sync message sent as {msg_id}."),
Some(msg_id) => println!("sync message sent as {}.", msg_id),
None => println!("sync message not needed."),
},
"sendupdate" => {
@@ -954,7 +936,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"listmsgs" => {
ensure!(!arg1.is_empty(), "Argument <query> missing.");
let query = format!("{arg1} {arg2}").trim().to_string();
let query = format!("{} {}", arg1, arg2).trim().to_string();
let chat = sel_chat.as_ref().map(|sel_chat| sel_chat.get_id());
let time_start = std::time::SystemTime::now();
let msglist = context.search_msgs(chat, &query).await?;
@@ -972,7 +954,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
},
query,
);
println!("{time_needed:?} to create this list");
println!("{:?} to create this list", time_needed);
}
"draft" => {
ensure!(sel_chat.is_some(), "No chat selected.");
@@ -1018,9 +1000,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
println!("{} images or videos: ", images.len());
for (i, data) in images.iter().enumerate() {
if 0 == i {
print!("{data}");
print!("{}", data);
} else {
print!(", {data}");
print!(", {}", data);
}
}
println!();
@@ -1091,12 +1073,12 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?);
let res = message::get_msg_info(&context, id).await?;
println!("{res}");
println!("{}", res);
}
"download" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?);
println!("Scheduling download for {id:?}");
println!("Scheduling download for {:?}", id);
id.download_full(&context).await?;
}
"html" => {
@@ -1107,7 +1089,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.join(format!("msg-{}.html", id.to_u32()));
let html = id.get_html(&context).await?.unwrap_or_default();
fs::write(&file, html).await?;
println!("HTML written to: {file:#?}");
println!("HTML written to: {:#?}", file);
}
"listfresh" => {
let msglist = context.get_fresh_msgs().await?;
@@ -1169,7 +1151,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
if !arg2.is_empty() {
let book = format!("{arg1}\n{arg2}");
let book = format!("{}\n{}", arg1, arg2);
Contact::add_address_book(&context, &book).await?;
} else {
Contact::create(&context, "", arg1).await?;
@@ -1196,7 +1178,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let chatlist = Chatlist::try_load(&context, 0, None, Some(contact_id)).await?;
let chatlist_cnt = chatlist.len();
if chatlist_cnt > 0 {
res += &format!("\n\n{chatlist_cnt} chats shared with Contact#{contact_id}: ",);
res += &format!(
"\n\n{} chats shared with Contact#{}: ",
chatlist_cnt, contact_id,
);
for i in 0..chatlist_cnt {
if 0 != i {
res += ", ";
@@ -1206,7 +1191,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
}
println!("{res}");
println!("{}", res);
}
"delcontact" => {
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
@@ -1230,13 +1215,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"checkqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
let qr = check_qr(&context, arg1).await?;
println!("qr={qr:?}");
println!("qr={:?}", qr);
}
"setqr" => {
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
match set_config_from_qr(&context, arg1).await {
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
Err(err) => println!("Cannot set config from QR code: {err:?}"),
Err(err) => println!("Cannot set config from QR code: {:?}", err),
}
}
"providerinfo" => {
@@ -1246,7 +1231,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
.await?;
match provider::get_provider_info(&context, arg1, socks5_enabled).await {
Some(info) => {
println!("Information for provider belonging to {arg1}:");
println!("Information for provider belonging to {}:", arg1);
println!("status: {}", info.status as u32);
println!("before_login_hint: {}", info.before_login_hint);
println!("after_login_hint: {}", info.after_login_hint);
@@ -1256,7 +1241,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
}
None => {
println!("No information for provider belonging to {arg1} found.");
println!("No information for provider belonging to {} found.", arg1);
}
}
}
@@ -1265,7 +1250,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
if let Ok(buf) = read_file(&context, &arg1).await {
let (width, height) = get_filemeta(&buf)?;
println!("width={width}, height={height}");
println!("width={}, height={}", width, height);
} else {
bail!("Command failed.");
}
@@ -1276,7 +1261,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let device_cnt = message::estimate_deletion_cnt(&context, false, seconds).await?;
let server_cnt = message::estimate_deletion_cnt(&context, true, seconds).await?;
println!(
"estimated count of messages older than {seconds} seconds:\non device: {device_cnt}\non server: {server_cnt}"
"estimated count of messages older than {} seconds:\non device: {}\non server: {}",
seconds, device_cnt, server_cnt
);
}
"" => (),

View File

@@ -67,7 +67,8 @@ fn receive_event(event: EventType) {
info!(
"{}",
yellow.paint(format!(
"Received MSGS_CHANGED(chat_id={chat_id}, msg_id={msg_id})",
"Received MSGS_CHANGED(chat_id={}, msg_id={})",
chat_id, msg_id,
))
);
}
@@ -79,7 +80,8 @@ fn receive_event(event: EventType) {
info!(
"{}",
yellow.paint(format!(
"Received REACTIONS_CHANGED(chat_id={chat_id}, msg_id={msg_id}, contact_id={contact_id})"
"Received REACTIONS_CHANGED(chat_id={}, msg_id={}, contact_id={})",
chat_id, msg_id, contact_id
))
);
}
@@ -89,7 +91,7 @@ fn receive_event(event: EventType) {
EventType::LocationChanged(contact) => {
info!(
"{}",
yellow.paint(format!("Received LOCATION_CHANGED(contact={contact:?})"))
yellow.paint(format!("Received LOCATION_CHANGED(contact={:?})", contact))
);
}
EventType::ConfigureProgress { progress, comment } => {
@@ -97,20 +99,21 @@ fn receive_event(event: EventType) {
info!(
"{}",
yellow.paint(format!(
"Received CONFIGURE_PROGRESS({progress} ‰, {comment})"
"Received CONFIGURE_PROGRESS({} ‰, {})",
progress, comment
))
);
} else {
info!(
"{}",
yellow.paint(format!("Received CONFIGURE_PROGRESS({progress} ‰)"))
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
);
}
}
EventType::ImexProgress(progress) => {
info!(
"{}",
yellow.paint(format!("Received IMEX_PROGRESS({progress} ‰)"))
yellow.paint(format!("Received IMEX_PROGRESS({} ‰)", progress))
);
}
EventType::ImexFileWritten(file) => {
@@ -122,7 +125,7 @@ fn receive_event(event: EventType) {
EventType::ChatModified(chat) => {
info!(
"{}",
yellow.paint(format!("Received CHAT_MODIFIED({chat})"))
yellow.paint(format!("Received CHAT_MODIFIED({})", chat))
);
}
_ => {
@@ -152,15 +155,13 @@ impl Completer for DcHelper {
}
}
const IMEX_COMMANDS: [&str; 14] = [
const IMEX_COMMANDS: [&str; 12] = [
"initiate-key-transfer",
"get-setupcodebegin",
"continue-key-transfer",
"has-backup",
"export-backup",
"import-backup",
"send-backup",
"receive-backup",
"export-keys",
"import-keys",
"export-setup",
@@ -352,8 +353,8 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
match readline {
Ok(line) => {
// TODO: ignore "set mail_pw"
rl.add_history_entry(line.as_str())?;
let should_continue = Handle::current().block_on(async {
rl.add_history_entry(line.as_str());
let contine = Handle::current().block_on(async {
match handle_cmd(line.trim(), ctx.clone(), &mut selected_chat).await {
Ok(ExitResult::Continue) => true,
Ok(ExitResult::Exit) => {
@@ -361,13 +362,13 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
false
}
Err(err) => {
println!("Error: {err:#}");
println!("Error: {}", err);
true
}
}
});
if !should_continue {
if !contine {
break;
}
}
@@ -376,7 +377,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
break;
}
Err(err) => {
println!("Error: {err:#}");
println!("Error: {}", err);
break;
}
}
@@ -443,7 +444,7 @@ async fn handle_cmd(
if arg0 == "getbadqr" && qr.len() > 40 {
qr.replace_range(12..22, "0000000000")
}
println!("{qr}");
println!("{}", qr);
let output = Command::new("qrencode")
.args(["-t", "ansiutf8", qr.as_str(), "-o", "-"])
.output()
@@ -459,7 +460,7 @@ async fn handle_cmd(
match get_securejoin_qr_svg(&ctx, group).await {
Ok(svg) => {
fs::write(&file, svg).await?;
println!("QR code svg written to: {file:#?}");
println!("QR code svg written to: {:#?}", file);
}
Err(err) => {
bail!("Failed to get QR code svg: {}", err);

View File

@@ -28,7 +28,7 @@ fn cb(event: EventType) {
}
}
/// Run with `RUST_LOG=simple=info cargo run --release --example simple -- email pw`.
/// Run with `RUST_LOG=simple=info cargo run --release --example simple --features repl -- email pw`.
#[tokio::main]
async fn main() {
pretty_env_logger::try_init_timed().ok();
@@ -74,7 +74,7 @@ async fn main() {
for i in 0..1 {
log::info!("sending message {}", i);
chat::send_text_msg(&ctx, chat_id, format!("Hi, here is my {i}nth message!"))
chat::send_text_msg(&ctx, chat_id, format!("Hi, here is my {}nth message!", i))
.await
.unwrap();
}

161
fuzz/Cargo.lock generated
View File

@@ -111,7 +111,7 @@ version = "0.6.0"
source = "git+https://github.com/async-email/async-imap?branch=master#85ff7a3d9d71a3715354fabf2fc1a8d047b5710e"
dependencies = [
"async-channel",
"async-native-tls 0.4.0",
"async-native-tls",
"base64 0.13.1",
"byte-pool",
"chrono",
@@ -140,31 +140,22 @@ dependencies = [
]
[[package]]
name = "async-native-tls"
name = "async-smtp"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9343dc5acf07e79ff82d0c37899f079db3534d99f189a1837c8e549c99405bec"
checksum = "6da21e1dd19fbad3e095ad519fb1558ab77fd82e5c4778dca8f9be0464589e1e"
dependencies = [
"native-tls",
"thiserror",
"tokio",
"url",
]
[[package]]
name = "async-smtp"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7384febcabdd07a498c9f4fbaa7e488ff4eb60d0ade14b47b09ec44b8f645301"
dependencies = [
"anyhow",
"async-native-tls",
"async-trait",
"base64 0.13.1",
"bufstream",
"fast-socks5",
"futures",
"hostname",
"log",
"nom 7.1.1",
"pin-project",
"pin-utils",
"thiserror",
"tokio",
]
@@ -244,9 +235,9 @@ checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
[[package]]
name = "base64"
version = "0.21.0"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a"
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
[[package]]
name = "base64ct"
@@ -708,16 +699,16 @@ checksum = "23d8666cb01533c39dde32bcbab8e227b4ed6679b2c925eba05feabea39508fb"
[[package]]
name = "deltachat"
version = "1.111.0"
version = "1.104.0"
dependencies = [
"anyhow",
"async-channel",
"async-imap",
"async-native-tls 0.5.0",
"async-native-tls",
"async-smtp",
"async_zip",
"backtrace",
"base64 0.21.0",
"base64 0.20.0",
"bitflags",
"chrono",
"deltachat_derive",
@@ -735,17 +726,18 @@ dependencies = [
"lettre_email",
"libc",
"mailparse 0.14.0",
"native-tls",
"num-derive",
"num-traits",
"num_cpus",
"once_cell",
"parking_lot",
"percent-encoding",
"pgp",
"qrcodegen",
"quick-xml",
"r2d2",
"r2d2_sqlite",
"rand 0.8.5",
"ratelimit",
"regex",
"reqwest",
"rusqlite",
@@ -1294,6 +1286,15 @@ dependencies = [
"tracing",
]
[[package]]
name = "hashbrown"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
dependencies = [
"ahash",
]
[[package]]
name = "hashbrown"
version = "0.12.3"
@@ -1305,11 +1306,11 @@ dependencies = [
[[package]]
name = "hashlink"
version = "0.8.1"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa"
checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf"
dependencies = [
"hashbrown",
"hashbrown 0.11.2",
]
[[package]]
@@ -1507,7 +1508,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [
"autocfg",
"hashbrown",
"hashbrown 0.12.3",
]
[[package]]
@@ -1632,9 +1633,9 @@ checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb"
[[package]]
name = "libsqlite3-sys"
version = "0.25.2"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29f835d03d717946d28b1d1ed632eb6f0e24a299388ee623d0c23118d3e8a7fa"
checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14"
dependencies = [
"cc",
"openssl-sys",
@@ -1811,15 +1812,6 @@ dependencies = [
"minimal-lexical",
]
[[package]]
name = "nom8"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae01545c9c7fc4486ab7debaf2aad7003ac19431791868fb2e8066df97fad2f8"
dependencies = [
"memchr",
]
[[package]]
name = "num-bigint-dig"
version = "0.8.2"
@@ -1956,9 +1948,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
[[package]]
name = "openssl-src"
version = "111.25.0+1.1.1t"
version = "111.24.0+1.1.1s"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3173cd3626c43e3854b1b727422a276e568d9ec5fe8cec197822cf52cfb743d6"
checksum = "3498f259dab01178c6228c6b00dcef0ed2a2d5e20d648c017861227773ea4abd"
dependencies = [
"cc",
]
@@ -2242,6 +2234,27 @@ version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20f14e071918cbeefc5edc986a7aa92c425dae244e003a35e1cdddb5ca39b5cb"
[[package]]
name = "r2d2"
version = "0.8.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51de85fb3fb6524929c8a2eb85e6b6d363de4e8c48f9e2c2eac4944abc181c93"
dependencies = [
"log",
"parking_lot",
"scheduled-thread-pool",
]
[[package]]
name = "r2d2_sqlite"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6fdc8e4da70586127893be32b7adf21326a4c6b1aba907611edf467d13ffe895"
dependencies = [
"r2d2",
"rusqlite",
]
[[package]]
name = "rand"
version = "0.7.3"
@@ -2313,10 +2326,6 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "ratelimit"
version = "1.0.0"
[[package]]
name = "redox_syscall"
version = "0.2.16"
@@ -2354,11 +2363,11 @@ dependencies = [
[[package]]
name = "reqwest"
version = "0.11.14"
version = "0.11.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "21eed90ec8570952d53b772ecf8f206aa1ec9a3d76b2521c56c42973f2d91ee9"
checksum = "68cc60575865c7831548863cc02356512e3f1dc2f3f82cb837d7fc4cc8f3c97c"
dependencies = [
"base64 0.21.0",
"base64 0.13.1",
"bytes",
"encoding_rs",
"futures-core",
@@ -2431,15 +2440,16 @@ dependencies = [
[[package]]
name = "rusqlite"
version = "0.28.0"
version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01e213bc3ecb39ac32e81e51ebe31fd888a940515173e3a18a35f8c6e896422a"
checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a"
dependencies = [
"bitflags",
"fallible-iterator",
"fallible-streaming-iterator",
"hashlink",
"libsqlite3-sys",
"memchr",
"smallvec",
]
@@ -2493,6 +2503,15 @@ dependencies = [
"windows-sys 0.36.1",
]
[[package]]
name = "scheduled-thread-pool"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "977a7519bff143a44f842fd07e80ad1329295bd71686457f18e496736f4bf9bf"
dependencies = [
"parking_lot",
]
[[package]]
name = "scopeguard"
version = "1.1.0"
@@ -2559,15 +2578,6 @@ dependencies = [
"serde",
]
[[package]]
name = "serde_spanned"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4"
dependencies = [
"serde",
]
[[package]]
name = "serde_urlencoded"
version = "0.7.1"
@@ -2853,9 +2863,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.25.0"
version = "1.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af"
checksum = "1d9f76183f91ecfb55e1d7d5602bd1d979e38a3a522fe900241cf195624d67ae"
dependencies = [
"autocfg",
"bytes",
@@ -2942,36 +2952,11 @@ dependencies = [
[[package]]
name = "toml"
version = "0.7.2"
version = "0.5.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7afcae9e3f0fe2c370fd4657108972cbb2fa9db1b9f84849cefd80741b01cb6"
checksum = "1333c76748e868a4d9d1017b5ab53171dfd095f70c712fdb4653a406547f598f"
dependencies = [
"serde",
"serde_spanned",
"toml_datetime",
"toml_edit",
]
[[package]]
name = "toml_datetime"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.19.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e6a7712b49e1775fb9a7b998de6635b299237f48b404dde71704f2e0e7f37e5"
dependencies = [
"indexmap",
"nom8",
"serde",
"serde_spanned",
"toml_datetime",
]
[[package]]
@@ -3096,7 +3081,7 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137"
dependencies = [
"hashbrown",
"hashbrown 0.12.3",
"regex",
]

View File

@@ -28,7 +28,7 @@ This code used to live at [`deltachat-node`](https://github.com/deltachat/deltac
## Install
By default the installation will try to use the bundled prebuilds in the
By default the installation will build try to use the bundled prebuilds in the
npm package. If this fails it falls back to compile `../deltachat-core-rust` from
this repository, using `scripts/rebuild-core.js`.
@@ -124,7 +124,7 @@ $ npm run test
```
(when using [fnm](https://github.com/Schniz/fnm) instead of nvm, you can select the architecture)
If your node and electron are already build for arm64 you can also try building for arm:
If your node and electron are already build for arm64 you can also try bulding for arm:
```
$ fnm install 16 --arch arm64
@@ -182,7 +182,7 @@ this example can also be found in the examples folder [examples/send_message.js]
### Generating Docs
We are currently migrating to automatically generated documentation.
We are curently migrating to automaticaly generated documentation.
You can find the old documentation at [old_docs](./old_docs).
to generate the documentation, run:

View File

@@ -2,7 +2,7 @@ import { join } from 'path'
/**
* bindings are not typed yet.
* if the available function names are required they can be found inside of `../src/module.c`
* if the availible function names are required they can be found inside of `../src/module.c`
*/
export const bindings: any = require('node-gyp-build')(join(__dirname, '../'))

View File

@@ -543,7 +543,7 @@ export class Context extends EventEmitter {
/**
*
* @deprecated please use `AccountManager.getSystemInfo()` instead
* @deprectated please use `AccountManager.getSystemInfo()` instead
*/
static getSystemInfo() {
return AccountManager.getSystemInfo()
@@ -818,7 +818,7 @@ export class Context extends EventEmitter {
* Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC.
* 0 for "all up to now".
* @return Array of locations, NULL is never returned.
* The array is sorted descending;
* The array is sorted decending;
* the first entry in the array is the location with the newest timestamp.
*
* Examples:

View File

@@ -217,7 +217,7 @@ export class AccountManager extends EventEmitter {
/** get information about the provider
*
* This function creates a temporary context to be standalone,
* if possible use `Context.getProviderFromEmail` instead. (otherwise potential proxy settings are not used)
* if posible use `Context.getProviderFromEmail` instead. (otherwise potential proxy settings are not used)
* @deprecated
*/
static getProviderFromEmail(email: string) {

View File

@@ -769,7 +769,7 @@ describe('Integration tests', function () {
this.beforeAll(async function () {
if (!process.env.DCC_NEW_TMP_EMAIL) {
console.log(
'Missing DCC_NEW_TMP_EMAIL environment variable!, skip integration tests'
'Missing DCC_NEW_TMP_EMAIL environment variable!, skip intergration tests'
)
this.skip()
}
@@ -777,7 +777,7 @@ describe('Integration tests', function () {
account = await createTempUser(process.env.DCC_NEW_TMP_EMAIL)
if (!account || !account.email || !account.password) {
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 intergration tests"
)
this.skip()
}

View File

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

View File

@@ -64,7 +64,7 @@
- use new experimental full-Rust Delta Chat core
- support Autocrypt Setup Messages
- remove synchronous events
- use CircleCI for continuous integration and packaging of Linux wheels
- use CircleCI for continous integration and packaging of Linux wheels
- use docker image for building wheels
- fix code documentation links

View File

@@ -14,10 +14,10 @@ class EchoPlugin:
message.create_chat()
addr = message.get_sender_contact().addr
if message.is_system_message():
message.chat.send_text(f"echoing system message from {addr}:\n{message}")
message.chat.send_text("echoing system message from {}:\n{}".format(addr, message))
else:
text = message.text
message.chat.send_text(f"echoing from {addr}:\n{text}")
message.chat.send_text("echoing from {}:\n{}".format(addr, text))
@account_hookimpl
def ac_message_delivered(self, message):

View File

@@ -14,7 +14,7 @@ class GroupTrackingPlugin:
message.create_chat()
addr = message.get_sender_contact().addr
text = message.text
message.chat.send_text(f"echoing from {addr}:\n{text}")
message.chat.send_text("echoing from {}:\n{}".format(addr, text))
@account_hookimpl
def ac_outgoing_message(self, message):
@@ -28,7 +28,7 @@ class GroupTrackingPlugin:
def ac_chat_modified(self, chat):
print("ac_chat_modified:", chat.id, chat.get_name())
for member in chat.get_contacts():
print(f"chat member: {member.addr}")
print("chat member: {}".format(member.addr))
@account_hookimpl
def ac_member_added(self, chat, contact, actor, message):
@@ -40,7 +40,7 @@ class GroupTrackingPlugin:
),
)
for member in chat.get_contacts():
print(f"chat member: {member.addr}")
print("chat member: {}".format(member.addr))
@account_hookimpl
def ac_member_removed(self, chat, contact, actor, message):

View File

@@ -21,7 +21,6 @@ classifiers = [
dependencies = [
"cffi>=1.0.0",
"imap-tools",
"importlib_metadata;python_version<'3.8'",
"pluggy",
"requests",
]
@@ -37,21 +36,16 @@ dynamic = [
[project.entry-points.pytest11]
"deltachat.testplugin" = "deltachat.testplugin"
[tool.setuptools.package-data]
deltachat = [
"py.typed"
]
[tool.setuptools_scm]
root = ".."
tag_regex = '^(?P<prefix>v)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
git_describe_command = "git describe --dirty --tags --long --match v*.*"
tag_regex = '^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
git_describe_command = "git describe --dirty --tags --long --match py-*.*"
[tool.black]
line-length = 120
[tool.ruff]
select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM", "UP004", "UP010", "UP031", "UP032", "ANN204"]
select = ["E", "F", "W", "YTT", "C4", "ISC", "ICN", "TID", "DTZ", "PLC", "PLE", "PLW", "PIE", "COM"]
line-length = 120
[tool.isort]

View File

@@ -1,9 +1,6 @@
import sys
if sys.version_info >= (3, 8):
from importlib.metadata import PackageNotFoundError, version
else:
from importlib_metadata import PackageNotFoundError, version
from pkg_resources import DistributionNotFound, get_distribution
from . import capi, events, hookspec # noqa
from .account import Account, get_core_info # noqa
@@ -14,8 +11,8 @@ from .hookspec import account_hookimpl, global_hookimpl # noqa
from .message import Message # noqa
try:
__version__ = version(__name__)
except PackageNotFoundError:
__version__ = get_distribution(__name__).version
except DistributionNotFound:
# package is not installed
__version__ = "0.0.0.dev0-unknown"
@@ -58,7 +55,6 @@ def run_cmdline(argv=None, account_plugins=None):
ac.run_account(addr=args.email, password=args.password, account_plugins=account_plugins, show_ffi=args.show_ffi)
addr = ac.get_config("addr")
print(f"{addr}: waiting for message")
print("{}: waiting for message".format(ac.get_config("addr")))
ac.wait_shutdown()

View File

@@ -171,7 +171,7 @@ def extract_defines(flags):
match = defines_re.match(line)
if match:
defines.append(match.group(1))
return "\n".join(f"#define {d} ..." for d in defines)
return "\n".join("#define {} ...".format(d) for d in defines)
def ffibuilder():

View File

@@ -1,12 +1,13 @@
"""Account class implementation."""
from __future__ import print_function
import os
from array import array
from contextlib import contextmanager
from email.utils import parseaddr
from threading import Event
from typing import Any, Dict, Generator, List, Optional, Union, TYPE_CHECKING
from typing import Any, Dict, Generator, List, Optional, Union
from . import const, hookspec
from .capi import ffi, lib
@@ -19,12 +20,10 @@ from .cutil import (
from_optional_dc_charpointer,
iter_array,
)
from .events import EventThread, FFIEventLogger
from .message import Message
from .tracker import ConfigureTracker, ImexTracker
if TYPE_CHECKING:
from .events import FFIEventTracker
class MissingCredentials(ValueError):
"""Account is missing `addr` and `mail_pw` config values."""
@@ -55,7 +54,7 @@ def get_dc_info_as_dict(dc_context):
return info_dict
class Account:
class Account(object):
"""Each account is tied to a sqlite database file which is fully managed
by the underlying deltachat core library. All public Account methods are
meant to be memory-safe and return memory-safe objects.
@@ -63,12 +62,7 @@ class Account:
MissingCredentials = MissingCredentials
_logid: str
_evtracker: "FFIEventTracker"
def __init__(self, db_path, os_name=None, logging=True, closed=False) -> None:
from .events import EventThread
"""initialize account object.
:param db_path: a path to the account database. The database
@@ -90,7 +84,7 @@ class Account:
ptr = lib.dc_context_new_closed(db_path) if closed else lib.dc_context_new(ffi.NULL, db_path, ffi.NULL)
if ptr == ffi.NULL:
raise ValueError(f"Could not dc_context_new: {os_name} {db_path}")
raise ValueError("Could not dc_context_new: {} {}".format(os_name, db_path))
self._dc_context = ffi.gc(
ptr,
lib.dc_context_unref,
@@ -121,8 +115,8 @@ class Account:
"""re-enable logging."""
self._logging = True
def __repr__(self) -> str:
return f"<Account path={self.db_path}>"
def __repr__(self):
return "<Account path={}>".format(self.db_path)
# def __del__(self):
# self.shutdown()
@@ -133,7 +127,7 @@ class Account:
def _check_config_key(self, name: str) -> None:
if name not in self._configkeys:
raise KeyError(f"{name!r} not a valid config key, existing keys: {self._configkeys!r}")
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(name, self._configkeys))
def get_info(self) -> Dict[str, str]:
"""return dictionary of built config parameters."""
@@ -147,7 +141,7 @@ class Account:
log("=============== " + self.get_config("displayname") + " ===============")
cursor = 0
for name, val in self.get_info().items():
entry = f"{name.upper()}={val}"
entry = "{}={}".format(name.upper(), val)
if cursor + len(entry) > 80:
log("")
cursor = 0
@@ -159,7 +153,7 @@ class Account:
"""set stock translation string.
:param id: id of stock string (const.DC_STR_*)
:param value: string to set as new translation
:param value: string to set as new transalation
:returns: None
"""
bytestring = string.encode("utf8")
@@ -192,7 +186,7 @@ class Account:
self._check_config_key(name)
namebytes = name.encode("utf8")
res = lib.dc_get_config(self._dc_context, namebytes)
assert res != ffi.NULL, f"config value not found for: {name!r}"
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
return from_dc_charpointer(res)
def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None:
@@ -284,9 +278,9 @@ class Account:
:returns: :class:`deltachat.contact.Contact` instance.
"""
(name, addr) = self.get_contact_addr_and_name(obj, name)
name_c = as_dc_charpointer(name)
addr_c = as_dc_charpointer(addr)
contact_id = lib.dc_create_contact(self._dc_context, name_c, addr_c)
name = as_dc_charpointer(name)
addr = as_dc_charpointer(addr)
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
return Contact(self, contact_id)
def get_contact(self, obj) -> Optional[Contact]:
@@ -302,12 +296,12 @@ class Account:
addr, displayname = obj.get_config("addr"), obj.get_config("displayname")
elif isinstance(obj, Contact):
if obj.account != self:
raise ValueError(f"account mismatch {obj}")
raise ValueError("account mismatch {}".format(obj))
addr, displayname = obj.addr, obj.name
elif isinstance(obj, str):
displayname, addr = parseaddr(obj)
else:
raise TypeError(f"don't know how to create chat for {obj!r}")
raise TypeError("don't know how to create chat for %r" % (obj,))
if name is None and displayname:
name = displayname
@@ -363,18 +357,18 @@ class Account:
:returns: list of :class:`deltachat.contact.Contact` objects.
"""
flags = 0
query_c = as_dc_charpointer(query)
query = as_dc_charpointer(query)
if only_verified:
flags |= const.DC_GCL_VERIFIED_ONLY
if with_self:
flags |= const.DC_GCL_ADD_SELF
dc_array = ffi.gc(lib.dc_get_contacts(self._dc_context, flags, query_c), lib.dc_array_unref)
dc_array = ffi.gc(lib.dc_get_contacts(self._dc_context, flags, query), lib.dc_array_unref)
return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_fresh_messages(self) -> Generator[Message, None, None]:
"""yield all fresh messages from all chats."""
dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref)
return (x for x in iter_array(dc_array, lambda x: Message.from_db(self, x)) if x is not None)
yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
def create_chat(self, obj) -> Chat:
"""Create a 1:1 chat with Account, Contact or e-mail address."""
@@ -419,7 +413,7 @@ class Account:
def get_device_chat(self) -> Chat:
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
def get_message_by_id(self, msg_id: int) -> Optional[Message]:
def get_message_by_id(self, msg_id: int) -> Message:
"""return Message instance.
:param msg_id: integer id of this message.
:returns: :class:`deltachat.message.Message` instance.
@@ -434,7 +428,7 @@ class Account:
"""
res = lib.dc_get_chat(self._dc_context, chat_id)
if res == ffi.NULL:
raise ValueError(f"cannot get chat with id={chat_id}")
raise ValueError("cannot get chat with id={}".format(chat_id))
lib.dc_chat_unref(res)
return Chat(self, chat_id)
@@ -551,7 +545,7 @@ class Account:
res = ffi.gc(lib.dc_check_qr(self._dc_context, as_dc_charpointer(qr)), lib.dc_lot_unref)
lot = DCLot(res)
if lot.state() == const.DC_QR_ERROR:
raise ValueError(f"invalid or unknown QR code: {lot.text1()}")
raise ValueError("invalid or unknown QR code: {}".format(lot.text1()))
return ScannedQRCode(lot)
def qr_setup_contact(self, qr):
@@ -602,8 +596,6 @@ class Account:
#
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False):
from .events import FFIEventLogger
"""get the account running, configure it if necessary. add plugins if provided.
:param addr: the email address of the account
@@ -624,11 +616,13 @@ class Account:
assert addr and password, "you must specify email and password once to configure this database/account"
self.set_config("addr", addr)
self.set_config("mail_pw", password)
self.set_config("mvbox_move", "0")
self.set_config("sentbox_watch", "0")
self.set_config("bot", "1")
configtracker = self.configure()
configtracker.wait_finish()
# start IO threads and configure if necessary
# start IO threads and configure if neccessary
self.start_io()
def add_account_plugin(self, plugin, name=None):
@@ -751,7 +745,7 @@ class Account:
try:
self._event_thread.wait(timeout=5)
except RuntimeError as e:
self.log(f"Waiting for event thread failed: {e}")
self.log("Waiting for event thread failed: {}".format(e))
if self._event_thread.is_alive():
self.log("WARN: event thread did not terminate yet, ignoring.")
@@ -767,7 +761,7 @@ class Account:
class ScannedQRCode:
def __init__(self, dc_lot) -> None:
def __init__(self, dc_lot):
self._dc_lot = dc_lot
def is_ask_verifycontact(self):

View File

@@ -18,13 +18,13 @@ from .cutil import (
from .message import Message
class Chat:
class Chat(object):
"""Chat object which manages members and through which you can send and retrieve messages.
You obtain instances of it through :class:`deltachat.account.Account`.
"""
def __init__(self, account, id: int) -> None:
def __init__(self, account, id) -> None:
from .account import Account
assert isinstance(account, Account), repr(account)
@@ -40,7 +40,7 @@ class Chat:
return not self == other
def __repr__(self) -> str:
return f"<Chat id={self.id} name={self.get_name()}>"
return "<Chat id={} name={}>".format(self.id, self.get_name())
@property
def _dc_chat(self):
@@ -162,8 +162,8 @@ class Chat:
:param name: as a unicode string.
:returns: True on success, False otherwise
"""
name_c = as_dc_charpointer(name)
return bool(lib.dc_set_chat_name(self.account._dc_context, self.id, name_c))
name = as_dc_charpointer(name)
return bool(lib.dc_set_chat_name(self.account._dc_context, self.id, name))
def get_color(self):
"""return the color of the chat.
@@ -282,20 +282,12 @@ class Chat:
if msg.is_out_preparing():
assert msg.id != 0
# get a fresh copy of dc_msg, the core needs it
maybe_msg = Message.from_db(self.account, msg.id)
if maybe_msg is not None:
msg = maybe_msg
else:
raise ValueError("message does not exist")
msg = Message.from_db(self.account, msg.id)
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
if sent_id == 0:
raise ValueError("message could not be sent")
# modify message in place to avoid bad state for the caller
sent_msg = Message.from_db(self.account, sent_id)
if sent_msg is None:
raise ValueError("cannot load just sent message from the database")
msg._dc_msg = sent_msg._dc_msg
msg._dc_msg = Message.from_db(self.account, sent_id)._dc_msg
return msg
def send_text(self, text):
@@ -452,7 +444,7 @@ class Chat:
contact = self.account.create_contact(obj)
ret = lib.dc_add_contact_to_chat(self.account._dc_context, self.id, contact.id)
if ret != 1:
raise ValueError(f"could not add contact {contact!r} to chat")
raise ValueError("could not add contact {!r} to chat".format(contact))
return contact
def remove_contact(self, obj):
@@ -465,7 +457,7 @@ class Chat:
contact = self.account.get_contact(obj)
ret = lib.dc_remove_contact_from_chat(self.account._dc_context, self.id, contact.id)
if ret != 1:
raise ValueError(f"could not remove contact {contact!r} from chat")
raise ValueError("could not remove contact {!r} from chat".format(contact))
def get_contacts(self):
"""get all contacts for this chat.
@@ -501,7 +493,7 @@ class Chat:
p = as_dc_charpointer(img_path)
res = lib.dc_set_chat_profile_image(self.account._dc_context, self.id, p)
if res != 1:
raise ValueError(f"Setting Profile Image {p!r} failed")
raise ValueError("Setting Profile Image {!r} failed".format(p))
def remove_profile_image(self):
"""remove group profile image.
@@ -532,13 +524,13 @@ class Chat:
# ------ location streaming API ------------------------------
def is_sending_locations(self) -> bool:
def is_sending_locations(self):
"""return True if this chat has location-sending enabled currently.
:returns: True if location sending is enabled.
"""
return bool(lib.dc_is_sending_locations_to_chat(self.account._dc_context, self.id))
return lib.dc_is_sending_locations_to_chat(self.account._dc_context, self.id)
def enable_sending_locations(self, seconds) -> None:
def enable_sending_locations(self, seconds):
"""enable sending locations for this chat.
all subsequent messages will carry a location with them.
@@ -572,7 +564,7 @@ class Chat:
class Location:
def __init__(self, latitude, longitude, accuracy, timestamp, marker) -> None:
def __init__(self, latitude, longitude, accuracy, timestamp, marker):
assert isinstance(timestamp, datetime)
self.latitude = latitude
self.longitude = longitude
@@ -580,5 +572,5 @@ class Location:
self.timestamp = timestamp
self.marker = marker
def __eq__(self, other) -> bool:
def __eq__(self, other):
return self.__dict__ == other.__dict__

View File

@@ -9,13 +9,13 @@ from .chat import Chat
from .cutil import from_dc_charpointer, from_optional_dc_charpointer
class Contact:
class Contact(object):
"""Delta-Chat Contact.
You obtain instances of it through :class:`deltachat.account.Account`.
"""
def __init__(self, account, id) -> None:
def __init__(self, account, id):
from .account import Account
assert isinstance(account, Account), repr(account)
@@ -27,11 +27,11 @@ class Contact:
return False
return self.account._dc_context == other.account._dc_context and self.id == other.id
def __ne__(self, other) -> bool:
def __ne__(self, other):
return not self == other
def __repr__(self) -> str:
return f"<Contact id={self.id} addr={self.addr} dc_context={self.account._dc_context}>"
def __repr__(self):
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self.account._dc_context)
@property
def _dc_contact(self):

View File

@@ -82,7 +82,7 @@ class DirectImap:
configured, otherwise None.
"""
if "_" not in config_name:
config_name = f"configured_{config_name}_folder"
config_name = "configured_{}_folder".format(config_name)
foldername = self.account.get_config(config_name)
if foldername:
return self.select_folder(foldername)
@@ -191,7 +191,7 @@ class DirectImap:
class IdleManager:
def __init__(self, direct_imap) -> None:
def __init__(self, direct_imap):
self.direct_imap = direct_imap
self.log = direct_imap.account.log
# fetch latest messages before starting idle so that it only
@@ -203,18 +203,18 @@ class IdleManager:
"""(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}")
self.log("imap-direct: idle_check returned {!r}".format(res))
return res
def wait_for_new_message(self, timeout=None) -> bytes:
while True:
while 1:
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:
while 1:
for item in self.check(timeout=timeout):
if FETCH in item:
self.log(str(item))

View File

@@ -13,7 +13,6 @@ from .capi import ffi, lib
from .cutil import from_optional_dc_charpointer
from .hookspec import account_hookimpl
from .message import map_system_message
from .account import Account
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
@@ -25,18 +24,18 @@ def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
class FFIEvent:
def __init__(self, name: str, data1, data2) -> None:
def __init__(self, name: str, data1, data2):
self.name = name
self.data1 = data1
self.data2 = data2
def __str__(self) -> str:
def __str__(self):
if self.name == "DC_EVENT_INFO":
return f"INFO {self.data2}"
return "INFO {data2}".format(data2=self.data2)
if self.name == "DC_EVENT_WARNING":
return f"WARNING {self.data2}"
return "WARNING {data2}".format(data2=self.data2)
if self.name == "DC_EVENT_ERROR":
return f"ERROR {self.data2}"
return "ERROR {data2}".format(data2=self.data2)
return "{name} data1={data1} data2={data2}".format(**self.__dict__)
@@ -69,7 +68,7 @@ class FFIEventLogger:
locname = tname
if self.logid:
locname += "-" + self.logid
s = f"{elapsed:2.2f} [{locname}] {message}"
s = "{:2.2f} [{}] {}".format(elapsed, locname, message)
if os.name == "posix":
WARN = "\033[93m"
@@ -84,10 +83,7 @@ class FFIEventLogger:
class FFIEventTracker:
account: Account
_event_queue: Queue
def __init__(self, account: Account, timeout=None) -> None:
def __init__(self, account, timeout=None):
self.account = account
self._timeout = timeout
self._event_queue = Queue()
@@ -107,29 +103,29 @@ class FFIEventTracker:
timeout = timeout if timeout is not None else self._timeout
ev = self._event_queue.get(timeout=timeout)
if check_error and ev.name == "DC_EVENT_ERROR":
raise ValueError(f"unexpected event: {ev}")
raise ValueError("unexpected event: {}".format(ev))
return ev
def iter_events(self, timeout=None, check_error=True):
while True:
while 1:
yield self.get(timeout=timeout, check_error=check_error)
def get_matching(self, event_name_regex, check_error=True, timeout=None):
rex = re.compile(f"^(?:{event_name_regex})$")
rex = re.compile("^(?:{})$".format(event_name_regex))
for ev in self.iter_events(timeout=timeout, check_error=check_error):
if rex.match(ev.name):
return ev
def get_info_contains(self, regex: str) -> FFIEvent:
rex = re.compile(regex)
while True:
while 1:
ev = self.get_matching("DC_EVENT_INFO")
if rex.search(ev.data2):
return ev
def get_info_regex_groups(self, regex, check_error=True):
rex = re.compile(regex)
while True:
while 1:
ev = self.get_matching("DC_EVENT_INFO", check_error=check_error)
m = rex.match(ev.data2)
if m is not None:
@@ -140,7 +136,7 @@ class FFIEventTracker:
This only works reliably if the connectivity doesn't change
again too quickly, otherwise we might miss it.
"""
while True:
while 1:
if self.account.get_connectivity() == connectivity:
return
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
@@ -149,7 +145,7 @@ class FFIEventTracker:
"""Wait until the connectivity changes to `expected_next`.
Fails the test if it changes to something else.
"""
while True:
while 1:
current = self.account.get_connectivity()
if current == expected_next:
return
@@ -159,27 +155,27 @@ class FFIEventTracker:
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
def wait_for_all_work_done(self):
while True:
while 1:
if self.account.all_work_done():
return
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
def ensure_event_not_queued(self, event_name_regex):
__tracebackhide__ = True
rex = re.compile(f"(?:{event_name_regex}).*")
while True:
rex = re.compile("(?:{}).*".format(event_name_regex))
while 1:
try:
ev = self._event_queue.get(False)
except Empty:
break
else:
assert not rex.match(ev.name), f"event found {ev}"
assert not rex.match(ev.name), "event found {}".format(ev)
def wait_securejoin_inviter_progress(self, target):
while True:
while 1:
event = self.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
if event.data2 >= target:
print(f"** SECUREJOINT-INVITER PROGRESS {target}", self.account)
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), self.account)
break
def wait_idle_inbox_ready(self):
@@ -225,7 +221,7 @@ class EventThread(threading.Thread):
With each Account init this callback thread is started.
"""
def __init__(self, account: Account) -> None:
def __init__(self, account) -> None:
self.account = account
super(EventThread, self).__init__(name="events")
self.daemon = True
@@ -251,37 +247,36 @@ class EventThread(threading.Thread):
def run(self) -> None:
"""get and run events until shutdown."""
with self.log_execution("EVENT THREAD"):
event_emitter = ffi.gc(
lib.dc_get_event_emitter(self.account._dc_context),
lib.dc_event_emitter_unref,
)
while not self._marked_for_shutdown:
with self.swallow_and_log_exception("Unexpected error in event thread"):
event = lib.dc_get_next_event(event_emitter)
if event == ffi.NULL or self._marked_for_shutdown:
break
self._process_event(event)
self._inner_run()
def _process_event(self, event) -> None:
evt = lib.dc_event_get_id(event)
data1 = lib.dc_event_get_data1_int(event)
# the following code relates to the deltachat/_build.py's helper
# function which provides us signature info of an event call
evt_name = get_dc_event_name(evt)
if lib.dc_event_has_string_data(evt):
data2 = from_optional_dc_charpointer(lib.dc_event_get_data2_str(event))
else:
data2 = lib.dc_event_get_data2_int(event)
def _inner_run(self):
event_emitter = ffi.gc(
lib.dc_get_event_emitter(self.account._dc_context),
lib.dc_event_emitter_unref,
)
while not self._marked_for_shutdown:
event = lib.dc_get_next_event(event_emitter)
if event == ffi.NULL or self._marked_for_shutdown:
break
evt = lib.dc_event_get_id(event)
data1 = lib.dc_event_get_data1_int(event)
# the following code relates to the deltachat/_build.py's helper
# function which provides us signature info of an event call
evt_name = get_dc_event_name(evt)
if lib.dc_event_has_string_data(evt):
data2 = from_optional_dc_charpointer(lib.dc_event_get_data2_str(event))
else:
data2 = lib.dc_event_get_data2_int(event)
lib.dc_event_unref(event)
ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2)
with self.swallow_and_log_exception(f"ac_process_ffi_event {ffi_event}"):
self.account._pm.hook.ac_process_ffi_event(account=self, ffi_event=ffi_event)
for name, kwargs in self._map_ffi_event(ffi_event):
hook = getattr(self.account._pm.hook, name)
info = f"call {name} kwargs={kwargs} failed"
with self.swallow_and_log_exception(info):
hook(**kwargs)
lib.dc_event_unref(event)
ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2)
with self.swallow_and_log_exception("ac_process_ffi_event {}".format(ffi_event)):
self.account._pm.hook.ac_process_ffi_event(account=self, ffi_event=ffi_event)
for name, kwargs in self._map_ffi_event(ffi_event):
hook = getattr(self.account._pm.hook, name)
info = "call {} kwargs={} failed".format(name, kwargs)
with self.swallow_and_log_exception(info):
hook(**kwargs)
@contextmanager
def swallow_and_log_exception(self, info):
@@ -290,7 +285,7 @@ class EventThread(threading.Thread):
except Exception as ex:
logfile = io.StringIO()
traceback.print_exception(*sys.exc_info(), file=logfile)
self.account.log(f"{info}\nException {ex}\nTraceback:\n{logfile.getvalue()}")
self.account.log("{}\nException {}\nTraceback:\n{}".format(info, ex, logfile.getvalue()))
def _map_ffi_event(self, ffi_event: FFIEvent):
name = ffi_event.name
@@ -303,22 +298,20 @@ class EventThread(threading.Thread):
yield "ac_configure_completed", {"success": success, "comment": comment}
elif name == "DC_EVENT_INCOMING_MSG":
msg = account.get_message_by_id(ffi_event.data2)
if msg is not None:
yield map_system_message(msg) or ("ac_incoming_message", {"message": msg})
yield map_system_message(msg) or ("ac_incoming_message", {"message": msg})
elif name == "DC_EVENT_MSGS_CHANGED":
if ffi_event.data2 != 0:
msg = account.get_message_by_id(ffi_event.data2)
if msg is not None:
if msg.is_outgoing():
res = map_system_message(msg)
if res and res[0].startswith("ac_member"):
yield res
yield "ac_outgoing_message", {"message": msg}
elif msg.is_in_fresh():
yield map_system_message(msg) or (
"ac_incoming_message",
{"message": msg},
)
if msg.is_outgoing():
res = map_system_message(msg)
if res and res[0].startswith("ac_member"):
yield res
yield "ac_outgoing_message", {"message": msg}
elif msg.is_in_fresh():
yield map_system_message(msg) or (
"ac_incoming_message",
{"message": msg},
)
elif name == "DC_EVENT_REACTIONS_CHANGED":
assert ffi_event.data1 > 0
msg = account.get_message_by_id(ffi_event.data2)

View File

@@ -12,14 +12,14 @@ from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_char
from .reactions import Reactions
class Message:
class Message(object):
"""Message object.
You obtain instances of it through :class:`deltachat.account.Account` or
:class:`deltachat.chat.Chat`.
"""
def __init__(self, account, dc_msg) -> None:
def __init__(self, account, dc_msg):
self.account = account
assert isinstance(self.account._dc_context, ffi.CData)
assert isinstance(dc_msg, ffi.CData)
@@ -33,24 +33,24 @@ class Message:
return False
return self.account == other.account and self.id == other.id
def __repr__(self) -> str:
def __repr__(self):
c = self.get_sender_contact()
typ = "outgoing" if self.is_outgoing() else "incoming"
return (
f"<Message {typ} sys={self.is_system_message()} {repr(self.text[:100])} "
f"id={self.id} sender={c.id}/{c.addr} chat={self.chat.id}/{self.chat.get_name()}>"
return "<Message {} sys={} {} id={} sender={}/{} chat={}/{}>".format(
typ,
self.is_system_message(),
repr(self.text[:10]),
self.id,
c.id,
c.addr,
self.chat.id,
self.chat.get_name(),
)
@classmethod
def from_db(cls, account, id) -> Optional["Message"]:
"""Attempt to load the message from the database given its ID.
None is returned if the message does not exist, i.e. deleted."""
def from_db(cls, account, id):
assert id > 0
res = lib.dc_get_msg(account._dc_context, id)
if res == ffi.NULL:
return None
return cls(account, ffi.gc(res, lib.dc_msg_unref))
return cls(account, ffi.gc(lib.dc_get_msg(account._dc_context, id), lib.dc_msg_unref))
@classmethod
def new_empty(cls, account, view_type):
@@ -115,7 +115,7 @@ class Message:
"""set file for this message from path and mime_type."""
mtype = ffi.NULL if mime_type is None else as_dc_charpointer(mime_type)
if not os.path.exists(path):
raise ValueError(f"path does not exist: {path!r}")
raise ValueError("path does not exist: {!r}".format(path))
lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype)
@props.with_doc

View File

@@ -8,7 +8,7 @@ class ProviderNotFoundError(Exception):
"""The provider information was not found."""
class Provider:
class Provider(object):
"""
Provider information.

View File

@@ -4,21 +4,21 @@ from .capi import ffi, lib
from .cutil import from_dc_charpointer, iter_array
class Reactions:
class Reactions(object):
"""Reactions object.
You obtain instances of it through :class:`deltachat.message.Message`.
"""
def __init__(self, account, dc_reactions) -> None:
def __init__(self, account, dc_reactions):
assert isinstance(account._dc_context, ffi.CData)
assert isinstance(dc_reactions, ffi.CData)
assert dc_reactions != ffi.NULL
self.account = account
self._dc_reactions = dc_reactions
def __repr__(self) -> str:
return f"<Reactions dc_reactions={self._dc_reactions}>"
def __repr__(self):
return "<Reactions dc_reactions={}>".format(self._dc_reactions)
@classmethod
def from_msg(cls, msg):

View File

@@ -1,3 +1,5 @@
from __future__ import print_function
import fnmatch
import io
import os
@@ -9,7 +11,7 @@ import threading
import time
import weakref
from queue import Queue
from typing import Callable, List, Optional, Dict, Set
from typing import Callable, List, Optional
import pytest
import requests
@@ -60,13 +62,13 @@ def pytest_configure(config):
# Make sure we don't get garbled output because threads keep running
# collect all ever created accounts in a weakref-set (so we don't
# keep objects unnecessarily alive) and enable/disable logging
# keep objects unneccessarily alive) and enable/disable logging
# for each pytest test phase # (setup/call/teardown).
# Additionally make the acfactory use a logging/no-logging default.
class LoggingAspect:
def __init__(self) -> None:
self._accounts: weakref.WeakSet = weakref.WeakSet()
def __init__(self):
self._accounts = weakref.WeakSet()
@deltachat.global_hookimpl
def dc_account_init(self, account):
@@ -129,9 +131,9 @@ def pytest_report_header(config, startdir):
if cfg:
if "?" in cfg:
url, token = cfg.split("?", 1)
summary.append(f"Liveconfig provider: {url}?<token omitted>")
summary.append("Liveconfig provider: {}?<token ommitted>".format(url))
else:
summary.append(f"Liveconfig file: {cfg}")
summary.append("Liveconfig file: {}".format(cfg))
return summary
@@ -143,12 +145,10 @@ def testprocess(request):
class TestProcess:
"""A pytest session-scoped instance to help with managing "live" account configurations."""
_addr2files: Dict[str, Dict[pathlib.Path, bytes]]
def __init__(self, pytestconfig) -> None:
def __init__(self, pytestconfig):
self.pytestconfig = pytestconfig
self._addr2files = {}
self._configlist: List[Dict[str, str]] = []
self._configlist = []
def get_liveconfig_producer(self):
"""provide live account configs, cached on a per-test-process scope
@@ -178,13 +178,13 @@ class TestProcess:
except IndexError:
res = requests.post(liveconfig_opt, timeout=60)
if res.status_code != 200:
pytest.fail(f"newtmpuser count={index} code={res.status_code}: '{res.text}'")
pytest.fail("newtmpuser count={} code={}: '{}'".format(index, res.status_code, res.text))
d = res.json()
config = {"addr": d["email"], "mail_pw": d["password"]}
print("newtmpuser {}: addr={}".format(index, config["addr"]))
self._configlist.append(config)
yield config
pytest.fail(f"more than {MAX_LIVE_CREATED_ACCOUNTS} live accounts requested.")
pytest.fail("more than {} live accounts requested.".format(MAX_LIVE_CREATED_ACCOUNTS))
def cache_maybe_retrieve_configured_db_files(self, cache_addr, db_target_path):
db_target_path = pathlib.Path(db_target_path)
@@ -252,7 +252,7 @@ def data(request):
fn = os.path.join(path, *bn.split("/"))
if os.path.exists(fn):
return fn
print(f"WARNING: path does not exist: {fn!r}")
print("WARNING: path does not exist: {!r}".format(fn))
return None
def read_path(self, bn, mode="r"):
@@ -277,17 +277,15 @@ class ACSetup:
CONFIGURED = "CONFIGURED"
IDLEREADY = "IDLEREADY"
_configured_events: Queue
def __init__(self, testprocess, init_time) -> None:
def __init__(self, testprocess, init_time):
self._configured_events = Queue()
self._account2state: Dict[Account, str] = {}
self._imap_cleaned: Set[str] = set()
self._account2state = {}
self._imap_cleaned = set()
self.testprocess = testprocess
self.init_time = init_time
def log(self, *args):
print("[acsetup]", f"{time.time() - self.init_time:.3f}", *args)
print("[acsetup]", "{:.3f}".format(time.time() - self.init_time), *args)
def add_configured(self, account):
"""add an already configured account."""
@@ -311,7 +309,7 @@ class ACSetup:
def wait_one_configured(self, account):
"""wait until this account has successfully configured."""
if self._account2state[account] == self.CONFIGURING:
while True:
while 1:
acc = self._pop_config_success()
if acc == account:
break
@@ -341,7 +339,7 @@ class ACSetup:
def _pop_config_success(self):
acc, success, comment = self._configured_events.get()
if not success:
pytest.fail(f"configuring online account {acc} failed: {comment}")
pytest.fail("configuring online account {} failed: {}".format(acc, comment))
self._account2state[acc] = self.CONFIGURED
return acc
@@ -375,18 +373,13 @@ class ACSetup:
imap.delete("1:*", expunge=True)
else:
imap.conn.folder.delete(folder)
acc.log(f"imap cleaned for addr {addr}")
acc.log("imap cleaned for addr {}".format(addr))
self._imap_cleaned.add(addr)
class ACFactory:
"""Account factory"""
init_time: float
_finalizers: List[Callable[[], None]]
_accounts: List[Account]
_acsetup: ACSetup
_preconfigured_keys: List[str]
def __init__(self, request, testprocess, tmpdir, data) -> None:
self.init_time = time.time()
@@ -404,7 +397,7 @@ class ACFactory:
request.addfinalizer(self.finalize)
def log(self, *args):
print("[acfactory]", f"{time.time() - self.init_time:.3f}", *args)
print("[acfactory]", "{:.3f}".format(time.time() - self.init_time), *args)
def finalize(self):
while self._finalizers:
@@ -438,16 +431,15 @@ class ACFactory:
assert "addr" in configdict and "mail_pw" in configdict
return configdict
def _get_cached_account(self, addr) -> Optional[Account]:
def _get_cached_account(self, addr):
if addr in self.testprocess._addr2files:
return self._getaccount(addr)
return None
def get_unconfigured_account(self, closed=False) -> Account:
def get_unconfigured_account(self, closed=False):
return self._getaccount(closed=closed)
def _getaccount(self, try_cache_addr=None, closed=False) -> Account:
logid = f"ac{len(self._accounts) + 1}"
def _getaccount(self, try_cache_addr=None, closed=False):
logid = "ac{}".format(len(self._accounts) + 1)
# we need to use fixed database basename for maybe_cache_* functions to work
path = self.tmpdir.mkdir(logid).join("dc.db")
if try_cache_addr:
@@ -460,10 +452,10 @@ class ACFactory:
self._accounts.append(ac)
return ac
def set_logging_default(self, logging) -> None:
def set_logging_default(self, logging):
self._logging = bool(logging)
def remove_preconfigured_keys(self) -> None:
def remove_preconfigured_keys(self):
self._preconfigured_keys = []
def _preconfigure_key(self, account, addr):
@@ -473,12 +465,12 @@ class ACFactory:
except IndexError:
pass
else:
fname_pub = self.data.read_path(f"key/{keyname}-public.asc")
fname_sec = self.data.read_path(f"key/{keyname}-secret.asc")
fname_pub = self.data.read_path("key/{name}-public.asc".format(name=keyname))
fname_sec = self.data.read_path("key/{name}-secret.asc".format(name=keyname))
if fname_pub and fname_sec:
account._preconfigure_keypair(addr, fname_pub, fname_sec)
return True
print(f"WARN: could not use preconfigured keys for {addr!r}")
print("WARN: could not use preconfigured keys for {!r}".format(addr))
def get_pseudo_configured_account(self, passphrase: Optional[str] = None) -> Account:
# do a pseudo-configured account
@@ -486,7 +478,7 @@ class ACFactory:
if passphrase:
ac.open(passphrase)
acname = ac._logid
addr = f"{acname}@offline.org"
addr = "{}@offline.org".format(acname)
ac.update_config(
{
"addr": addr,
@@ -501,7 +493,7 @@ class ACFactory:
self._acsetup.init_logging(ac)
return ac
def new_online_configuring_account(self, cloned_from=None, cache=False, **kwargs) -> Account:
def new_online_configuring_account(self, cloned_from=None, cache=False, **kwargs):
if cloned_from is None:
configdict = self.get_next_liveconfig()
else:
@@ -523,7 +515,7 @@ class ACFactory:
self._acsetup.start_configure(ac)
return ac
def prepare_account_from_liveconfig(self, configdict) -> Account:
def prepare_account_from_liveconfig(self, configdict):
ac = self.get_unconfigured_account()
assert "addr" in configdict and "mail_pw" in configdict, configdict
configdict.setdefault("bcc_self", False)
@@ -533,11 +525,11 @@ class ACFactory:
self._preconfigure_key(ac, configdict["addr"])
return ac
def wait_configured(self, account) -> None:
def wait_configured(self, account):
"""Wait until the specified account has successfully completed configure."""
self._acsetup.wait_one_configured(account)
def bring_accounts_online(self) -> None:
def bring_accounts_online(self):
print("bringing accounts online")
self._acsetup.bring_online()
print("all accounts online")
@@ -640,7 +632,7 @@ class BotProcess:
def _run_stdout_thread(self) -> None:
try:
while True:
while 1:
line = self.popen.stdout.readline()
if not line:
break
@@ -661,7 +653,7 @@ class BotProcess:
for next_pattern in patterns:
print("+++FNMATCH:", next_pattern)
ignored = []
while True:
while 1:
line = self.stdout_queue.get()
if line is None:
if ignored:

View File

@@ -1,25 +1,19 @@
from queue import Queue
from threading import Event
from typing import List, TYPE_CHECKING
from .hookspec import Global, account_hookimpl
if TYPE_CHECKING:
from .events import FFIEvent
class ImexFailed(RuntimeError):
"""Exception for signalling that import/export operations failed."""
class ImexTracker:
_imex_events: Queue
def __init__(self) -> None:
def __init__(self):
self._imex_events = Queue()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event: "FFIEvent") -> None:
def ac_process_ffi_event(self, ffi_event):
if ffi_event.name == "DC_EVENT_IMEX_PROGRESS":
self._imex_events.put(ffi_event.data1)
elif ffi_event.name == "DC_EVENT_IMEX_FILE_WRITTEN":
@@ -44,7 +38,7 @@ class ImexTracker:
if isinstance(ev, str):
files_written.append(ev)
elif ev == 0:
raise ImexFailed(f"export failed, exp-files: {files_written}")
raise ImexFailed("export failed, exp-files: {}".format(files_written))
elif ev == 1000:
return files_written
@@ -56,13 +50,7 @@ class ConfigureFailed(RuntimeError):
class ConfigureTracker:
ConfigureFailed = ConfigureFailed
_configure_events: Queue
_smtp_finished: Event
_imap_finished: Event
_ffi_events: List["FFIEvent"]
_progress: Queue
def __init__(self, account) -> None:
def __init__(self, account):
self.account = account
self._configure_events = Queue()
self._smtp_finished = Event()
@@ -72,7 +60,7 @@ class ConfigureTracker:
self._gm = Global._get_plugin_manager()
@account_hookimpl
def ac_process_ffi_event(self, ffi_event: "FFIEvent") -> None:
def ac_process_ffi_event(self, ffi_event):
self._ffi_events.append(ffi_event)
if ffi_event.name == "DC_EVENT_SMTP_CONNECTED":
self._smtp_finished.set()
@@ -97,7 +85,7 @@ class ConfigureTracker:
self._imap_finished.wait()
def wait_progress(self, data1=None):
while True:
while 1:
evdata = self._progress.get()
if data1 is None or evdata == data1:
break

View File

@@ -14,7 +14,7 @@ def test_db_busy_error(acfactory, tmpdir):
def log(string):
with log_lock:
print(f"{time.time() - starttime:3.2f} {string}")
print("%3.2f %s" % (time.time() - starttime, string))
# make a number of accounts
accounts = acfactory.get_many_online_accounts(3)
@@ -59,15 +59,15 @@ def test_db_busy_error(acfactory, tmpdir):
if report_type == ReportType.exit:
replier.log("EXIT")
elif report_type == ReportType.ffi_error:
replier.log(f"ERROR: {report_args[0]}")
replier.log("ERROR: {}".format(report_args[0]))
elif report_type == ReportType.message_echo:
continue
else:
raise ValueError(f"{addr} unknown report type {report_type}, args={report_args}")
raise ValueError("{} unknown report type {}, args={}".format(addr, report_type, report_args))
alive_count -= 1
replier.log("shutting down")
replier.account.shutdown()
replier.log(f"shut down complete, remaining={alive_count}")
replier.log("shut down complete, remaining={}".format(alive_count))
class ReportType:
@@ -77,7 +77,7 @@ class ReportType:
class AutoReplier:
def __init__(self, account, log, num_send, num_bigfiles, report_func) -> None:
def __init__(self, account, log, num_send, num_bigfiles, report_func):
self.account = account
self._log = log
self.report_func = report_func
@@ -86,12 +86,12 @@ class AutoReplier:
self.current_sent = 0
self.addr = self.account.get_self_contact().addr
self._thread = threading.Thread(name=f"Stats{self.account}", target=self.thread_stats)
self._thread = threading.Thread(name="Stats{}".format(self.account), target=self.thread_stats)
self._thread.setDaemon(True)
self._thread.start()
def log(self, message) -> None:
self._log(f"{self.addr} {message}")
def log(self, message):
self._log("{} {}".format(self.addr, message))
def thread_stats(self):
# XXX later use, for now we just quit
@@ -107,17 +107,17 @@ class AutoReplier:
return
message.create_chat()
message.mark_seen()
self.log(f"incoming message: {message}")
self.log("incoming message: {}".format(message))
self.current_sent += 1
# we are still alive, let's send a reply
if self.num_bigfiles and self.current_sent % (self.num_send / self.num_bigfiles) == 0:
message.chat.send_text(f"send big file as reply to: {message.text}")
message.chat.send_text("send big file as reply to: {}".format(message.text))
msg = message.chat.send_file(self.account.bigfile)
else:
msg = message.chat.send_text(f"got message id {message.id}, small text reply")
msg = message.chat.send_text("got message id {}, small text reply".format(message.id))
assert msg.text
self.log(f"message-sent: {msg}")
self.log("message-sent: {}".format(msg))
self.report_func(self, ReportType.message_echo)
if self.current_sent >= self.num_send:
self.report_func(self, ReportType.exit)

View File

@@ -239,7 +239,7 @@ def test_fetch_existing(acfactory, lp, mvbox_move):
ac1_clone.start_io()
assert_folders_configured(ac1_clone)
lp.sec("check that ac2 contact was fetched during configure")
lp.sec("check that ac2 contact was fetchted during configure")
ac1_clone._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
ac2_addr = ac2.get_config("addr")
assert any(c.addr == ac2_addr for c in ac1_clone.get_contacts())
@@ -537,54 +537,3 @@ def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp):
msg_in = ac1_offl._evtracker.wait_next_incoming_message()
assert msg_in.text == msg_out.text
assert msg_in.get_sender_contact().addr == ac2_addr
ac1.set_config("bcc_self", "0")
def test_use_new_verified_group_after_going_online(acfactory, tmpdir, lp):
"""Another test for the bug #3836:
- Bob has two devices, the second is offline.
- Alice creates a verified group and sends a QR invitation to Bob.
- Bob joins the group.
- Bob's second devices goes online, but sees a contact request instead of the verified group.
- The "member added" message is not a system message but a plain text message.
- Sending a message fails as the key is missing -- message info says "proper enc-key for <Alice>
missing, cannot encrypt".
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2)
for ac in [ac2, ac2_offl]:
ac.set_config("bcc_self", "1")
acfactory.bring_accounts_online()
dir = tmpdir.mkdir("exportdir")
ac2.export_self_keys(dir.strpath)
ac2_offl.import_self_keys(dir.strpath)
ac2_offl.stop_io()
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
chat = ac1.create_group_chat("hello", verified=True)
assert chat.is_protected()
qr = chat.get_join_qr()
lp.sec("ac2: start QR-code based join-group protocol")
ac2.qr_join_chat(qr)
ac1._evtracker.wait_securejoin_inviter_progress(1000)
lp.sec("ac2_offl: going online, checking the 'member added' message")
ac2_offl.start_io()
# Receive "Member Me (<addr>) added by <addr>." message.
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
assert msg_in.is_system_message()
assert msg_in.get_sender_contact().addr == ac1.get_config("addr")
chat2 = msg_in.chat
assert chat2.is_protected()
lp.sec("ac2_offl: sending message")
msg_out = chat2.send_text("hello")
lp.sec("ac1: receiving message")
msg_in = ac1._evtracker.wait_next_incoming_message()
assert msg_in.chat == chat
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
assert msg_in.text == msg_out.text
ac2.set_config("bcc_self", "0")

View File

@@ -72,19 +72,6 @@ def test_configure_canceled(acfactory):
pass
def test_configure_unref(tmpdir):
"""Test that removing the last reference to the context during ongoing configuration
does not result in use-after-free."""
from deltachat.capi import ffi, lib
path = tmpdir.mkdir("test_configure_unref").join("dc.db").strpath
dc_context = lib.dc_context_new(ffi.NULL, path.encode("utf8"), ffi.NULL)
lib.dc_set_config(dc_context, "addr".encode("utf8"), "foo@x.testrun.org".encode("utf8"))
lib.dc_set_config(dc_context, "mail_pw".encode("utf8"), "abc".encode("utf8"))
lib.dc_configure(dc_context)
lib.dc_context_unref(dc_context)
def test_export_import_self_keys(acfactory, tmpdir, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
@@ -217,13 +204,13 @@ def test_html_message(acfactory, lp):
lp.sec("ac1: prepare and send text message to ac2")
msg1 = chat.send_text("message0")
assert not msg1.has_html()
assert not msg1.html
assert msg1.html == ""
lp.sec("wait for ac2 to receive message")
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.text == "message0"
assert not msg2.has_html()
assert not msg2.html
assert msg2.html == ""
lp.sec("ac1: prepare and send HTML+text message to ac2")
msg1 = Message.new_empty(ac1, "text")
@@ -1262,7 +1249,7 @@ def test_send_mark_seen_clean_incoming_events(acfactory, lp):
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
lp.sec(f"ac2: mark seen {msg}")
lp.sec("ac2: mark seen {}".format(msg))
msg.mark_seen()
for ev in ac1._evtracker.iter_events():
@@ -1450,8 +1437,9 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
backupdir = tmpdir.mkdir("backup")
lp.sec(f"export all to {backupdir}")
lp.sec("export all to {}".format(backupdir))
with ac1.temp_plugin(ImexTracker()) as imex_tracker:
ac1.stop_io()
ac1.imex(backupdir.strpath, const.DC_IMEX_EXPORT_BACKUP)
@@ -1487,7 +1475,7 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
assert_account_is_proper(ac1)
assert_account_is_proper(ac2)
lp.sec(f"Second-time export all to {backupdir}")
lp.sec("Second-time export all to {}".format(backupdir))
ac1.stop_io()
path2 = ac1.export_all(backupdir.strpath)
assert os.path.exists(path2)
@@ -1497,7 +1485,7 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
def test_ac_setup_message(acfactory, lp):
# note that the receiving account needs to be configured and running
# before the setup message is send. DC does not read old messages
# before ther setup message is send. DC does not read old messages
# as of Jul2019
ac1 = acfactory.new_online_configuring_account()
ac2 = acfactory.new_online_configuring_account(cloned_from=ac1)
@@ -1617,7 +1605,7 @@ def test_add_remove_member_remote_events(acfactory, lp):
in_list = queue.Queue()
class EventHolder:
def __init__(self, **kwargs) -> None:
def __init__(self, **kwargs):
self.__dict__.update(kwargs)
class InPlugin:
@@ -1967,7 +1955,6 @@ def test_immediate_autodelete(acfactory, lp):
assert msg.text == "hello"
lp.sec("ac2: wait for close/expunge on autodelete")
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_info_contains("close/expunge succeeded")
lp.sec("ac2: check that message was autodeleted on server")
@@ -2000,48 +1987,13 @@ def test_delete_multiple_messages(acfactory, lp):
lp.sec("ac2: deleting all messages except third")
assert len(to_delete) == len(texts) - 1
ac2.delete_messages(to_delete)
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_info_contains("close/expunge succeeded")
lp.sec("ac2: test that only one message is left")
while 1:
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
ac2._evtracker.get_info_contains("close/expunge succeeded")
ac2.direct_imap.select_config_folder("inbox")
nr_msgs = len(ac2.direct_imap.get_all_messages())
assert nr_msgs > 0
if nr_msgs == 1:
break
def test_trash_multiple_messages(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
ac2.set_config("delete_to_trash", "1")
chat12 = acfactory.get_accepted_chat(ac1, ac2)
lp.sec("ac1: sending 3 messages")
texts = ["first", "second", "third"]
for text in texts:
chat12.send_text(text)
lp.sec("ac2: waiting for all messages on the other side")
to_delete = []
for text in texts:
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text in texts
if text != "second":
to_delete.append(msg)
lp.sec("ac2: deleting all messages except second")
assert len(to_delete) == len(texts) - 1
ac2.delete_messages(to_delete)
lp.sec("ac2: test that only one message is left")
while 1:
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_MOVED")
ac2.direct_imap.select_config_folder("inbox")
nr_msgs = len(ac2.direct_imap.get_all_messages())
assert nr_msgs > 0
if nr_msgs == 1:
break
ac2.direct_imap.select_config_folder("inbox")
assert len(ac2.direct_imap.get_all_messages()) == 1
def test_configure_error_msgs_wrong_pw(acfactory):
@@ -2150,7 +2102,7 @@ def test_status(acfactory):
chat12.send_text("hello")
msg = ac2._evtracker.wait_next_incoming_message()
assert msg.text == "hello"
assert not msg.get_sender_contact().status
assert msg.get_sender_contact().status == ""
def test_group_quote(acfactory, lp):

View File

@@ -1,3 +1,5 @@
from __future__ import print_function
import os.path
import shutil
from filecmp import cmp
@@ -15,7 +17,7 @@ def wait_msg_delivered(account, msg_list):
def wait_msgs_changed(account, msgs_list):
"""wait for one or more MSGS_CHANGED events to match msgs_list contents."""
account.log(f"waiting for msgs_list={msgs_list}")
account.log("waiting for msgs_list={}".format(msgs_list))
msgs_list = list(msgs_list)
while msgs_list:
ev = account._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
@@ -25,7 +27,7 @@ def wait_msgs_changed(account, msgs_list):
del msgs_list[i]
break
else:
account.log(f"waiting mismatch data1={data1} data2={data2}")
account.log("waiting mismatch data1={} data2={}".format(data1, data2))
return ev.data1, ev.data2

View File

@@ -1,3 +1,5 @@
from __future__ import print_function
import os
import time
from datetime import datetime, timedelta, timezone
@@ -295,8 +297,8 @@ class TestOfflineChat:
assert d["archived"] == chat.is_archived()
# assert d["param"] == chat.param
assert d["color"] == chat.get_color()
assert not d["profile_image"] if chat.get_profile_image() is None else chat.get_profile_image()
assert not d["draft"] if chat.get_draft() is None else chat.get_draft()
assert d["profile_image"] == "" if chat.get_profile_image() is None else chat.get_profile_image()
assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft()
def test_group_chat_creation_with_translation(self, ac1):
ac1.set_stock_translation(const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s")
@@ -742,7 +744,7 @@ class TestOfflineChat:
contacts = []
for i in range(10):
lp.sec("create contact")
contact = ac1.create_contact(f"some{i}@example.org")
contact = ac1.create_contact("some{}@example.org".format(i))
contacts.append(contact)
lp.sec("add contact")
chat.add_contact(contact)

View File

@@ -151,7 +151,7 @@ def test_markseen_invalid_message_ids(acfactory):
ac1 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact("some1@example.com", name="some1")
chat = contact1.create_chat()
chat.send_text("one message")
chat.send_text("one messae")
ac1._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
msg_ids = [9]
lib.dc_markseen_msgs(ac1._dc_context, msg_ids, len(msg_ids))

View File

@@ -8,7 +8,7 @@ envlist =
[testenv]
commands =
pytest -n6 --exitfirst --extra-info -v -rsXx --ignored --strict-tls {posargs: tests examples}
pytest -n6 --extra-info --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
pip wheel . -w {toxworkdir}/wheelhouse --no-deps
setenv =
# Avoid stack overflow when Rust core is built without optimizations.
@@ -21,6 +21,7 @@ passenv =
RUSTC_WRAPPER
deps =
pytest
pytest-rerunfailures
pytest-timeout
pytest-xdist
pdbpp
@@ -55,7 +56,7 @@ deps =
pygments
restructuredtext_lint
commands =
black --quiet --check --diff setup.py install_python_bindings.py src/deltachat examples/ tests/
black --check setup.py install_python_bindings.py src/deltachat examples/ tests/
ruff src/deltachat tests/ examples/
rst-lint --encoding 'utf-8' README.rst

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