mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 23:52:11 +03:00
Compare commits
339 Commits
link2xt/li
...
v1.126.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
03bb92c942 | ||
|
|
b0da5a54cc | ||
|
|
44e056e210 | ||
|
|
48a8680ba4 | ||
|
|
b73bcc2c22 | ||
|
|
cff42936aa | ||
|
|
d3b04004b4 | ||
|
|
5cd92f10ef | ||
|
|
22a3ab983b | ||
|
|
83d2e6b8b4 | ||
|
|
4e979c5880 | ||
|
|
71e1089139 | ||
|
|
349c154a99 | ||
|
|
4e08bb7b05 | ||
|
|
934ca6a7d7 | ||
|
|
e878caebe3 | ||
|
|
088eda2983 | ||
|
|
418cd24979 | ||
|
|
b4fe9e3eec | ||
|
|
c13bbd05cd | ||
|
|
58330fe8b2 | ||
|
|
680d024b05 | ||
|
|
defcd5764b | ||
|
|
0227bbc305 | ||
|
|
d05afec289 | ||
|
|
64035d3ecb | ||
|
|
21e0bb28ad | ||
|
|
c6358169ad | ||
|
|
955f4fbb19 | ||
|
|
df7c44ae42 | ||
|
|
8573649bf7 | ||
|
|
52c46c6dca | ||
|
|
54ea3ec5d6 | ||
|
|
b239535964 | ||
|
|
da5d844ec4 | ||
|
|
a87635dcf4 | ||
|
|
e30517e62c | ||
|
|
a54f3c4b31 | ||
|
|
bda6cea0ce | ||
|
|
6fece09ed7 | ||
|
|
3917c6b2f0 | ||
|
|
96a89b5bdc | ||
|
|
b55027fe71 | ||
|
|
eacbb82399 | ||
|
|
ee279f84ad | ||
|
|
26959d5b75 | ||
|
|
ff5005fa93 | ||
|
|
8f316e12d5 | ||
|
|
5f00fc4e27 | ||
|
|
f279730b0f | ||
|
|
5a5f8b03d1 | ||
|
|
5e73e9cd72 | ||
|
|
129de9182f | ||
|
|
09798df7a0 | ||
|
|
b360225e08 | ||
|
|
09d5e44b13 | ||
|
|
8ba89c0fa1 | ||
|
|
f984a27379 | ||
|
|
425a2310fe | ||
|
|
95571be278 | ||
|
|
7bf44a237e | ||
|
|
47dbac9b50 | ||
|
|
a49282727b | ||
|
|
0d22fc7ac1 | ||
|
|
1040bc551f | ||
|
|
5aa0205c80 | ||
|
|
210a4ebcbe | ||
|
|
7fc2b06b3f | ||
|
|
1177c19a43 | ||
|
|
8a2417f32d | ||
|
|
a154347834 | ||
|
|
d51adf2aa0 | ||
|
|
a5f0c1613e | ||
|
|
7c4bcf9004 | ||
|
|
2dd44d5f89 | ||
|
|
f656cb29be | ||
|
|
53230b6eb0 | ||
|
|
80ca59f152 | ||
|
|
eb624e43c0 | ||
|
|
532e9cb09a | ||
|
|
ef4d2a7ed0 | ||
|
|
6d2ac30461 | ||
|
|
33a203d56e | ||
|
|
a19811f379 | ||
|
|
f23023961e | ||
|
|
b463a0566e | ||
|
|
38d5743c06 | ||
|
|
6990312051 | ||
|
|
a7cf51868b | ||
|
|
815c1b9c49 | ||
|
|
88bba83383 | ||
|
|
b1d517398d | ||
|
|
4e5b41f150 | ||
|
|
56b2361f01 | ||
|
|
968cc65323 | ||
|
|
d0ee21e6dc | ||
|
|
a1345f2542 | ||
|
|
f290fe0871 | ||
|
|
aa78e82fed | ||
|
|
d4e670d5e9 | ||
|
|
4553c6521f | ||
|
|
e72d527d88 | ||
|
|
f5c36043f6 | ||
|
|
b227ff87dc | ||
|
|
676f311f97 | ||
|
|
f02299c06c | ||
|
|
ed781af52c | ||
|
|
67043177a9 | ||
|
|
49cc5fb673 | ||
|
|
68c95dee17 | ||
|
|
9bd7ab7280 | ||
|
|
7a359f6318 | ||
|
|
38b31aa88d | ||
|
|
e12e026bd8 | ||
|
|
212fbc125c | ||
|
|
2939de013b | ||
|
|
0562e23ee0 | ||
|
|
a8551510cd | ||
|
|
087f6edd0c | ||
|
|
d6b7ee04a0 | ||
|
|
d5c5ff8b3f | ||
|
|
dc4396a699 | ||
|
|
a74b00c3f9 | ||
|
|
2fdb9f8b7e | ||
|
|
80fac3f1b8 | ||
|
|
17a6c88cc7 | ||
|
|
1ba69dbb9b | ||
|
|
ab1c7ebbe2 | ||
|
|
ee715da078 | ||
|
|
27e177dc05 | ||
|
|
7aac4bfc83 | ||
|
|
7b24f9b7a4 | ||
|
|
b36b902eeb | ||
|
|
30024abb6c | ||
|
|
1d9702e9e7 | ||
|
|
ee2eae63d6 | ||
|
|
cd477936b5 | ||
|
|
dbe9d7e34e | ||
|
|
49f143e0d5 | ||
|
|
9d7bdf369d | ||
|
|
a270db1d87 | ||
|
|
7c7cd9cc80 | ||
|
|
47d465e6e4 | ||
|
|
03d3e0578f | ||
|
|
440a442f30 | ||
|
|
1da52d7d1d | ||
|
|
4d74f625d3 | ||
|
|
0a94fbc735 | ||
|
|
9ef34890fa | ||
|
|
3e07f2c173 | ||
|
|
ee28298d7f | ||
|
|
62aed13880 | ||
|
|
bffe934acc | ||
|
|
87ffcaf03e | ||
|
|
2635146328 | ||
|
|
d727d85f6d | ||
|
|
81a7af10c7 | ||
|
|
4a6e94f8ab | ||
|
|
146fe50e20 | ||
|
|
9bf2850fb1 | ||
|
|
ba2c36548e | ||
|
|
d07c743cdc | ||
|
|
d70c1d48b5 | ||
|
|
a8e0cb9b5a | ||
|
|
6ea9a8988b | ||
|
|
45e35b3571 | ||
|
|
e43f9066d8 | ||
|
|
bba6c8f15a | ||
|
|
55aaec744a | ||
|
|
2f24eddb7d | ||
|
|
a33c91afa9 | ||
|
|
d52f2883cf | ||
|
|
b872953bc5 | ||
|
|
c1cb6eef08 | ||
|
|
200b808c27 | ||
|
|
d572d960e5 | ||
|
|
5db75128ba | ||
|
|
fbd2fc8ead | ||
|
|
bc73c16df7 | ||
|
|
0a50bad555 | ||
|
|
82c0058129 | ||
|
|
1bd307a26a | ||
|
|
740f43a2d6 | ||
|
|
c14f45a8f5 | ||
|
|
8269116dba | ||
|
|
db941ccf88 | ||
|
|
a464cbdfe6 | ||
|
|
ea4a0530b8 | ||
|
|
9d3b2d4844 | ||
|
|
c312280ab3 | ||
|
|
572b99a2e1 | ||
|
|
3992b5a063 | ||
|
|
b97cb4b55e | ||
|
|
64c218f1ea | ||
|
|
deed790950 | ||
|
|
b33ae3cd0f | ||
|
|
9480699362 | ||
|
|
94c190e844 | ||
|
|
578e47666f | ||
|
|
7eeced50d1 | ||
|
|
46e127ad27 | ||
|
|
4891849e28 | ||
|
|
e0dd83d538 | ||
|
|
aac8bb950c | ||
|
|
bf21796bc0 | ||
|
|
9cbf413064 | ||
|
|
1b57eb4d8d | ||
|
|
5152e702bd | ||
|
|
c80f1a1997 | ||
|
|
88759c815b | ||
|
|
9c68fac4b6 | ||
|
|
8e17e400b3 | ||
|
|
dae3857db8 | ||
|
|
695f71e124 | ||
|
|
2d30afd212 | ||
|
|
5fe94e8bce | ||
|
|
1351f71632 | ||
|
|
d42322b38b | ||
|
|
ce6876c418 | ||
|
|
2a6b7d9766 | ||
|
|
fa1924da2b | ||
|
|
d5214eb192 | ||
|
|
c47324d671 | ||
|
|
3f8ec5ec56 | ||
|
|
fab504b54c | ||
|
|
dd32430ade | ||
|
|
eb943625a6 | ||
|
|
32ac4a01ca | ||
|
|
f01a9d7d5c | ||
|
|
a5db7104c2 | ||
|
|
18aeb14003 | ||
|
|
4ad2d6e340 | ||
|
|
ce9cd54993 | ||
|
|
23f540f9f9 | ||
|
|
f994b2d8e4 | ||
|
|
6e42b85a36 | ||
|
|
d69e42377d | ||
|
|
de9330b52f | ||
|
|
01d1c4c04b | ||
|
|
7d98978269 | ||
|
|
5024f48609 | ||
|
|
e975568122 | ||
|
|
1f71c69325 | ||
|
|
b80ec8507c | ||
|
|
3a3f3542d9 | ||
|
|
657c5fa947 | ||
|
|
7d0b25c209 | ||
|
|
8d26303cad | ||
|
|
0d8a76593a | ||
|
|
7b49fb2eb6 | ||
|
|
efa37dd283 | ||
|
|
323e44da04 | ||
|
|
70efd0f10a | ||
|
|
fcec81b4c1 | ||
|
|
dd806b2d88 | ||
|
|
5659c1b9c2 | ||
|
|
d538d29b94 | ||
|
|
b4209fac2e | ||
|
|
4d6dfa120e | ||
|
|
f92108be1d | ||
|
|
00cb72f04d | ||
|
|
92e34d67e6 | ||
|
|
65bff8339f | ||
|
|
768f8175e6 | ||
|
|
c3f352aff1 | ||
|
|
5ac2d1b8cb | ||
|
|
8214b2b8c1 | ||
|
|
53ab8a3b35 | ||
|
|
cbe1671104 | ||
|
|
0d0e223238 | ||
|
|
4767f1ce74 | ||
|
|
1a62b6d77f | ||
|
|
915008d474 | ||
|
|
9646766793 | ||
|
|
e948ec3256 | ||
|
|
9ab9d2eb7b | ||
|
|
437f8c48c4 | ||
|
|
e6d9a49187 | ||
|
|
33a014eea4 | ||
|
|
9be871ccf6 | ||
|
|
6eb8abe535 | ||
|
|
91bf87fa80 | ||
|
|
a2599ef08a | ||
|
|
22d0a4bb32 | ||
|
|
7a160033b6 | ||
|
|
3442748be7 | ||
|
|
d451bcfbe3 | ||
|
|
b2993242e4 | ||
|
|
5eaa9eeed2 | ||
|
|
3ed2ac8f0c | ||
|
|
0145203f7b | ||
|
|
59588b319e | ||
|
|
f917c7de6b | ||
|
|
84888fa4c4 | ||
|
|
e0b1644488 | ||
|
|
4beba8ce3c | ||
|
|
bc521a685d | ||
|
|
33caa0f499 | ||
|
|
033ce41c0f | ||
|
|
88a62e1f6e | ||
|
|
dd30f6ab7d | ||
|
|
140d116d98 | ||
|
|
d96b783909 | ||
|
|
572c7f2efb | ||
|
|
bcd6c226f6 | ||
|
|
bae61746f8 | ||
|
|
31f2766074 | ||
|
|
b06c8baa9c | ||
|
|
1e479fe4a3 | ||
|
|
8ea8ee02ed | ||
|
|
55bc556bcf | ||
|
|
3b6d21301b | ||
|
|
472195c7d9 | ||
|
|
afb8b5ce55 | ||
|
|
de3c82ef43 | ||
|
|
4255ae4c2d | ||
|
|
4b4e2f700e | ||
|
|
81fde5c680 | ||
|
|
5340a7d033 | ||
|
|
fc82d728fc | ||
|
|
136e9179e9 | ||
|
|
31e19ca56c | ||
|
|
f2b02b7bb0 | ||
|
|
646ace8e7a | ||
|
|
a2495716b6 | ||
|
|
0f579c6415 | ||
|
|
3eddc9164c | ||
|
|
dd29fae49b | ||
|
|
5b435d11c7 | ||
|
|
b9b0d20e8d | ||
|
|
c68a2e3820 | ||
|
|
c7ad0b1f4f | ||
|
|
0dd9e3a77e | ||
|
|
d27e3d085e | ||
|
|
10b2aa5350 | ||
|
|
3a29a555bf | ||
|
|
f024f396bf | ||
|
|
24d52c5909 | ||
|
|
f9dc8edbcb |
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -2,6 +2,14 @@
|
||||
# ensures this even if the user has not set core.autocrlf.
|
||||
* text=auto
|
||||
|
||||
# Checkout JavaScript files with LF line endings
|
||||
# to prevent `prettier` from reporting errors on Windows.
|
||||
*.js eol=lf
|
||||
*.jsx eol=lf
|
||||
*.ts eol=lf
|
||||
*.tsx eol=lf
|
||||
*.json eol=lf
|
||||
|
||||
# This directory contains email messages verbatim, and changing CRLF to
|
||||
# LF will corrupt them.
|
||||
test-data/** text=false
|
||||
|
||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -5,5 +5,5 @@ updates:
|
||||
schedule:
|
||||
interval: "monthly"
|
||||
commit-message:
|
||||
prefix: "cargo"
|
||||
prefix: "chore(cargo)"
|
||||
open-pull-requests-limit: 50
|
||||
|
||||
15
.github/mergeable.yml
vendored
15
.github/mergeable.yml
vendored
@@ -1,15 +0,0 @@
|
||||
version: 2
|
||||
mergeable:
|
||||
- when: pull_request.*
|
||||
name: "Conventional Commits"
|
||||
validate:
|
||||
- do: title
|
||||
begins_with:
|
||||
match: ['feat', 'fix', 'api', 'refactor', 'perf', 'test', 'style', 'chore', 'cargo', 'build', 'ci']
|
||||
|
||||
fail:
|
||||
- do: checks
|
||||
status: "action_required"
|
||||
payload:
|
||||
title: PR title should follow conventional commits
|
||||
summary: "PR title should follow https://conventionalcommits.org. E.g. start with 'feat:' (for Features / Changes), 'fix:' (for Fixes), 'api:' (for API-Changes), 'api!:' (for breaking API-Changes) 'refactor:' (for Refactor), 'perf:' (for Performance), 'test:' (for Tests), 'style:' (for Styling), 'chore:' (for Miscellaneous Tasks)"
|
||||
32
.github/workflows/ci.yml
vendored
32
.github/workflows/ci.yml
vendored
@@ -1,3 +1,7 @@
|
||||
# GitHub Actions workflow to
|
||||
# lint Rust and Python code
|
||||
# and run Rust tests, Python tests and async Python tests.
|
||||
|
||||
name: Rust CI
|
||||
|
||||
# Cancel previously started workflow runs
|
||||
@@ -11,6 +15,7 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- stable
|
||||
|
||||
env:
|
||||
RUSTFLAGS: -Dwarnings
|
||||
@@ -20,7 +25,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.68.2
|
||||
RUSTUP_TOOLCHAIN: 1.73.0
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install rustfmt and clippy
|
||||
@@ -72,11 +77,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.68.2
|
||||
rust: 1.73.0
|
||||
- os: windows-latest
|
||||
rust: 1.68.2
|
||||
rust: 1.73.0
|
||||
- os: macos-latest
|
||||
rust: 1.68.2
|
||||
rust: 1.73.0
|
||||
|
||||
# Minimum Supported Rust Version = 1.65.0
|
||||
#
|
||||
@@ -173,15 +178,15 @@ jobs:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.11
|
||||
python: 3.12
|
||||
- os: macos-latest
|
||||
python: 3.11
|
||||
python: 3.12
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
python: pypy3.9
|
||||
python: pypy3.10
|
||||
- os: macos-latest
|
||||
python: pypy3.9
|
||||
python: pypy3.10
|
||||
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
@@ -222,21 +227,18 @@ jobs:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.11
|
||||
python: 3.12
|
||||
- os: macos-latest
|
||||
python: 3.11
|
||||
python: 3.12
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
python: pypy3.9
|
||||
python: pypy3.10
|
||||
- os: macos-latest
|
||||
python: pypy3.9
|
||||
python: pypy3.10
|
||||
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built. Test it with minimum supported Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.7
|
||||
|
||||
|
||||
112
.github/workflows/deltachat-rpc-server.yml
vendored
112
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -1,4 +1,10 @@
|
||||
# Manually triggered action to build deltachat-rpc-server binaries.
|
||||
# GitHub Actions workflow
|
||||
# to build `deltachat-rpc-server` binaries
|
||||
# and upload them to the release.
|
||||
#
|
||||
# The workflow is automatically triggered on releases.
|
||||
# It can also be triggered manually
|
||||
# to produce binary artifacts for testing.
|
||||
|
||||
name: Build deltachat-rpc-server binaries
|
||||
|
||||
@@ -20,35 +26,17 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Build
|
||||
- name: Install ziglang
|
||||
run: pip install wheel ziglang==0.11.0
|
||||
|
||||
- name: Build deltachat-rpc-server binaries
|
||||
run: sh scripts/zig-rpc-server.sh
|
||||
|
||||
- name: Upload x86_64 binary
|
||||
- name: Upload dist directory with Linux binaries
|
||||
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 i686 binary
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-i686
|
||||
path: target/i686-unknown-linux-musl/release/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload aarch64 binary
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64
|
||||
path: target/aarch64-unknown-linux-musl/release/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload armv7 binary
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-armv7
|
||||
path: target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server
|
||||
name: linux
|
||||
path: dist/
|
||||
if-no-files-found: error
|
||||
|
||||
build_windows:
|
||||
@@ -86,39 +74,87 @@ jobs:
|
||||
|
||||
build_macos:
|
||||
name: Build deltachat-rpc-server for macOS
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
include:
|
||||
- arch: x86_64
|
||||
- arch: aarch64
|
||||
|
||||
runs-on: macos-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Setup rust target
|
||||
run: rustup target add x86_64-apple-darwin
|
||||
run: rustup target add ${{ matrix.arch }}-apple-darwin
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release --package deltachat-rpc-server --target x86_64-apple-darwin --features vendored
|
||||
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
||||
|
||||
- name: Upload binary
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-macos
|
||||
path: target/x86_64-apple-darwin/release/deltachat-rpc-server
|
||||
name: deltachat-rpc-server-${{ matrix.arch }}-macos
|
||||
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
name: Upload binaries to the release
|
||||
name: Build wheels and upload binaries to the release
|
||||
needs: ["build_linux", "build_windows", "build_macos"]
|
||||
permissions:
|
||||
contents: write
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: Download built binaries
|
||||
uses: "actions/download-artifact@v3"
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
- name: Compose dist/ directory
|
||||
- name: Download Linux binaries
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: linux
|
||||
path: dist/
|
||||
|
||||
- name: Download win32 binary
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-win32.exe
|
||||
path: deltachat-rpc-server-win32.exe.d
|
||||
|
||||
- name: Download win64 binary
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-win64.exe
|
||||
path: deltachat-rpc-server-win64.exe.d
|
||||
|
||||
- name: Download macOS binary for x86_64
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-x86_64-macos
|
||||
path: deltachat-rpc-server-x86_64-macos.d
|
||||
|
||||
- name: Download macOS binary for aarch64
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: deltachat-rpc-server-aarch64-macos
|
||||
path: deltachat-rpc-server-aarch64-macos.d
|
||||
|
||||
- name: Flatten dist/ directory
|
||||
run: |
|
||||
mkdir dist
|
||||
for x in x86_64 i686 aarch64 armv7 win32.exe win64.exe x86_64-macos; do
|
||||
mv "deltachat-rpc-server-$x"/* "dist/deltachat-rpc-server-$x"
|
||||
done
|
||||
mv deltachat-rpc-server-win32.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win32.exe
|
||||
mv deltachat-rpc-server-win64.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win64.exe
|
||||
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-macos
|
||||
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-macos
|
||||
|
||||
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
|
||||
- name: Install python 3.12
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: 3.12
|
||||
|
||||
- name: Install wheel
|
||||
run: pip install wheel
|
||||
|
||||
- name: Build deltachat-rpc-server Python wheels and source package
|
||||
run: scripts/wheel-rpc-server.py
|
||||
|
||||
- name: List downloaded artifacts
|
||||
run: ls -l dist/
|
||||
|
||||
3
.github/workflows/dependabot.yml
vendored
3
.github/workflows/dependabot.yml
vendored
@@ -1,3 +1,6 @@
|
||||
# GitHub Actions workflow
|
||||
# to automatically approve PRs made by Dependabot.
|
||||
|
||||
name: Dependabot auto-approve
|
||||
on: pull_request
|
||||
|
||||
|
||||
@@ -38,13 +38,12 @@ jobs:
|
||||
node --version
|
||||
echo $DELTACHAT_JSONRPC_TAR_GZ
|
||||
- name: Install dependencies without running scripts
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm install --ignore-scripts
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm install --ignore-scripts
|
||||
- name: Package
|
||||
shell: bash
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm run build
|
||||
npm pack .
|
||||
ls -lah
|
||||
|
||||
25
.github/workflows/jsonrpc.yml
vendored
25
.github/workflows/jsonrpc.yml
vendored
@@ -22,24 +22,19 @@ jobs:
|
||||
- name: Add Rust cache
|
||||
uses: Swatinem/rust-cache@v2
|
||||
- name: npm install
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm install
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm install
|
||||
- name: Build TypeScript, run Rust tests, generate bindings
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm run build
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm run build
|
||||
- name: Run integration tests
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm run test
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: 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
|
||||
working-directory: deltachat-jsonrpc
|
||||
run: cargo build --bin deltachat-jsonrpc-server --features webserver
|
||||
- name: Run linter
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm run prettier:check
|
||||
working-directory: deltachat-jsonrpc/typescript
|
||||
run: npm run prettier:check
|
||||
|
||||
31
.github/workflows/node-delete-preview.yml
vendored
31
.github/workflows/node-delete-preview.yml
vendored
@@ -1,31 +0,0 @@
|
||||
# documentation: https://github.com/deltachat/sysadmin/tree/master/download.delta.chat
|
||||
name: Delete node PR previews
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
delete:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Get Pull Request ID
|
||||
id: getid
|
||||
run: |
|
||||
export PULLREQUEST_ID=$(jq .number < $GITHUB_EVENT_PATH)
|
||||
echo "prid=$PULLREQUEST_ID" >> $GITHUB_OUTPUT
|
||||
- name: Renaming
|
||||
run: |
|
||||
# create empty file to copy it over the outdated deliverable on download.delta.chat
|
||||
echo "This preview build is outdated and has been removed." > empty
|
||||
cp empty deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz
|
||||
- name: Replace builds with dummy files
|
||||
uses: horochx/deploy-via-scp@v1.0.1
|
||||
with:
|
||||
user: ${{ secrets.USERNAME }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
host: "download.delta.chat"
|
||||
port: 22
|
||||
local: "deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz"
|
||||
remote: "/var/www/html/download/node/preview/"
|
||||
7
.github/workflows/node-docs.yml
vendored
7
.github/workflows/node-docs.yml
vendored
@@ -1,3 +1,8 @@
|
||||
# GitHub Actions workflow to build
|
||||
# Node.js bindings documentation
|
||||
# and upload it to the web server.
|
||||
# Built documentation is available at <https://js.delta.chat/>
|
||||
|
||||
name: Generate & upload node.js documentation
|
||||
|
||||
on:
|
||||
@@ -17,8 +22,8 @@ jobs:
|
||||
node-version: 16.x
|
||||
|
||||
- name: npm install and generate documentation
|
||||
working-directory: node
|
||||
run: |
|
||||
cd node
|
||||
npm i --ignore-scripts
|
||||
npx typedoc
|
||||
mv docs js
|
||||
|
||||
92
.github/workflows/node-package.yml
vendored
92
.github/workflows/node-package.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-20.04, macos-latest, windows-latest]
|
||||
os: [macos-latest, windows-latest]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
@@ -46,13 +46,12 @@ jobs:
|
||||
|
||||
- name: Install dependencies & build
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd node
|
||||
npm install --verbose
|
||||
working-directory: node
|
||||
run: npm install --verbose
|
||||
|
||||
- name: Build Prebuild
|
||||
working-directory: node
|
||||
run: |
|
||||
cd node
|
||||
npm run prebuildify
|
||||
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
|
||||
|
||||
@@ -62,10 +61,81 @@ jobs:
|
||||
name: ${{ matrix.os }}
|
||||
path: node/${{ matrix.os }}.tar.gz
|
||||
|
||||
prebuild-linux:
|
||||
name: Prebuild Linux
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
# Build Linux prebuilds inside a container with old glibc for backwards compatibility.
|
||||
# Debian 10 contained glibc 2.28 at the time of the writing (2023-06-04): https://packages.debian.org/buster/libc6
|
||||
# Ubuntu 18.04 is at the End of Standard Support since June 2023, but it contains glibc 2.27,
|
||||
# so we are using it to support Ubuntu 18.04 setups that are still not upgraded.
|
||||
container: ubuntu:18.04
|
||||
steps:
|
||||
# Working directory is owned by 1001:1001 by default.
|
||||
# Change it to our user.
|
||||
- name: Change working directory owner
|
||||
run: chown root:root .
|
||||
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: "16"
|
||||
- run: apt-get update
|
||||
|
||||
# Python is needed for node-gyp
|
||||
- name: Install curl, python and compilers
|
||||
run: apt-get install -y curl build-essential python3
|
||||
- name: Install Rust
|
||||
run: |
|
||||
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||
- name: System info
|
||||
run: |
|
||||
rustc -vV
|
||||
rustup -vV
|
||||
cargo -vV
|
||||
npm --version
|
||||
node --version
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
${{ env.APPDATA }}/npm-cache
|
||||
~/.npm
|
||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v3
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}-2
|
||||
|
||||
- name: Install dependencies & build
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
working-directory: node
|
||||
run: npm install --verbose
|
||||
|
||||
- name: Build Prebuild
|
||||
working-directory: node
|
||||
run: |
|
||||
npm run prebuildify
|
||||
tar -zcvf "linux.tar.gz" -C prebuilds .
|
||||
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v3
|
||||
with:
|
||||
name: linux
|
||||
path: node/linux.tar.gz
|
||||
|
||||
pack-module:
|
||||
needs: prebuild
|
||||
needs: [prebuild, prebuild-linux]
|
||||
name: Package deltachat-node and upload to download.delta.chat
|
||||
runs-on: ubuntu-20.04
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Install tree
|
||||
run: sudo apt install tree
|
||||
@@ -96,10 +166,10 @@ jobs:
|
||||
npm --version
|
||||
node --version
|
||||
echo $DELTACHAT_NODE_TAR_GZ
|
||||
- name: Download Ubuntu prebuild
|
||||
- name: Download Linux prebuild
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: ubuntu-20.04
|
||||
name: linux
|
||||
- name: Download macOS prebuild
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
@@ -111,11 +181,11 @@ jobs:
|
||||
- shell: bash
|
||||
run: |
|
||||
mkdir node/prebuilds
|
||||
tar -xvzf ubuntu-20.04/ubuntu-20.04.tar.gz -C node/prebuilds
|
||||
tar -xvzf linux/linux.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
|
||||
rm -rf linux macos-latest windows-latest
|
||||
- name: Install dependencies without running scripts
|
||||
run: |
|
||||
npm install --ignore-scripts
|
||||
|
||||
23
.github/workflows/node-tests.yml
vendored
23
.github/workflows/node-tests.yml
vendored
@@ -1,3 +1,6 @@
|
||||
# GitHub Actions workflow
|
||||
# to test Node.js bindings.
|
||||
|
||||
name: "node.js tests"
|
||||
|
||||
# Cancel previously started workflow runs
|
||||
@@ -52,25 +55,13 @@ jobs:
|
||||
|
||||
- name: Install dependencies & build
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd node
|
||||
npm install --verbose
|
||||
working-directory: node
|
||||
run: npm install --verbose
|
||||
|
||||
- name: Test
|
||||
timeout-minutes: 10
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
cd node
|
||||
npm run test
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"
|
||||
- name: Run tests on Windows, except lint
|
||||
timeout-minutes: 10
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
cd node
|
||||
npm run test:mocha
|
||||
working-directory: node
|
||||
run: npm run test
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"
|
||||
|
||||
3
.github/workflows/repl.yml
vendored
3
.github/workflows/repl.yml
vendored
@@ -1,4 +1,5 @@
|
||||
# Manually triggered action to build a Windows repl.exe which users can
|
||||
# Manually triggered GitHub Actions workflow
|
||||
# to build a Windows repl.exe which users can
|
||||
# download to debug complex bugs.
|
||||
|
||||
name: Build Windows REPL .exe
|
||||
|
||||
4
.github/workflows/upload-ffi-docs.yml
vendored
4
.github/workflows/upload-ffi-docs.yml
vendored
@@ -1,3 +1,7 @@
|
||||
# GitHub Actions workflow
|
||||
# to build `deltachat_fii` crate documentation
|
||||
# and upload it to <https://cffi.delta.chat/>
|
||||
|
||||
name: Build & Deploy Documentation on cffi.delta.chat
|
||||
|
||||
on:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
/build
|
||||
/dist
|
||||
|
||||
# ignore vi temporaries
|
||||
*~
|
||||
@@ -18,6 +19,9 @@ python/.eggs
|
||||
__pycache__
|
||||
python/src/deltachat/capi*.so
|
||||
python/.venv/
|
||||
python/venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
python/liveconfig*
|
||||
|
||||
|
||||
478
CHANGELOG.md
478
CHANGELOG.md
@@ -1,5 +1,469 @@
|
||||
# Changelog
|
||||
|
||||
## [1.126.1] - 2023-10-24
|
||||
|
||||
### Fixes
|
||||
|
||||
- Do not hardcode version in deltachat-rpc-server source package.
|
||||
- Do not interrupt IMAP loop from `get_connectivity_html()`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- imap: Buffer `STARTTLS` command.
|
||||
|
||||
### Build system
|
||||
|
||||
- Build `deltachat-rpc-server` binary for aarch64 macOS.
|
||||
- Build `deltachat-rpc-server` wheels for macOS and Windows.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Remove job queue.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- cargo: Update `ahash` to make `cargo-deny` happy.
|
||||
|
||||
## [1.126.0] - 2023-10-22
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Allow to filter by unread in `chatlist:try_load` ([#4824](https://github.com/deltachat/deltachat-core-rust/pull/4824)).
|
||||
- Add `misc_send_draft()` to JSON-RPC API ([#4839](https://github.com/deltachat/deltachat-core-rust/pull/4839)).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- [**breaking**] Make broadcast lists create their own chat ([#4644](https://github.com/deltachat/deltachat-core-rust/pull/4644)).
|
||||
- This means that UIs need to ask for the name when creating a broadcast list, similar to <https://github.com/deltachat/deltachat-android/pull/2653>.
|
||||
- Add self-address to backup filename ([#4820](https://github.com/deltachat/deltachat-core-rust/pull/4820))
|
||||
|
||||
### CI
|
||||
|
||||
- Build Python wheels for deltachat-rpc-server.
|
||||
|
||||
### Build system
|
||||
|
||||
- Strip release binaries.
|
||||
- Workaround OpenSSL crate expecting libatomic to be available.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Set `soft_heap_limit` on SQLite database.
|
||||
- imap: Fallback to `STATUS` if `SELECT` did not return UIDNEXT.
|
||||
|
||||
## [1.125.0] - 2023-10-14
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] deltachat-rpc-client: Replace `asyncio` with threads.
|
||||
- Validate boolean values passed to `set_config`. Attempts to set values other than `0` and `1` will result in an error.
|
||||
|
||||
### CI
|
||||
|
||||
- Reduce required Python version for deltachat-rpc-client from 3.8 to 3.7.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add developer option to disable IDLE.
|
||||
|
||||
### Fixes
|
||||
|
||||
- `deltachat-rpc-client`: Run `deltachat-rpc-server` in its own process group. This prevents reception of `SIGINT` by the server when the bot is terminated with `^C`.
|
||||
- python: Don't automatically set the displayname to "bot" when setting log level.
|
||||
- Don't update `timestamp`, `timestamp_rcvd`, `state` when replacing partially downloaded message ([#4700](https://github.com/deltachat/deltachat-core-rust/pull/4700)).
|
||||
- Assign encrypted partially downloaded group messages to 1:1 chat ([#4757](https://github.com/deltachat/deltachat-core-rust/pull/4757)).
|
||||
- Return all contacts from `Contact::get_all` for bots ([#4811](https://github.com/deltachat/deltachat-core-rust/pull/4811)).
|
||||
- Set connectivity status to "connected" during fake idle.
|
||||
- Return verifier contacts regardless of their origin.
|
||||
- Don't try to send more MDNs if there's a temporary SMTP error ([#4534](https://github.com/deltachat/deltachat-core-rust/pull/4534)).
|
||||
|
||||
### Refactor
|
||||
|
||||
- deltachat-rpc-client: Close stdin instead of sending `SIGTERM`.
|
||||
- deltachat-rpc-client: Remove print() calls. Standard `logging` package is for logging instead.
|
||||
|
||||
### Tests
|
||||
|
||||
- deltachat-rpc-client: Enable logs in pytest.
|
||||
|
||||
## [1.124.1] - 2023-10-05
|
||||
|
||||
### Fixes
|
||||
|
||||
- Remove footer from reactions on the receiver side ([#4780](https://github.com/deltachat/deltachat-core-rust/pull/4780)).
|
||||
|
||||
### CI
|
||||
|
||||
- Pin `urllib3` version to `<2`. ([#4788](https://github.com/deltachat/deltachat-core-rust/issues/4788))
|
||||
|
||||
## [1.124.0] - 2023-10-04
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Return `DC_CONTACT_ID_SELF` from `dc_contact_get_verifier_id()` for directly verified contacts.
|
||||
- Deprecate `dc_contact_get_verifier_addr`.
|
||||
- python: use `dc_contact_get_verifier_id()`. `get_verifier()` returns a Contact rather than an address now.
|
||||
- Deprecate `get_next_media()`.
|
||||
- Ignore public key argument in `dc_preconfigure_keypair()`. Public key is extracted from the private key.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Wrap base64-encoded parts to 76 characters.
|
||||
- Require valid email addresses in `dc_provider_new_from_email[_with_dns]`.
|
||||
- Do not trash messages with attachments and no text when `location.kml` is attached ([#4749](https://github.com/deltachat/deltachat-core-rust/issues/4749)).
|
||||
- Initialise `last_msg_id` to the highest known row id. This ensures bots migrated from older version to `dc_get_next_msgs()` API do not process all previous messages from scratch.
|
||||
- Do not put the status footer into reaction MIME parts.
|
||||
- Ignore special chats in `get_similar_chat_ids()`. This prevents trash chat from showing up in similar chat list ([#4756](https://github.com/deltachat/deltachat-core-rust/issues/4756)).
|
||||
- Cap percentage in connectivity layout to 100% ([#4765](https://github.com/deltachat/deltachat-core-rust/pull/4765)).
|
||||
- Add Let's Encrypt root certificate to `reqwest`. This should allow scanning `DCACCOUNT` QR-codes on older Android phones when the server has a Let's Encrypt certificate.
|
||||
- deltachat-rpc-client: Increase stdio buffer to 64 MiB to avoid Python bots crashing when trying to load large messages via a JSON-RPC call.
|
||||
- Add `protected-headers` directive to Content-Type of encrypted messages with attachments ([#2302](https://github.com/deltachat/deltachat-core-rust/issues/2302)). This makes Thunderbird show encrypted Subject for Delta Chat messages.
|
||||
- webxdc: Reset `document.update` on forwarding. This fixes the test `test_forward_webxdc_instance()`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Remove extra members from the local list in sake of group membership consistency ([#3782](https://github.com/deltachat/deltachat-core-rust/issues/3782)).
|
||||
- deltachat-rpc-client: Log exceptions when long-running tasks die.
|
||||
|
||||
### Build
|
||||
|
||||
- Build wheels for Python 3.12 and PyPy 3.10.
|
||||
|
||||
## [1.123.0] - 2023-09-22
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Make it possible to import secret key from a file with `DC_IMEX_IMPORT_SELF_KEYS`.
|
||||
- [**breaking**] Make `dc_jsonrpc_blocking_call` accept JSON-RPC request.
|
||||
|
||||
### Fixes
|
||||
|
||||
- `lookup_chat_by_reply()`: Skip not fully downloaded and undecipherable messages ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
|
||||
- `lookup_chat_by_reply()`: Skip undecipherable parent messages created by older versions ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
|
||||
- imex: Use "default" in the filename of the default key.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update OpenSSL from 3.1.2 to 3.1.3.
|
||||
|
||||
## [1.122.0] - 2023-09-12
|
||||
|
||||
### API-Changes
|
||||
|
||||
- jsonrpc: Return only chat IDs for similar chats.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Reopen all connections on database passpharse change.
|
||||
- Do not block new group chats if 1:1 chat is blocked.
|
||||
- Improve group membership consistency algorithm ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782))([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
|
||||
- Forbid membership changes from possible non-members ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782)).
|
||||
- `ChatId::parent_query()`: Don't filter out OutPending and OutFailed messages.
|
||||
|
||||
### Build system
|
||||
|
||||
- Update to OpenSSL 3.0.
|
||||
- Bump webpki from 0.22.0 to 0.22.1.
|
||||
- python: Add link to Mastodon into projects.urls.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Add RSA-4096 key generation support.
|
||||
|
||||
### Refactor
|
||||
|
||||
- pgp: Add constants for encryption algorithm and hash.
|
||||
|
||||
## [1.121.0] - 2023-09-06
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add `dc_context_change_passphrase()`.
|
||||
- Add `Message.set_file_from_bytes()` API.
|
||||
- Add experimental API to get similar chats.
|
||||
|
||||
### Build system
|
||||
|
||||
- Build node packages on Ubuntu 18.04 instead of Debian 10.
|
||||
This reduces the requirement for glibc version from 2.28 to 2.27.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Allow membership changes by a MUA if we're not in the group ([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
|
||||
- Save mime headers for messages not signed with a known key ([#4557](https://github.com/deltachat/deltachat-core-rust/pull/4557)).
|
||||
- Return from `dc_get_chatlist(DC_GCL_FOR_FORWARDING)` only chats where we can send ([#4616](https://github.com/deltachat/deltachat-core-rust/pull/4616)).
|
||||
- Do not allow dots at the end of email addresses.
|
||||
- deltachat-rpc-client: Remove `aiodns` optional dependency from required dependencies.
|
||||
`aiodns` depends on `pycares` which [fails to install in Termux](https://github.com/saghul/aiodns/issues/98).
|
||||
|
||||
## [1.120.0] - 2023-08-28
|
||||
|
||||
### API-Changes
|
||||
|
||||
- jsonrpc: Add `resend_messages`.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Update async-imap to 0.9.1 to fix memory leak.
|
||||
- Delete messages from SMTP queue only on user demand ([#4579](https://github.com/deltachat/deltachat-core-rust/pull/4579)).
|
||||
- Do not send images without transparency as stickers ([#4611](https://github.com/deltachat/deltachat-core-rust/pull/4611)).
|
||||
- `prepare_msg_blob()`: do not use the image if it has Exif metadata but the image cannot be recoded.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Hide accounts.rs constants from public API.
|
||||
- Hide pgp module from public API.
|
||||
|
||||
### Build system
|
||||
|
||||
- Update to Zig 0.11.0.
|
||||
- Update to Rust 1.72.0.
|
||||
|
||||
### CI
|
||||
|
||||
- Run on push to stable branch.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- python: Fix lint errors.
|
||||
- python: Fix `ruff` 0.0.286 warnings.
|
||||
- Fix beta clippy warnings.
|
||||
|
||||
## [1.119.1] - 2023-08-06
|
||||
|
||||
Bugfix release attempting to fix the [iOS build error](https://github.com/deltachat/deltachat-core-rust/issues/4610).
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Guess message viewtype from "application/octet-stream" attachment extension ([#4378](https://github.com/deltachat/deltachat-core-rust/pull/4378)).
|
||||
|
||||
### Fixes
|
||||
|
||||
- Update `xattr` from 1.0.0 to 1.0.1 to fix UnsupportedPlatformError import.
|
||||
|
||||
### Tests
|
||||
|
||||
- webxdc: Ensure unknown WebXDC update properties do not result in an error.
|
||||
|
||||
## [1.119.0] - 2023-08-03
|
||||
|
||||
### Fixes
|
||||
|
||||
- imap: Avoid IMAP move loops when DeltaChat folder is aliased.
|
||||
- imap: Do not resync IMAP after initial configuration.
|
||||
|
||||
- webxdc: Accept WebXDC updates in mailing lists.
|
||||
- webxdc: Base64-encode WebXDC updates to prevent corruption of large unencrypted WebXDC updates.
|
||||
- webxdc: Delete old webxdc status updates during housekeeping.
|
||||
|
||||
- Return valid MsgId from `receive_imf()` when the message is replaced.
|
||||
- Emit MsgsChanged event with correct chat id for replaced messages.
|
||||
|
||||
- deltachat-rpc-server: Update tokio-tar to fix backup import.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- deltachat-rpc-client: Add `MSG_DELETED` constant.
|
||||
- Make `dc_msg_get_filename()` return the original attachment filename ([#4309](https://github.com/deltachat/deltachat-core-rust/pull/4309)).
|
||||
|
||||
### API-Changes
|
||||
|
||||
- deltachat-rpc-client: Add `Account.{import,export}_backup` methods.
|
||||
- deltachat-jsonrpc: Make `MessageObject.text` non-optional.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Update default value for `show_emails` in `dc_set_config()` documentation.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Improve IMAP logs.
|
||||
|
||||
### Tests
|
||||
|
||||
- Add basic import/export test for async python.
|
||||
- Add `test_webxdc_download_on_demand`.
|
||||
- Add tests for deletion of webxdc status-updates.
|
||||
|
||||
## [1.118.0] - 2023-07-07
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Remove `Contact::load_from_db()` in favor of `Contact::get_by_id()`.
|
||||
- Add `Contact::get_by_id_optional()` API.
|
||||
- [**breaking**] Make `Message.text` non-optional.
|
||||
- [**breaking**] Replace `message::get_msg_info()` with `MsgId.get_info()`.
|
||||
- Move `handle_mdn` and `handle_ndn` to mimeparser and make them private.
|
||||
Previously `handle_mdn` was erroneously exposed in the public API.
|
||||
- python: flatten the API of `deltachat` module.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Use different member added/removal messages locally and on the network.
|
||||
- Update tokio to 1.29.1 to fix core panic after sending 29 offline messages ([#4414](https://github.com/deltachat/deltachat-core-rust/issues/4414)).
|
||||
- Make SVG avatar image work on more platforms (use `xlink:href`).
|
||||
- Preserve indentation when converting plaintext to HTML.
|
||||
- Do not run simplify() on dehtml() output.
|
||||
- Rewrite member added/removed messages even if the change is not allowed PR ([#4529](https://github.com/deltachat/deltachat-core-rust/pull/4529)).
|
||||
|
||||
### Documentation
|
||||
|
||||
- Document how to regenerate Node.js constants before the release.
|
||||
|
||||
### Build system
|
||||
|
||||
- git-cliff: Do not fail if commit.footers is undefined.
|
||||
|
||||
### Other
|
||||
|
||||
- Dependency updates.
|
||||
- Update MPL 2.0 license text.
|
||||
- Add LICENSE file to deltachat-rpc-client.
|
||||
- deltachat-rpc-client: Add Trove classifiers.
|
||||
- python: Change bindings status to production/stable.
|
||||
|
||||
### Tests
|
||||
|
||||
- Add `make-python-testenv.sh` script.
|
||||
|
||||
## [1.117.0] - 2023-06-15
|
||||
|
||||
### Features
|
||||
|
||||
- New group membership update algorithm.
|
||||
|
||||
New algorithm improves group consistency
|
||||
in cases of missing messages,
|
||||
restored old backups and replies from classic MUAs.
|
||||
|
||||
- Add `DC_EVENT_MSG_DELETED` event.
|
||||
|
||||
This event notifies the UI about the message
|
||||
being deleted from the messagelist, e.g. when the message expires
|
||||
or the user deletes it.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Emit `DC_EVENT_MSGS_CHANGED` without IDs when the message expires.
|
||||
|
||||
Specifying msg IDs that cannot be loaded in the event payload
|
||||
results in an error when the UI tries to load the message.
|
||||
Instead, emit an event without IDs
|
||||
to make the UI reload the whole messagelist.
|
||||
|
||||
- Ignore address case when comparing the `To:` field to `Autocrypt-Gossip:`.
|
||||
|
||||
This bug resulted in failure to propagate verification
|
||||
if the contact list already contained a new verified group member
|
||||
with a non-lowercase address.
|
||||
|
||||
- dehtml: skip links with empty text.
|
||||
|
||||
Links like `<a href="https://delta.chat/"></a>` in HTML mails are now skipped
|
||||
instead of being converted to a link without a label like `[](https://delta.chat/)`.
|
||||
|
||||
- dehtml: Do not insert unnecessary newlines when parsing `<p>` tags.
|
||||
|
||||
- Update from yanked `libc` 0.2.145 to 0.2.146.
|
||||
- Update to async-imap 0.9.0 to remove deprecated `ouroboros` dependency.
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Emit `DC_EVENT_MSGS_CHANGED` per chat when messages are deleted.
|
||||
|
||||
Previously a single event with zero chat ID was emitted.
|
||||
|
||||
- python: make `Contact.is_verified()` return bool.
|
||||
|
||||
- rust: add API endpoint `get_status_update` ([#4468](https://github.com/deltachat/deltachat-core-rust/pull/4468)).
|
||||
|
||||
- rust: make `WebxdcManifest` type public.
|
||||
|
||||
### Build system
|
||||
|
||||
- Use Rust 1.70.0 to compile deltachat-rpc-server releases.
|
||||
- Disable unused `brotli` feature `ffi-api` and use 1 codegen-units for release builds to reduce the size of the binaries.
|
||||
|
||||
### CI
|
||||
|
||||
- Run `cargo check` with musl libc.
|
||||
- concourse: Install devpi in a virtual environment.
|
||||
- Remove [mergeable](https://mergeable.us/) configuration.
|
||||
|
||||
### Documentation
|
||||
|
||||
- README: mark napi.rs bindings as experimental. CFFI bindings are not legacy and are the recommended Node.js bindings currently.
|
||||
- CONTRIBUTING: document how conventional commits interact with squash merges.
|
||||
|
||||
### Refactor
|
||||
|
||||
- Rename `MimeMessage.header` into `MimeMessage.headers`.
|
||||
|
||||
- Derive `Default` trait for `WebxdcManifest`.
|
||||
|
||||
### Tests
|
||||
|
||||
- Regression test for case-sensitive comparison of gossip header to contact address.
|
||||
- Multiple new group consistency tests in Rust.
|
||||
- python: Replace legacy `tmpdir` fixture with `tmp_path`.
|
||||
|
||||
## [1.116.0] - 2023-06-05
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Add `dc_jsonrpc_blocking_call()`.
|
||||
|
||||
### Changes
|
||||
|
||||
- Generate OpenRPC definitions for JSON-RPC.
|
||||
- Add more context to message loading errors.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Build deltachat-node prebuilds on Debian 10.
|
||||
|
||||
### Documentation
|
||||
|
||||
- Document release process in `RELEASE.md`.
|
||||
- Add contributing guidelines `CONTRIBUTING.md`.
|
||||
- Update instructions for python devenv.
|
||||
- python: Document pytest fixtures.
|
||||
|
||||
### Tests
|
||||
|
||||
- python: Make `test_mdn_asymmetric` less flaky.
|
||||
- Make `test_group_with_removed_message_id` less flaky.
|
||||
- Add golden tests infrastructure ([#4395](https://github.com/deltachat/deltachat-core-rust/pull/4395)).
|
||||
|
||||
### Build system
|
||||
|
||||
- git-cliff: Changelog generation improvements.
|
||||
- `set_core_version.py`: Expect release date in the changelog.
|
||||
|
||||
### CI
|
||||
|
||||
- Require Python 3.8 for deltachat-rpc-client.
|
||||
- mergeable: Allow PR titles to start with "ci" and "build".
|
||||
- Remove incorrect comment.
|
||||
- dependabot: Use `chore` prefix for dependency updates.
|
||||
- Remove broken `node-delete-preview.yml` workflow.
|
||||
- Add top comments to GH Actions workflows.
|
||||
- Run node.js lint on Windows.
|
||||
- Update clippy to 1.70.0.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Remove release.toml.
|
||||
- gitattributes: Configure LF line endings for JavaScript files.
|
||||
- Update dependencies
|
||||
|
||||
## [1.112.10] - 2023-06-01
|
||||
|
||||
### Fixes
|
||||
|
||||
- Disable `fetch_existing_msgs` setting by default.
|
||||
- Update `h2` to fix RUSTSEC-2023-0034.
|
||||
|
||||
## [1.115.0] - 2023-05-12
|
||||
|
||||
### JSON-RPC API Changes
|
||||
@@ -2486,6 +2950,20 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.112.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.6...v1.112.7
|
||||
[1.112.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.7...v1.112.8
|
||||
[1.112.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.8...v1.112.9
|
||||
[1.112.10]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.112.10
|
||||
[1.113.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.113.0
|
||||
[1.114.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.113.0...v1.114.0
|
||||
[1.115.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.114.0...v1.115.0
|
||||
[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.0...v1.116.0
|
||||
[1.117.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.116.0...v1.117.0
|
||||
[1.118.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.117.0...v1.118.0
|
||||
[1.119.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.118.0...v1.119.0
|
||||
[1.119.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.0...v1.119.1
|
||||
[1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0
|
||||
[1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0
|
||||
[1.122.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.121.0...v1.122.0
|
||||
[1.123.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.122.0...v1.123.0
|
||||
[1.124.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.123.0...v1.124.0
|
||||
[1.124.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.0...v1.124.1
|
||||
[1.125.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.1...v1.125.0
|
||||
[1.126.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.125.0...v1.126.0
|
||||
|
||||
92
CONTRIBUTING.md
Normal file
92
CONTRIBUTING.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Contributing guidelines
|
||||
|
||||
## Reporting bugs
|
||||
|
||||
If you found a bug, [report it on GitHub](https://github.com/deltachat/deltachat-core-rust/issues).
|
||||
If the bug you found is specific to
|
||||
[Android](https://github.com/deltachat/deltachat-android/issues),
|
||||
[iOS](https://github.com/deltachat/deltachat-ios/issues) or
|
||||
[Desktop](https://github.com/deltachat/deltachat-desktop/issues),
|
||||
report it to the corresponding repository.
|
||||
|
||||
## Proposing features
|
||||
|
||||
If you have a feature request, create a new topic on the [forum](https://support.delta.chat/).
|
||||
|
||||
## Contributing code
|
||||
|
||||
If you want to contribute a code, [open a Pull Request](https://github.com/deltachat/deltachat-core-rust/pulls).
|
||||
|
||||
If you have write access to the repository,
|
||||
push a branch named `<username>/<feature>`
|
||||
so it is clear who is responsible for the branch,
|
||||
and open a PR proposing to merge the change.
|
||||
Otherwise fork the repository and create a branch in your fork.
|
||||
|
||||
You can find the list of good first issues
|
||||
and a link to this guide
|
||||
on the contributing page: <https://github.com/deltachat/deltachat-core-rust/contribute>
|
||||
|
||||
### Coding conventions
|
||||
|
||||
We format the code using `rustfmt`. Run `cargo fmt` prior to committing the code.
|
||||
Run `scripts/clippy.sh` to check the code for common mistakes with [Clippy].
|
||||
|
||||
Commit messages follow the [Conventional Commits] notation.
|
||||
We use [git-cliff] to generate the changelog from commit messages before the release.
|
||||
|
||||
With **`git cliff --unreleased`**, you can check how the changelog entry for your commit will look.
|
||||
|
||||
The following prefix types are used:
|
||||
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
|
||||
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is cancelled"
|
||||
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
|
||||
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
|
||||
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
|
||||
- `test`: Test changes and improvements to the testing framework.
|
||||
- `build`: Build system and tool configuration changes, e.g. "build(git-cliff): put "ci" commits into "CI" section of changelog"
|
||||
- `ci`: CI configuration changes, e.g. "ci: limit artifact retention time for `libdeltachat.a` to 1 day"
|
||||
- `docs`: Documentation changes, e.g. "docs: add contributing guidelines"
|
||||
- `chore`: miscellaneous tasks, e.g. "chore: add `.DS_Store` to `.gitignore`"
|
||||
|
||||
Release preparation commits are marked as "chore(release): prepare for vX.Y.Z".
|
||||
|
||||
If you intend to squash merge the PR from the web interface,
|
||||
make sure the PR title follows the conventional commits notation
|
||||
as it will end up being a commit title.
|
||||
Otherwise make sure each commit title follows the conventional commit notation.
|
||||
|
||||
#### Breaking Changes
|
||||
|
||||
Use a `!` to mark breaking changes, e.g. "api!: Remove `dc_chat_can_send`".
|
||||
|
||||
Alternatively, breaking changes can go into the commit description, e.g.:
|
||||
|
||||
```
|
||||
fix: Fix race condition and db corruption when a message was received during backup
|
||||
|
||||
BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`
|
||||
```
|
||||
|
||||
#### Multiple Changes in one PR
|
||||
|
||||
If you have multiple changes in one PR, create multiple conventional commits, and then do a rebase merge. Otherwise, you should usually do a squash merge.
|
||||
|
||||
[Clippy]: https://doc.rust-lang.org/clippy/
|
||||
[Conventional Commits]: https://www.conventionalcommits.org/
|
||||
[git-cliff]: https://git-cliff.org/
|
||||
|
||||
### Reviewing
|
||||
|
||||
Once a PR has an approval and passes CI, it can be merged.
|
||||
|
||||
PRs from a branch created in the main repository, i.e. authored by those who have write access, are merged by their authors.
|
||||
This is to ensure that PRs are merged as intended by the author,
|
||||
e.g. as a squash merge, by rebasing from the web interface or manually from the command line.
|
||||
|
||||
If you do not have access to the repository and created a PR from a fork,
|
||||
ask the maintainers to merge the PR and say how it should be merged.
|
||||
|
||||
## Other ways to contribute
|
||||
|
||||
For other ways to contribute, refer to the [website](https://delta.chat/en/contribute).
|
||||
1616
Cargo.lock
generated
1616
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
33
Cargo.toml
33
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.115.0"
|
||||
version = "1.126.1"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.65"
|
||||
@@ -23,6 +23,8 @@ opt-level = "z"
|
||||
lto = true
|
||||
panic = 'abort'
|
||||
opt-level = "z"
|
||||
codegen-units = 1
|
||||
strip = true
|
||||
|
||||
[patch.crates-io]
|
||||
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
|
||||
@@ -35,13 +37,13 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
||||
|
||||
anyhow = "1"
|
||||
async-channel = "1.8.0"
|
||||
async-imap = { version = "0.8.0", default-features = false, features = ["runtime-tokio"] }
|
||||
async-imap = { version = "0.9.1", default-features = false, features = ["runtime-tokio"] }
|
||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
|
||||
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
|
||||
backtrace = "0.3"
|
||||
base64 = "0.21"
|
||||
brotli = "3.3"
|
||||
brotli = { version = "3.3", default-features=false, features = ["std"] }
|
||||
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
||||
@@ -58,19 +60,19 @@ lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master"
|
||||
libc = "0.2"
|
||||
mailparse = "0.14"
|
||||
mime = "0.3.17"
|
||||
num_cpus = "1.15"
|
||||
num-derive = "0.3"
|
||||
num_cpus = "1.16"
|
||||
num-derive = "0.4"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.17.0"
|
||||
percent-encoding = "2.2"
|
||||
once_cell = "1.18.0"
|
||||
percent-encoding = "2.3"
|
||||
parking_lot = "0.12"
|
||||
pgp = { version = "0.10", default-features = false }
|
||||
pretty_env_logger = { version = "0.4", optional = true }
|
||||
pretty_env_logger = { version = "0.5", optional = true }
|
||||
qrcodegen = "1.7.0"
|
||||
quick-xml = "0.28"
|
||||
quick-xml = "0.29"
|
||||
rand = "0.8"
|
||||
regex = "1.8"
|
||||
reqwest = { version = "0.11.17", features = ["json"] }
|
||||
reqwest = { version = "0.11.18", features = ["json"] }
|
||||
rusqlite = { version = "0.29", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
sanitize-filename = "0.4"
|
||||
@@ -79,8 +81,8 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
smallvec = "1"
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
strum = "0.25"
|
||||
strum_macros = "0.25"
|
||||
tagger = "4.3.4"
|
||||
textwrap = "0.16.0"
|
||||
thiserror = "1"
|
||||
@@ -96,14 +98,15 @@ uuid = { version = "1", features = ["serde", "v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
criterion = { version = "0.4.0", features = ["async_tokio"] }
|
||||
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||
futures-lite = "1.13"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
pretty_env_logger = "0.5"
|
||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||
tempfile = "3"
|
||||
testdir = "0.7.3"
|
||||
testdir = "0.8.0"
|
||||
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
||||
pretty_assertions = "1.3.0"
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
|
||||
2
LICENSE
2
LICENSE
@@ -361,7 +361,7 @@ Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
|
||||
@@ -167,8 +167,8 @@ Language bindings are available for:
|
||||
|
||||
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
||||
- **Node.js**
|
||||
- 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)\]
|
||||
- over cffi: \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
|
||||
- over jsonrpc built with napi.rs (experimental): \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
|
||||
- **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/)\]
|
||||
|
||||
21
RELEASE.md
Normal file
21
RELEASE.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Releasing a new version of DeltaChat core
|
||||
|
||||
For example, to release version 1.116.0 of the core, do the following steps.
|
||||
|
||||
1. Resolve all [blocker issues](https://github.com/deltachat/deltachat-core-rust/labels/blocker).
|
||||
|
||||
2. Run `npm run build:core:constants` in the root of the repository
|
||||
and commit generated `node/constants.js`, `node/events.js` and `node/lib/constants.js`.
|
||||
|
||||
3. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
|
||||
|
||||
4. Update the version by running `scripts/set_core_version.py 1.116.0`.
|
||||
|
||||
5. Commit the changes as `chore(release): prepare for 1.116.0`.
|
||||
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
|
||||
|
||||
6. Tag the release: `git tag -a v1.116.0`.
|
||||
|
||||
7. Push the release tag: `git push origin v1.116.0`.
|
||||
|
||||
8. Create a GitHub release: `gh release create v1.116.0 -n ''`.
|
||||
@@ -67,9 +67,11 @@ body = """
|
||||
- {% if commit.breaking %}[**breaking**] {% endif %}\
|
||||
{% if commit.scope %}{{ commit.scope }}: {% endif %}\
|
||||
{{ commit.message | upper_first }}.\
|
||||
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
|
||||
{% raw %} {% endraw %}- {{ footer.value }}\
|
||||
{% endif %}{% endfor %}\
|
||||
{% if commit.footers is defined %}\
|
||||
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
|
||||
{% raw %} {% endraw %}- {{ footer.value }}\
|
||||
{% endif %}{% endfor %}\
|
||||
{% endif%}\
|
||||
{% endfor %}
|
||||
{% endfor %}\n
|
||||
"""
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.115.0"
|
||||
version = "1.126.1"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
@@ -24,8 +24,8 @@ tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
rand = "0.8"
|
||||
once_cell = "1.17.0"
|
||||
yerpc = { version = "0.4.4", features = ["anyhow_expose"] }
|
||||
once_cell = "1.18.0"
|
||||
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -301,6 +301,19 @@ dc_context_t* dc_context_new_closed (const char* dbfile);
|
||||
int dc_context_open (dc_context_t *context, const char* passphrase);
|
||||
|
||||
|
||||
/**
|
||||
* Changes the passphrase on the open database.
|
||||
* Existing database must already be encrypted and the passphrase cannot be NULL or empty.
|
||||
* It is impossible to encrypt unencrypted database with this method and vice versa.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param passphrase The new passphrase.
|
||||
* @return 1 on success, 0 on error.
|
||||
*/
|
||||
int dc_context_change_passphrase (dc_context_t* context, const char* passphrase);
|
||||
|
||||
|
||||
/**
|
||||
* Returns 1 if database is open.
|
||||
*
|
||||
@@ -420,17 +433,19 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* 0=watch all folders normally (default)
|
||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
|
||||
* show direct replies to chats only (default),
|
||||
* show direct replies to chats only,
|
||||
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
|
||||
* also show all mails of confirmed contacts,
|
||||
* DC_SHOW_EMAILS_ALL (2)=
|
||||
* also show mails of unconfirmed contacts.
|
||||
* also show mails of unconfirmed contacts (default).
|
||||
* - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)=
|
||||
* generate recommended key type (default),
|
||||
* DC_KEY_GEN_RSA2048 (1)=
|
||||
* generate RSA 2048 keypair
|
||||
* DC_KEY_GEN_ED25519 (2)=
|
||||
* generate Ed25519 keypair
|
||||
* generate Curve25519 keypair
|
||||
* DC_KEY_GEN_RSA4096 (3)=
|
||||
* generate RSA 4096 keypair
|
||||
* - `save_mime_headers` = 1=save mime headers
|
||||
* and make dc_get_mime_headers() work for subsequent calls,
|
||||
* 0=do not save mime headers (default)
|
||||
@@ -477,6 +492,9 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
|
||||
* 0=do not fetch existing messages on configure.
|
||||
* In both cases, existing recipients are added to the contact database.
|
||||
* - `disable_idle` = 1=disable IMAP IDLE even if the server supports it,
|
||||
* 0=use IMAP IDLE if the server supports it.
|
||||
* This is a developer option used for testing polling used as an IDLE fallback.
|
||||
* - `download_limit` = Messages up to this number of bytes are downloaded automatically.
|
||||
* For larger messages, only the header is downloaded and a placeholder is shown.
|
||||
* These messages can be downloaded fully using dc_download_full_msg() later.
|
||||
@@ -485,6 +503,9 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* to not mess up with non-delivery-reports or read-receipts.
|
||||
* 0=no limit (default).
|
||||
* Changes affect future messages only.
|
||||
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
|
||||
* seconds. 2 days by default.
|
||||
* This is not supposed to be changed by UIs and only used for testing.
|
||||
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
|
||||
* The prefix should be followed by the system and maybe subsystem,
|
||||
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
|
||||
@@ -809,7 +830,7 @@ void dc_maybe_network (dc_context_t* context);
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param addr The e-mail address of the user. This must match the
|
||||
* configured_addr setting of the context as well as the UID of the key.
|
||||
* @param public_data ASCII armored public key.
|
||||
* @param public_data Ignored, actual public key is extracted from secret_data.
|
||||
* @param secret_data ASCII armored secret key.
|
||||
* @return 1 on success, 0 on failure.
|
||||
*/
|
||||
@@ -863,7 +884,8 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
|
||||
* - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
|
||||
* is added as needed.
|
||||
* @param query_str An optional query for filtering the list. Only chats matching this query
|
||||
* are returned. Give NULL for no filtering.
|
||||
* are returned. Give NULL for no filtering. When `is:unread` is contained in the query,
|
||||
* the chatlist is filtered such that only chats with unread messages show up.
|
||||
* @param query_id An optional contact ID for filtering the list. Only chats including this contact ID
|
||||
* are returned. Give 0 for no filtering.
|
||||
* @return A chatlist as an dc_chatlist_t object.
|
||||
@@ -1321,6 +1343,20 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch
|
||||
int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Returns a list of similar chats.
|
||||
*
|
||||
* @warning This is an experimental API which may change or be removed in the future.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param chat_id The ID of the chat for which to find similar chats.
|
||||
* @return The list of similar chats.
|
||||
* On errors, NULL is returned.
|
||||
* Must be freed using dc_chatlist_unref() when no longer used.
|
||||
*/
|
||||
dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Estimate the number of messages that will be deleted
|
||||
@@ -1452,6 +1488,7 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
|
||||
* Typically used to implement the "next" and "previous" buttons
|
||||
* in a gallery or in a media player.
|
||||
*
|
||||
* @deprecated Deprecated 2023-10-03, use dc_get_chat_media() and navigate the returned array instead.
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param msg_id The ID of the current message from which the next or previous message should be searched.
|
||||
@@ -1681,24 +1718,12 @@ uint32_t dc_create_group_chat (dc_context_t* context, int protect
|
||||
* Create a new broadcast list.
|
||||
*
|
||||
* Broadcast lists are similar to groups on the sending device,
|
||||
* however, recipients get the messages in normal one-to-one chats
|
||||
* and will not be aware of other members.
|
||||
* however, recipients get the messages in a read-only chat
|
||||
* and will see who the other members are.
|
||||
*
|
||||
* Replies to broadcasts go only to the sender
|
||||
* and not to all broadcast recipients.
|
||||
* Moreover, replies will not appear in the broadcast list
|
||||
* but in the one-to-one chat with the person answering.
|
||||
*
|
||||
* The name and the image of the broadcast list is set automatically
|
||||
* and is visible to the sender only.
|
||||
* Not asking for these data allows more focused creation
|
||||
* and we bypass the question who will get which data.
|
||||
* Also, many users will have at most one broadcast list
|
||||
* so, a generic name and image is sufficient at the first place.
|
||||
*
|
||||
* Later on, however, the name can be changed using dc_set_chat_name().
|
||||
* The image cannot be changed to have a unique, recognizable icon in the chat lists.
|
||||
* All in all, this is also what other messengers are doing here.
|
||||
* For historical reasons, this function does not take a name directly,
|
||||
* instead you have to set the name using dc_set_chat_name()
|
||||
* after creating the broadcast list.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
@@ -2241,8 +2266,7 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
|
||||
* the backup is not encrypted.
|
||||
* The backup contains all contacts, chats, images and other data and device independent settings.
|
||||
* The backup does not contain device dependent settings as ringtones or LED notification settings.
|
||||
* The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
|
||||
* the format is `delta-chat-<day>-<number>.tar`
|
||||
* The name of the backup is `delta-chat-backup-<day>-<number>-<addr>.tar`.
|
||||
*
|
||||
* - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. `param2` is the passphrase.
|
||||
* The file is normally created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
|
||||
@@ -2255,6 +2279,7 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
|
||||
*
|
||||
* - **DC_IMEX_IMPORT_SELF_KEYS** (2) - Import private keys found in the directory given as `param1`.
|
||||
* The last imported key is made the default keys unless its name contains the string `legacy`. Public keys are not imported.
|
||||
* If `param1` is a filename, import the private key from the file and make it the default.
|
||||
*
|
||||
* While dc_imex() returns immediately, the started job may take a while,
|
||||
* you can stop it using dc_stop_ongoing_process(). During execution of the job,
|
||||
@@ -3925,7 +3950,7 @@ int64_t dc_msg_get_received_timestamp (const dc_msg_t* msg);
|
||||
* Get the message time used for sorting.
|
||||
* This function returns the timestamp that is used for sorting the message
|
||||
* into lists as returned e.g. by dc_get_chat_msgs().
|
||||
* This may be the reveived time, the sending time or another time.
|
||||
* This may be the received time, the sending time or another time.
|
||||
*
|
||||
* To get the receiving time, use dc_msg_get_received_timestamp().
|
||||
* To get the sending time, use dc_msg_get_timestamp().
|
||||
@@ -3978,16 +4003,17 @@ char* dc_msg_get_text (const dc_msg_t* msg);
|
||||
*/
|
||||
char* dc_msg_get_subject (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Find out full path, file name and extension of the file associated with a
|
||||
* message.
|
||||
* Find out full path of the file associated with a message.
|
||||
*
|
||||
* Typically files are associated with images, videos, audios, documents.
|
||||
* Plain text messages do not have a file.
|
||||
* File name may be mangled. To obtain the original attachment filename use dc_msg_get_filename().
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return The full path, the file name, and the extension of the file associated with the message.
|
||||
* @return The full path (with file name and extension) of the file associated with the message.
|
||||
* If there is no file associated with the message, an empty string is returned.
|
||||
* NULL is never returned and the returned value must be released using dc_str_unref().
|
||||
*/
|
||||
@@ -3995,14 +4021,13 @@ char* dc_msg_get_file (const dc_msg_t* msg);
|
||||
|
||||
|
||||
/**
|
||||
* Get a base file name without the path. The base file name includes the extension; the path
|
||||
* is not returned. To get the full path, use dc_msg_get_file().
|
||||
* Get an original attachment filename, with extension but without the path. To get the full path,
|
||||
* use dc_msg_get_file().
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @return The base file name plus the extension without part. If there is no file
|
||||
* associated with the message, an empty string is returned. The returned
|
||||
* value must be released using dc_str_unref().
|
||||
* @return The attachment filename. If there is no file associated with the message, an empty string
|
||||
* is returned. The returned value must be released using dc_str_unref().
|
||||
*/
|
||||
char* dc_msg_get_filename (const dc_msg_t* msg);
|
||||
|
||||
@@ -5017,6 +5042,7 @@ int dc_contact_is_verified (dc_contact_t* contact);
|
||||
* A string containing the verifiers address. If it is the same address as the contact itself,
|
||||
* we verified the contact ourself. If it is an empty string, we don't have verifier
|
||||
* information or the contact is not verified.
|
||||
* @deprecated 2023-09-28, use dc_contact_get_verifier_id instead
|
||||
*/
|
||||
char* dc_contact_get_verifier_addr (dc_contact_t* contact);
|
||||
|
||||
@@ -5029,7 +5055,7 @@ char* dc_contact_get_verifier_addr (dc_contact_t* contact);
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return
|
||||
* The `ContactId` of the verifiers address. If it is the same address as the contact itself,
|
||||
* The contact ID of the verifier. If it is DC_CONTACT_ID_SELF,
|
||||
* we verified the contact ourself. If it is 0, we don't have verifier information or
|
||||
* the contact is not verified.
|
||||
*/
|
||||
@@ -5725,12 +5751,11 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
|
||||
*
|
||||
* @memberof dc_jsonrpc_instance_t
|
||||
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
|
||||
* @param method JSON-RPC method name, e.g. `check_email_validity`.
|
||||
* @param params JSON-RPC method parameters, e.g. `["alice@example.org"]`.
|
||||
* @param input JSON-RPC request.
|
||||
* @return JSON-RPC response as string, must be freed using dc_str_unref() after usage.
|
||||
* On error, NULL is returned.
|
||||
* If there is no response, NULL is returned.
|
||||
*/
|
||||
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *method, const char *params);
|
||||
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input);
|
||||
|
||||
/**
|
||||
* @class dc_event_emitter_t
|
||||
@@ -6079,6 +6104,15 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_MSG_READ 2015
|
||||
|
||||
|
||||
/**
|
||||
* A single message is deleted.
|
||||
*
|
||||
* @param data1 (int) chat_id
|
||||
* @param data2 (int) msg_id
|
||||
*/
|
||||
#define DC_EVENT_MSG_DELETED 2016
|
||||
|
||||
|
||||
/**
|
||||
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
* Or the verify state of a chat has changed.
|
||||
@@ -6257,6 +6291,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_KEY_GEN_DEFAULT 0
|
||||
#define DC_KEY_GEN_RSA2048 1
|
||||
#define DC_KEY_GEN_ED25519 2
|
||||
#define DC_KEY_GEN_RSA4096 3
|
||||
|
||||
|
||||
/**
|
||||
|
||||
@@ -29,7 +29,7 @@ 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::key::{DcKey, DcSecretKey};
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::net::read_url_blob;
|
||||
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
@@ -167,6 +167,24 @@ pub unsafe extern "C" fn dc_context_open(
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_context_change_passphrase(
|
||||
context: *mut dc_context_t,
|
||||
passphrase: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_context_change_passphrase()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let ctx = &*context;
|
||||
let passphrase = to_string_lossy(passphrase);
|
||||
block_on(ctx.change_passphrase(passphrase))
|
||||
.context("dc_context_change_passphrase() failed")
|
||||
.log_err(ctx)
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
@@ -527,6 +545,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::MsgDelivered { .. } => 2010,
|
||||
EventType::MsgFailed { .. } => 2012,
|
||||
EventType::MsgRead { .. } => 2015,
|
||||
EventType::MsgDeleted { .. } => 2016,
|
||||
EventType::ChatModified(_) => 2020,
|
||||
EventType::ChatEphemeralTimerModified { .. } => 2021,
|
||||
EventType::ContactsChanged(_) => 2030,
|
||||
@@ -574,6 +593,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::MsgDelivered { chat_id, .. }
|
||||
| EventType::MsgFailed { chat_id, .. }
|
||||
| EventType::MsgRead { chat_id, .. }
|
||||
| EventType::MsgDeleted { chat_id, .. }
|
||||
| EventType::ChatModified(chat_id)
|
||||
| EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
|
||||
EventType::ContactsChanged(id) | EventType::LocationChanged(id) => {
|
||||
@@ -631,7 +651,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::IncomingMsg { msg_id, .. }
|
||||
| EventType::MsgDelivered { msg_id, .. }
|
||||
| EventType::MsgFailed { msg_id, .. }
|
||||
| EventType::MsgRead { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
| EventType::MsgRead { msg_id, .. }
|
||||
| EventType::MsgDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::SecurejoinInviterProgress { progress, .. }
|
||||
| EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
|
||||
EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
|
||||
@@ -674,6 +695,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::MsgDelivered { .. }
|
||||
| EventType::MsgFailed { .. }
|
||||
| EventType::MsgRead { .. }
|
||||
| EventType::MsgDeleted { .. }
|
||||
| EventType::ChatModified(_)
|
||||
| EventType::ContactsChanged(_)
|
||||
| EventType::LocationChanged(_)
|
||||
@@ -783,7 +805,7 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) {
|
||||
pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
context: *mut dc_context_t,
|
||||
addr: *const libc::c_char,
|
||||
public_data: *const libc::c_char,
|
||||
_public_data: *const libc::c_char,
|
||||
secret_data: *const libc::c_char,
|
||||
) -> i32 {
|
||||
if context.is_null() {
|
||||
@@ -793,8 +815,8 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
let ctx = &*context;
|
||||
block_on(async move {
|
||||
let addr = tools::EmailAddress::new(&to_string_lossy(addr))?;
|
||||
let public = key::SignedPublicKey::from_asc(&to_string_lossy(public_data))?.0;
|
||||
let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
|
||||
let public = secret.split_public_key()?;
|
||||
let keypair = key::KeyPair {
|
||||
addr,
|
||||
public,
|
||||
@@ -1238,6 +1260,30 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_similar_chatlist(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
) -> *mut dc_chatlist_t {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_similar_chatlist()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
match block_on(chat_id.get_similar_chatlist(ctx))
|
||||
.context("failed to get similar chatlist")
|
||||
.log_err(ctx)
|
||||
{
|
||||
Ok(list) => {
|
||||
let ffi_list = ChatlistWrapper { context, list };
|
||||
Box::into_raw(Box::new(ffi_list))
|
||||
}
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_estimate_deletion_cnt(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1385,6 +1431,7 @@ pub unsafe extern "C" fn dc_get_chat_media(
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[allow(deprecated)]
|
||||
pub unsafe extern "C" fn dc_get_next_media(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
@@ -1873,13 +1920,10 @@ pub unsafe extern "C" fn dc_get_msg_info(
|
||||
return "".strdup();
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
message::get_msg_info(ctx, MsgId::new(msg_id))
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to get msg id")
|
||||
.strdup()
|
||||
})
|
||||
let msg_id = MsgId::new(msg_id);
|
||||
block_on(msg_id.get_info(ctx))
|
||||
.unwrap_or_log_default(ctx, "failed to get msg id")
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2524,7 +2568,12 @@ pub unsafe extern "C" fn dc_set_location(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(location::set(ctx, latitude, longitude, accuracy)) as _
|
||||
block_on(async move {
|
||||
location::set(ctx, latitude, longitude, accuracy)
|
||||
.await
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
}) as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3304,7 +3353,7 @@ pub unsafe extern "C" fn dc_msg_get_text(msg: *mut dc_msg_t) -> *mut libc::c_cha
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_text().unwrap_or_default().strdup()
|
||||
ffi_msg.message.get_text().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3689,7 +3738,7 @@ pub unsafe extern "C" fn dc_msg_set_text(msg: *mut dc_msg_t, text: *const libc::
|
||||
return;
|
||||
}
|
||||
let ffi_msg = &mut *msg;
|
||||
ffi_msg.message.set_text(to_opt_string_lossy(text))
|
||||
ffi_msg.message.set_text(to_string_lossy(text))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -4488,7 +4537,14 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
|
||||
|
||||
let ctx = &*context;
|
||||
|
||||
match block_on(provider::get_provider_info(ctx, addr.as_str(), true)) {
|
||||
match block_on(provider::get_provider_info_by_addr(
|
||||
ctx,
|
||||
addr.as_str(),
|
||||
true,
|
||||
))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
@@ -4515,11 +4571,14 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
|
||||
|
||||
match socks5_enabled {
|
||||
Ok(socks5_enabled) => {
|
||||
match block_on(provider::get_provider_info(
|
||||
match block_on(provider::get_provider_info_by_addr(
|
||||
ctx,
|
||||
addr.as_str(),
|
||||
socks5_enabled,
|
||||
)) {
|
||||
))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
@@ -4967,7 +5026,7 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||
#[cfg(feature = "jsonrpc")]
|
||||
mod jsonrpc {
|
||||
use deltachat_jsonrpc::api::CommandApi;
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcServer, RpcSession};
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -5043,25 +5102,24 @@ mod jsonrpc {
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
|
||||
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
|
||||
method: *const libc::c_char,
|
||||
params: *const libc::c_char,
|
||||
input: *const libc::c_char,
|
||||
) -> *mut libc::c_char {
|
||||
if jsonrpc_instance.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let api = &*jsonrpc_instance;
|
||||
let method = to_string_lossy(method);
|
||||
let params = to_string_lossy(params);
|
||||
let params: Option<yerpc::Params> = match serde_json::from_str(¶ms) {
|
||||
Ok(params) => Some(params),
|
||||
Err(_) => None,
|
||||
};
|
||||
let params = params.map(yerpc::Params::into_value).unwrap_or_default();
|
||||
let res = block_on(api.handle.server().handle_request(method, params));
|
||||
let input = to_string_lossy(input);
|
||||
let res = block_on(api.handle.process_incoming(&input));
|
||||
match res {
|
||||
Ok(res) => res.to_string().strdup(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
Some(message) => {
|
||||
if let Ok(message) = serde_json::to_string(&message) {
|
||||
message.strdup()
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
1
deltachat-jsonrpc/.gitignore
vendored
1
deltachat-jsonrpc/.gitignore
vendored
@@ -1,3 +1,4 @@
|
||||
openrpc/openrpc.json
|
||||
accounts/
|
||||
|
||||
.cargo
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.115.0"
|
||||
version = "1.126.1"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
@@ -15,15 +15,16 @@ required-features = ["webserver"]
|
||||
anyhow = "1"
|
||||
deltachat = { path = ".." }
|
||||
num-traits = "0.2"
|
||||
schemars = "0.8.11"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tempfile = "3.3.0"
|
||||
tempfile = "3.6.0"
|
||||
log = "0.4"
|
||||
async-channel = { version = "1.8.0" }
|
||||
futures = { version = "0.3.28" }
|
||||
serde_json = "1.0.96"
|
||||
yerpc = { version = "0.4.4", features = ["anyhow_expose"] }
|
||||
serde_json = "1.0.99"
|
||||
yerpc = { version = "0.5.1", features = ["anyhow_expose", "openrpc"] }
|
||||
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
|
||||
tokio = { version = "1.28.0" }
|
||||
tokio = { version = "1.29.1" }
|
||||
sanitize-filename = "0.4"
|
||||
walkdir = "2.3.3"
|
||||
base64 = "0.21"
|
||||
@@ -33,7 +34,7 @@ axum = { version = "0.6.18", optional = true, features = ["ws"] }
|
||||
env_logger = { version = "0.10.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.28.0", features = ["full", "rt-multi-thread"] }
|
||||
tokio = { version = "1.29.1", features = ["full", "rt-multi-thread"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
@@ -19,9 +19,7 @@ use deltachat::{
|
||||
context::get_info,
|
||||
ephemeral::Timer,
|
||||
imex, location,
|
||||
message::{
|
||||
self, delete_msgs, get_msg_info, markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
||||
},
|
||||
message::{self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype},
|
||||
provider::get_provider_info,
|
||||
qr,
|
||||
qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg},
|
||||
@@ -144,7 +142,11 @@ impl CommandApi {
|
||||
}
|
||||
}
|
||||
|
||||
#[rpc(all_positional, ts_outdir = "typescript/generated")]
|
||||
#[rpc(
|
||||
all_positional,
|
||||
ts_outdir = "typescript/generated",
|
||||
openrpc_outdir = "openrpc"
|
||||
)]
|
||||
impl CommandApi {
|
||||
/// Test function.
|
||||
async fn sleep(&self, delay: f64) {
|
||||
@@ -564,6 +566,21 @@ impl CommandApi {
|
||||
Ok(l)
|
||||
}
|
||||
|
||||
/// Returns chats similar to the given one.
|
||||
///
|
||||
/// Experimental API, subject to change without notice.
|
||||
async fn get_similar_chat_ids(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let chat_id = ChatId::new(chat_id);
|
||||
let list = chat_id
|
||||
.get_similar_chat_ids(&ctx)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|(chat_id, _metric)| chat_id.to_u32())
|
||||
.collect();
|
||||
Ok(list)
|
||||
}
|
||||
|
||||
async fn get_chatlist_items_by_entries(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -795,24 +812,12 @@ impl CommandApi {
|
||||
/// Create a new broadcast list.
|
||||
///
|
||||
/// Broadcast lists are similar to groups on the sending device,
|
||||
/// however, recipients get the messages in normal one-to-one chats
|
||||
/// and will not be aware of other members.
|
||||
/// however, recipients get the messages in a read-only chat
|
||||
/// and will see who the other members are.
|
||||
///
|
||||
/// Replies to broadcasts go only to the sender
|
||||
/// and not to all broadcast recipients.
|
||||
/// Moreover, replies will not appear in the broadcast list
|
||||
/// but in the one-to-one chat with the person answering.
|
||||
///
|
||||
/// The name and the image of the broadcast list is set automatically
|
||||
/// and is visible to the sender only.
|
||||
/// Not asking for these data allows more focused creation
|
||||
/// and we bypass the question who will get which data.
|
||||
/// Also, many users will have at most one broadcast list
|
||||
/// so, a generic name and image is sufficient at the first place.
|
||||
///
|
||||
/// Later on, however, the name can be changed using dc_set_chat_name().
|
||||
/// The image cannot be changed to have a unique, recognizable icon in the chat lists.
|
||||
/// All in all, this is also what other messengers are doing here.
|
||||
/// For historical reasons, this function does not take a name directly,
|
||||
/// instead you have to set the name using dc_set_chat_name()
|
||||
/// after creating the broadcast list.
|
||||
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
chat::create_broadcast_list(&ctx)
|
||||
@@ -897,7 +902,7 @@ impl CommandApi {
|
||||
) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some(text));
|
||||
msg.set_text(text);
|
||||
let message_id =
|
||||
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?;
|
||||
Ok(message_id.to_u32())
|
||||
@@ -1115,7 +1120,7 @@ impl CommandApi {
|
||||
/// max. text returned by dc_msg_get_text() (about 30000 characters).
|
||||
async fn get_message_info(&self, account_id: u32, message_id: u32) -> Result<String> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
get_msg_info(&ctx, MsgId::new(message_id)).await
|
||||
MsgId::new(message_id).get_info(&ctx).await
|
||||
}
|
||||
|
||||
/// Returns contacts that sent read receipts and the time of reading.
|
||||
@@ -1336,7 +1341,7 @@ impl CommandApi {
|
||||
) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let contact_id = ContactId::new(contact_id);
|
||||
let contact = Contact::load_from_db(&ctx, contact_id).await?;
|
||||
let contact = Contact::get_by_id(&ctx, contact_id).await?;
|
||||
let addr = contact.get_addr();
|
||||
Contact::create(&ctx, &name, addr).await?;
|
||||
Ok(())
|
||||
@@ -1410,6 +1415,10 @@ impl CommandApi {
|
||||
///
|
||||
/// one combined call for getting chat::get_next_media for both directions
|
||||
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
|
||||
///
|
||||
/// Deprecated 2023-10-03, use `get_chat_media` method
|
||||
/// and navigate the returned array instead.
|
||||
#[allow(deprecated)]
|
||||
async fn get_neighboring_chat_media(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1707,6 +1716,20 @@ impl CommandApi {
|
||||
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
|
||||
}
|
||||
|
||||
/// Resend messages and make information available for newly added chat members.
|
||||
/// Resending sends out the original message, however, recipients and webxdc-status may differ.
|
||||
/// Clients that already have the original message can still ignore the resent message as
|
||||
/// they have tracked the state by dedicated updates.
|
||||
///
|
||||
/// Some messages cannot be resent, eg. info-messages, drafts, already pending messages or messages that are not sent by SELF.
|
||||
///
|
||||
/// message_ids all message IDs that should be resend. All messages must belong to the same chat.
|
||||
async fn resend_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let message_ids: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
|
||||
chat::resend_msgs(&ctx, &message_ids).await
|
||||
}
|
||||
|
||||
async fn send_sticker(
|
||||
&self,
|
||||
account_id: u32,
|
||||
@@ -1763,9 +1786,7 @@ impl CommandApi {
|
||||
} else {
|
||||
Viewtype::Text
|
||||
});
|
||||
if data.text.is_some() {
|
||||
message.set_text(data.text);
|
||||
}
|
||||
message.set_text(data.text.unwrap_or_default());
|
||||
if data.html.is_some() {
|
||||
message.set_html(data.html);
|
||||
}
|
||||
@@ -1946,7 +1967,7 @@ impl CommandApi {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some(text));
|
||||
msg.set_text(text);
|
||||
|
||||
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
|
||||
Ok(message_id.to_u32())
|
||||
@@ -1969,9 +1990,7 @@ impl CommandApi {
|
||||
} else {
|
||||
Viewtype::Text
|
||||
});
|
||||
if text.is_some() {
|
||||
message.set_text(text);
|
||||
}
|
||||
message.set_text(text.unwrap_or_default());
|
||||
if let Some(file) = file {
|
||||
message.set_file(file, None);
|
||||
}
|
||||
@@ -2015,9 +2034,7 @@ impl CommandApi {
|
||||
} else {
|
||||
Viewtype::Text
|
||||
});
|
||||
if text.is_some() {
|
||||
draft.set_text(text);
|
||||
}
|
||||
draft.set_text(text.unwrap_or_default());
|
||||
if let Some(file) = file {
|
||||
draft.set_file(file, None);
|
||||
}
|
||||
@@ -2036,6 +2053,23 @@ impl CommandApi {
|
||||
|
||||
ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await
|
||||
}
|
||||
|
||||
// send the chat's current set draft
|
||||
async fn misc_send_draft(&self, account_id: u32, chat_id: u32) -> Result<u32> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
|
||||
let mut draft = draft;
|
||||
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut draft)
|
||||
.await?
|
||||
.to_u32();
|
||||
Ok(msg_id)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"chat with id {} doesn't have draft message",
|
||||
chat_id
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper functions (to prevent code duplication)
|
||||
|
||||
@@ -6,7 +6,7 @@ use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Account {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
|
||||
@@ -13,7 +13,7 @@ use typescript_type_def::TypeDef;
|
||||
use super::color_int_to_hex_string;
|
||||
use super::contact::ContactObject;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FullChat {
|
||||
id: u32,
|
||||
@@ -53,7 +53,7 @@ impl FullChat {
|
||||
contacts.push(
|
||||
ContactObject::try_from_dc_contact(
|
||||
context,
|
||||
Contact::load_from_db(context, *contact_id)
|
||||
Contact::get_by_id(context, *contact_id)
|
||||
.await
|
||||
.context("failed to load contact")?,
|
||||
)
|
||||
@@ -74,7 +74,7 @@ impl FullChat {
|
||||
|
||||
let was_seen_recently = if chat.get_type() == Chattype::Single {
|
||||
match contact_ids.get(0) {
|
||||
Some(contact) => Contact::load_from_db(context, *contact)
|
||||
Some(contact) => Contact::get_by_id(context, *contact)
|
||||
.await
|
||||
.context("failed to load contact for was_seen_recently")?
|
||||
.was_seen_recently(),
|
||||
@@ -121,7 +121,7 @@ impl FullChat {
|
||||
/// - can_send
|
||||
///
|
||||
/// used when you only need the basic metadata of a chat like type, name, profile picture
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BasicChat {
|
||||
id: u32,
|
||||
@@ -166,7 +166,7 @@ impl BasicChat {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, TypeDef)]
|
||||
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||
pub enum MuteDuration {
|
||||
NotMuted,
|
||||
Forever,
|
||||
@@ -191,7 +191,7 @@ impl MuteDuration {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, TypeDef)]
|
||||
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "ChatVisibility")]
|
||||
pub enum JSONRPCChatVisibility {
|
||||
Normal,
|
||||
|
||||
@@ -14,7 +14,7 @@ use typescript_type_def::TypeDef;
|
||||
use super::color_int_to_hex_string;
|
||||
use super::message::MessageViewtype;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum ChatListItemFetchResult {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -104,7 +104,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
|
||||
let contact = chat_contacts.get(0);
|
||||
let was_seen_recently = match contact {
|
||||
Some(contact) => Contact::load_from_db(ctx, *contact)
|
||||
Some(contact) => Contact::get_by_id(ctx, *contact)
|
||||
.await
|
||||
.context("contact")?
|
||||
.was_seen_recently(),
|
||||
|
||||
@@ -6,7 +6,7 @@ use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Contact", rename_all = "camelCase")]
|
||||
pub struct ContactObject {
|
||||
address: String,
|
||||
|
||||
@@ -2,7 +2,7 @@ use deltachat::{Event as CoreEvent, EventType as CoreEventType};
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Event {
|
||||
/// Event payload.
|
||||
@@ -21,7 +21,7 @@ impl From<CoreEvent> for Event {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum EventType {
|
||||
/// The library-user may write an informational string to the log.
|
||||
@@ -174,6 +174,13 @@ pub enum EventType {
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// A single message is deleted.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgDeleted {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
/// Or the verify state of a chat has changed.
|
||||
/// See setChatName(), setChatProfileImage(), addContactToChat()
|
||||
@@ -347,6 +354,10 @@ impl From<CoreEventType> for EventType {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
CoreEventType::MsgDeleted { chat_id, msg_id } => MsgDeleted {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
CoreEventType::ChatModified(chat_id) => ChatModified {
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
|
||||
@@ -2,7 +2,7 @@ use deltachat::net::HttpResponse as CoreHttpResponse;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
pub struct HttpResponse {
|
||||
/// base64-encoded response body.
|
||||
blob: String,
|
||||
|
||||
@@ -2,7 +2,7 @@ use deltachat::location::Location;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Location", rename_all = "camelCase")]
|
||||
pub struct JsonrpcLocation {
|
||||
pub location_id: u32,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use anyhow::{Context as _, Result};
|
||||
use deltachat::chat::Chat;
|
||||
use deltachat::chat::ChatItem;
|
||||
use deltachat::chat::ChatVisibility;
|
||||
@@ -10,8 +10,7 @@ use deltachat::message::MsgId;
|
||||
use deltachat::message::Viewtype;
|
||||
use deltachat::reaction::get_msg_reactions;
|
||||
use num_traits::cast::ToPrimitive;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
@@ -19,14 +18,14 @@ use super::contact::ContactObject;
|
||||
use super::reactions::JSONRPCReactions;
|
||||
use super::webxdc::WebxdcMessageInfo;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase", tag = "variant")]
|
||||
pub enum MessageLoadResult {
|
||||
Message(MessageObject),
|
||||
LoadingError { error: String },
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Message", rename_all = "camelCase")]
|
||||
pub struct MessageObject {
|
||||
id: u32,
|
||||
@@ -35,7 +34,7 @@ pub struct MessageObject {
|
||||
quote: Option<MessageQuote>,
|
||||
parent_id: Option<u32>,
|
||||
|
||||
text: Option<String>,
|
||||
text: String,
|
||||
has_location: bool,
|
||||
has_html: bool,
|
||||
view_type: MessageViewtype,
|
||||
@@ -86,7 +85,7 @@ pub struct MessageObject {
|
||||
reactions: Option<JSONRPCReactions>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(tag = "kind")]
|
||||
enum MessageQuote {
|
||||
JustText {
|
||||
@@ -114,8 +113,12 @@ impl MessageObject {
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||
let message = Message::load_from_db(context, msg_id).await?;
|
||||
|
||||
let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?;
|
||||
let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?;
|
||||
let sender_contact = Contact::get_by_id(context, message.get_from_id())
|
||||
.await
|
||||
.context("failed to load sender contact")?;
|
||||
let sender = ContactObject::try_from_dc_contact(context, sender_contact)
|
||||
.await
|
||||
.context("failed to load sender contact object")?;
|
||||
let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
|
||||
let override_sender_name = message.get_override_sender_name();
|
||||
|
||||
@@ -132,7 +135,9 @@ impl MessageObject {
|
||||
let quote = if let Some(quoted_text) = message.quoted_text() {
|
||||
match message.quoted_message(context).await? {
|
||||
Some(quote) => {
|
||||
let quote_author = Contact::load_from_db(context, quote.get_from_id()).await?;
|
||||
let quote_author = Contact::get_by_id(context, quote.get_from_id())
|
||||
.await
|
||||
.context("failed to load quote author contact")?;
|
||||
Some(MessageQuote::WithMessage {
|
||||
text: quoted_text,
|
||||
message_id: quote.get_id().to_u32(),
|
||||
@@ -160,7 +165,9 @@ impl MessageObject {
|
||||
None
|
||||
};
|
||||
|
||||
let reactions = get_msg_reactions(context, msg_id).await?;
|
||||
let reactions = get_msg_reactions(context, msg_id)
|
||||
.await
|
||||
.context("failed to load message reactions")?;
|
||||
let reactions = if reactions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
@@ -180,7 +187,7 @@ impl MessageObject {
|
||||
state: message
|
||||
.get_state()
|
||||
.to_u32()
|
||||
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
|
||||
.context("state conversion to number failed")?,
|
||||
error: message.error(),
|
||||
|
||||
timestamp: message.get_timestamp(),
|
||||
@@ -203,7 +210,7 @@ impl MessageObject {
|
||||
videochat_type: match message.get_videochat_type() {
|
||||
Some(vct) => Some(
|
||||
vct.to_u32()
|
||||
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
|
||||
.context("videochat type conversion to number failed")?,
|
||||
),
|
||||
None => None,
|
||||
},
|
||||
@@ -230,7 +237,7 @@ impl MessageObject {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, TypeDef)]
|
||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Viewtype")]
|
||||
pub enum MessageViewtype {
|
||||
Unknown,
|
||||
@@ -306,11 +313,12 @@ impl From<MessageViewtype> for Viewtype {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
pub enum DownloadState {
|
||||
Done,
|
||||
Available,
|
||||
Failure,
|
||||
Undecipherable,
|
||||
InProgress,
|
||||
}
|
||||
|
||||
@@ -320,12 +328,13 @@ impl From<download::DownloadState> for DownloadState {
|
||||
download::DownloadState::Done => DownloadState::Done,
|
||||
download::DownloadState::Available => DownloadState::Available,
|
||||
download::DownloadState::Failure => DownloadState::Failure,
|
||||
download::DownloadState::Undecipherable => DownloadState::Undecipherable,
|
||||
download::DownloadState::InProgress => DownloadState::InProgress,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
pub enum SystemMessageType {
|
||||
Unknown,
|
||||
GroupNameChanged,
|
||||
@@ -380,7 +389,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageNotificationInfo {
|
||||
id: u32,
|
||||
@@ -438,7 +447,7 @@ impl MessageNotificationInfo {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageSearchResult {
|
||||
id: u32,
|
||||
@@ -462,7 +471,7 @@ impl MessageSearchResult {
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||
let message = Message::load_from_db(context, msg_id).await?;
|
||||
let chat = Chat::load_from_db(context, message.get_chat_id()).await?;
|
||||
let sender = Contact::load_from_db(context, message.get_from_id()).await?;
|
||||
let sender = Contact::get_by_id(context, message.get_from_id()).await?;
|
||||
|
||||
let profile_image = match sender.get_profile_image(context).await? {
|
||||
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||
@@ -493,13 +502,13 @@ impl MessageSearchResult {
|
||||
is_chat_protected: chat.is_protected(),
|
||||
is_chat_contact_request: chat.is_contact_request(),
|
||||
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
|
||||
message: message.get_text().unwrap_or_default(),
|
||||
message: message.get_text(),
|
||||
timestamp: message.get_timestamp(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")]
|
||||
pub enum JSONRPCMessageListItem {
|
||||
Message {
|
||||
@@ -525,7 +534,7 @@ impl From<ChatItem> for JSONRPCMessageListItem {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, TypeDef)]
|
||||
#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageData {
|
||||
pub text: Option<String>,
|
||||
@@ -537,7 +546,7 @@ pub struct MessageData {
|
||||
pub quoted_message_id: Option<u32>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageReadReceipt {
|
||||
pub contact_id: u32,
|
||||
|
||||
@@ -3,7 +3,7 @@ use num_traits::cast::ToPrimitive;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ProviderInfo {
|
||||
pub before_login_hint: String,
|
||||
|
||||
@@ -2,7 +2,7 @@ use deltachat::qr::Qr;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum QrObject {
|
||||
|
||||
@@ -6,7 +6,7 @@ use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
/// A single reaction emoji.
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Reaction", rename_all = "camelCase")]
|
||||
pub struct JSONRPCReaction {
|
||||
/// Emoji.
|
||||
@@ -20,7 +20,7 @@ pub struct JSONRPCReaction {
|
||||
}
|
||||
|
||||
/// Structure representing all reactions to a particular message.
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "Reactions", rename_all = "camelCase")]
|
||||
pub struct JSONRPCReactions {
|
||||
/// Map from a contact to it's reaction to message.
|
||||
|
||||
@@ -8,7 +8,7 @@ use typescript_type_def::TypeDef;
|
||||
|
||||
use super::maybe_empty_string_to_option;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||
#[serde(rename = "WebxdcMessageInfo", rename_all = "camelCase")]
|
||||
pub struct WebxdcMessageInfo {
|
||||
/// The name of the app.
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.115.0"
|
||||
"version": "1.126.1"
|
||||
}
|
||||
|
||||
@@ -148,7 +148,7 @@ describe("online tests", function () {
|
||||
waitForEvent(dc, "IncomingMsg", accountId1),
|
||||
]);
|
||||
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
|
||||
// Check if answer arives at A and if it is encrypted
|
||||
// Check if answer arrives at A and if it is encrypted
|
||||
await eventPromise2;
|
||||
|
||||
const messageId = (
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.115.0"
|
||||
version = "1.126.1"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
|
||||
@@ -9,10 +9,10 @@ ansi_term = "0.12.1"
|
||||
anyhow = "1"
|
||||
deltachat = { path = "..", features = ["internals"]}
|
||||
dirs = "5"
|
||||
log = "0.4.16"
|
||||
pretty_env_logger = "0.4"
|
||||
log = "0.4.19"
|
||||
pretty_env_logger = "0.5"
|
||||
rusqlite = "0.29"
|
||||
rustyline = "11"
|
||||
rustyline = "12"
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
|
||||
[features]
|
||||
|
||||
@@ -138,11 +138,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
/* import a directory */
|
||||
let dir_name = std::path::Path::new(&real_spec);
|
||||
let dir = fs::read_dir(dir_name).await;
|
||||
if dir.is_err() {
|
||||
error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
|
||||
return false;
|
||||
} else {
|
||||
let mut dir = dir.unwrap();
|
||||
if let Ok(mut dir) = dir {
|
||||
while let Ok(Some(entry)) = dir.next_entry().await {
|
||||
let name_f = entry.file_name();
|
||||
let name = name_f.to_string_lossy();
|
||||
@@ -154,6 +150,9 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
|
||||
@@ -187,6 +186,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
DownloadState::Available => " [⬇ Download available]",
|
||||
DownloadState::InProgress => " [⬇ Download in progress...]️",
|
||||
DownloadState::Failure => " [⬇ Download failed]",
|
||||
DownloadState::Undecipherable => " [⬇ Decryption failed]",
|
||||
};
|
||||
|
||||
let temp2 = timestamp_to_str(msg.get_timestamp());
|
||||
@@ -199,7 +199,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
if msg.has_location() { "📍" } else { "" },
|
||||
&contact_name,
|
||||
contact_id,
|
||||
msgtext.unwrap_or_default(),
|
||||
msgtext,
|
||||
if msg.has_html() { "[HAS-HTML]️" } else { "" },
|
||||
if msg.get_from_id() == ContactId::SELF {
|
||||
""
|
||||
@@ -805,15 +805,30 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"chatinfo" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
let sel_chat_id = sel_chat.as_ref().unwrap().get_id();
|
||||
|
||||
let contacts =
|
||||
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||
let contacts = chat::get_chat_contacts(&context, sel_chat_id).await?;
|
||||
println!("Memberlist:");
|
||||
|
||||
log_contactlist(&context, &contacts).await?;
|
||||
println!("{} contacts", contacts.len());
|
||||
|
||||
let similar_chats = sel_chat_id.get_similar_chat_ids(&context).await?;
|
||||
if !similar_chats.is_empty() {
|
||||
println!("Similar chats: ");
|
||||
for (similar_chat_id, metric) in similar_chats {
|
||||
let similar_chat = Chat::load_from_db(&context, similar_chat_id).await?;
|
||||
println!(
|
||||
"{} (#{}) {:.1}",
|
||||
similar_chat.name,
|
||||
similar_chat_id,
|
||||
100.0 * metric
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"{} contacts\nLocation streaming: {}",
|
||||
contacts.len(),
|
||||
"Location streaming: {}",
|
||||
location::is_sending_locations_to_chat(
|
||||
&context,
|
||||
Some(sel_chat.as_ref().unwrap().get_id())
|
||||
@@ -878,7 +893,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let latitude = arg1.parse()?;
|
||||
let longitude = arg2.parse()?;
|
||||
|
||||
let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
|
||||
let continue_streaming = location::set(&context, latitude, longitude, 0.).await?;
|
||||
if continue_streaming {
|
||||
println!("Success, streaming should be continued.");
|
||||
} else {
|
||||
@@ -912,9 +927,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
Viewtype::File
|
||||
});
|
||||
msg.set_file(arg1, None);
|
||||
if !arg2.is_empty() {
|
||||
msg.set_text(Some(arg2.to_string()));
|
||||
}
|
||||
msg.set_text(arg2.to_string());
|
||||
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
||||
}
|
||||
"sendhtml" => {
|
||||
@@ -926,11 +939,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_html(Some(html.to_string()));
|
||||
msg.set_text(Some(if arg2.is_empty() {
|
||||
msg.set_text(if arg2.is_empty() {
|
||||
path.file_name().unwrap().to_string_lossy().to_string()
|
||||
} else {
|
||||
arg2.to_string()
|
||||
}));
|
||||
});
|
||||
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
||||
}
|
||||
"sendsyncmsg" => match context.send_sync_msg().await? {
|
||||
@@ -979,7 +992,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
if !arg1.is_empty() {
|
||||
let mut draft = Message::new(Viewtype::Text);
|
||||
draft.set_text(Some(arg1.to_string()));
|
||||
draft.set_text(arg1.to_string());
|
||||
sel_chat
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
@@ -1003,7 +1016,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"Please specify text to add as device message."
|
||||
);
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.set_text(Some(arg1.to_string()));
|
||||
msg.set_text(arg1.to_string());
|
||||
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
|
||||
}
|
||||
"listmedia" => {
|
||||
@@ -1090,7 +1103,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"msginfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let id = MsgId::new(arg1.parse()?);
|
||||
let res = message::get_msg_info(&context, id).await?;
|
||||
let res = id.get_info(&context).await?;
|
||||
println!("{res}");
|
||||
}
|
||||
"download" => {
|
||||
|
||||
373
deltachat-rpc-client/LICENSE
Normal file
373
deltachat-rpc-client/LICENSE
Normal file
@@ -0,0 +1,373 @@
|
||||
Mozilla Public License Version 2.0
|
||||
==================================
|
||||
|
||||
1. Definitions
|
||||
--------------
|
||||
|
||||
1.1. "Contributor"
|
||||
means each individual or legal entity that creates, contributes to
|
||||
the creation of, or owns Covered Software.
|
||||
|
||||
1.2. "Contributor Version"
|
||||
means the combination of the Contributions of others (if any) used
|
||||
by a Contributor and that particular Contributor's Contribution.
|
||||
|
||||
1.3. "Contribution"
|
||||
means Covered Software of a particular Contributor.
|
||||
|
||||
1.4. "Covered Software"
|
||||
means Source Code Form to which the initial Contributor has attached
|
||||
the notice in Exhibit A, the Executable Form of such Source Code
|
||||
Form, and Modifications of such Source Code Form, in each case
|
||||
including portions thereof.
|
||||
|
||||
1.5. "Incompatible With Secondary Licenses"
|
||||
means
|
||||
|
||||
(a) that the initial Contributor has attached the notice described
|
||||
in Exhibit B to the Covered Software; or
|
||||
|
||||
(b) that the Covered Software was made available under the terms of
|
||||
version 1.1 or earlier of the License, but not also under the
|
||||
terms of a Secondary License.
|
||||
|
||||
1.6. "Executable Form"
|
||||
means any form of the work other than Source Code Form.
|
||||
|
||||
1.7. "Larger Work"
|
||||
means a work that combines Covered Software with other material, in
|
||||
a separate file or files, that is not Covered Software.
|
||||
|
||||
1.8. "License"
|
||||
means this document.
|
||||
|
||||
1.9. "Licensable"
|
||||
means having the right to grant, to the maximum extent possible,
|
||||
whether at the time of the initial grant or subsequently, any and
|
||||
all of the rights conveyed by this License.
|
||||
|
||||
1.10. "Modifications"
|
||||
means any of the following:
|
||||
|
||||
(a) any file in Source Code Form that results from an addition to,
|
||||
deletion from, or modification of the contents of Covered
|
||||
Software; or
|
||||
|
||||
(b) any new file in Source Code Form that contains any Covered
|
||||
Software.
|
||||
|
||||
1.11. "Patent Claims" of a Contributor
|
||||
means any patent claim(s), including without limitation, method,
|
||||
process, and apparatus claims, in any patent Licensable by such
|
||||
Contributor that would be infringed, but for the grant of the
|
||||
License, by the making, using, selling, offering for sale, having
|
||||
made, import, or transfer of either its Contributions or its
|
||||
Contributor Version.
|
||||
|
||||
1.12. "Secondary License"
|
||||
means either the GNU General Public License, Version 2.0, the GNU
|
||||
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||
Public License, Version 3.0, or any later versions of those
|
||||
licenses.
|
||||
|
||||
1.13. "Source Code Form"
|
||||
means the form of the work preferred for making modifications.
|
||||
|
||||
1.14. "You" (or "Your")
|
||||
means an individual or a legal entity exercising rights under this
|
||||
License. For legal entities, "You" includes any entity that
|
||||
controls, is controlled by, or is under common control with You. For
|
||||
purposes of this definition, "control" means (a) the power, direct
|
||||
or indirect, to cause the direction or management of such entity,
|
||||
whether by contract or otherwise, or (b) ownership of more than
|
||||
fifty percent (50%) of the outstanding shares or beneficial
|
||||
ownership of such entity.
|
||||
|
||||
2. License Grants and Conditions
|
||||
--------------------------------
|
||||
|
||||
2.1. Grants
|
||||
|
||||
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||
non-exclusive license:
|
||||
|
||||
(a) under intellectual property rights (other than patent or trademark)
|
||||
Licensable by such Contributor to use, reproduce, make available,
|
||||
modify, display, perform, distribute, and otherwise exploit its
|
||||
Contributions, either on an unmodified basis, with Modifications, or
|
||||
as part of a Larger Work; and
|
||||
|
||||
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||
for sale, have made, import, and otherwise transfer either its
|
||||
Contributions or its Contributor Version.
|
||||
|
||||
2.2. Effective Date
|
||||
|
||||
The licenses granted in Section 2.1 with respect to any Contribution
|
||||
become effective for each Contribution on the date the Contributor first
|
||||
distributes such Contribution.
|
||||
|
||||
2.3. Limitations on Grant Scope
|
||||
|
||||
The licenses granted in this Section 2 are the only rights granted under
|
||||
this License. No additional rights or licenses will be implied from the
|
||||
distribution or licensing of Covered Software under this License.
|
||||
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||
Contributor:
|
||||
|
||||
(a) for any code that a Contributor has removed from Covered Software;
|
||||
or
|
||||
|
||||
(b) for infringements caused by: (i) Your and any other third party's
|
||||
modifications of Covered Software, or (ii) the combination of its
|
||||
Contributions with other software (except as part of its Contributor
|
||||
Version); or
|
||||
|
||||
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||
its Contributions.
|
||||
|
||||
This License does not grant any rights in the trademarks, service marks,
|
||||
or logos of any Contributor (except as may be necessary to comply with
|
||||
the notice requirements in Section 3.4).
|
||||
|
||||
2.4. Subsequent Licenses
|
||||
|
||||
No Contributor makes additional grants as a result of Your choice to
|
||||
distribute the Covered Software under a subsequent version of this
|
||||
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||
permitted under the terms of Section 3.3).
|
||||
|
||||
2.5. Representation
|
||||
|
||||
Each Contributor represents that the Contributor believes its
|
||||
Contributions are its original creation(s) or it has sufficient rights
|
||||
to grant the rights to its Contributions conveyed by this License.
|
||||
|
||||
2.6. Fair Use
|
||||
|
||||
This License is not intended to limit any rights You have under
|
||||
applicable copyright doctrines of fair use, fair dealing, or other
|
||||
equivalents.
|
||||
|
||||
2.7. Conditions
|
||||
|
||||
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||
in Section 2.1.
|
||||
|
||||
3. Responsibilities
|
||||
-------------------
|
||||
|
||||
3.1. Distribution of Source Form
|
||||
|
||||
All distribution of Covered Software in Source Code Form, including any
|
||||
Modifications that You create or to which You contribute, must be under
|
||||
the terms of this License. You must inform recipients that the Source
|
||||
Code Form of the Covered Software is governed by the terms of this
|
||||
License, and how they can obtain a copy of this License. You may not
|
||||
attempt to alter or restrict the recipients' rights in the Source Code
|
||||
Form.
|
||||
|
||||
3.2. Distribution of Executable Form
|
||||
|
||||
If You distribute Covered Software in Executable Form then:
|
||||
|
||||
(a) such Covered Software must also be made available in Source Code
|
||||
Form, as described in Section 3.1, and You must inform recipients of
|
||||
the Executable Form how they can obtain a copy of such Source Code
|
||||
Form by reasonable means in a timely manner, at a charge no more
|
||||
than the cost of distribution to the recipient; and
|
||||
|
||||
(b) You may distribute such Executable Form under the terms of this
|
||||
License, or sublicense it under different terms, provided that the
|
||||
license for the Executable Form does not attempt to limit or alter
|
||||
the recipients' rights in the Source Code Form under this License.
|
||||
|
||||
3.3. Distribution of a Larger Work
|
||||
|
||||
You may create and distribute a Larger Work under terms of Your choice,
|
||||
provided that You also comply with the requirements of this License for
|
||||
the Covered Software. If the Larger Work is a combination of Covered
|
||||
Software with a work governed by one or more Secondary Licenses, and the
|
||||
Covered Software is not Incompatible With Secondary Licenses, this
|
||||
License permits You to additionally distribute such Covered Software
|
||||
under the terms of such Secondary License(s), so that the recipient of
|
||||
the Larger Work may, at their option, further distribute the Covered
|
||||
Software under the terms of either this License or such Secondary
|
||||
License(s).
|
||||
|
||||
3.4. Notices
|
||||
|
||||
You may not remove or alter the substance of any license notices
|
||||
(including copyright notices, patent notices, disclaimers of warranty,
|
||||
or limitations of liability) contained within the Source Code Form of
|
||||
the Covered Software, except that You may alter any license notices to
|
||||
the extent required to remedy known factual inaccuracies.
|
||||
|
||||
3.5. Application of Additional Terms
|
||||
|
||||
You may choose to offer, and to charge a fee for, warranty, support,
|
||||
indemnity or liability obligations to one or more recipients of Covered
|
||||
Software. However, You may do so only on Your own behalf, and not on
|
||||
behalf of any Contributor. You must make it absolutely clear that any
|
||||
such warranty, support, indemnity, or liability obligation is offered by
|
||||
You alone, and You hereby agree to indemnify every Contributor for any
|
||||
liability incurred by such Contributor as a result of warranty, support,
|
||||
indemnity or liability terms You offer. You may include additional
|
||||
disclaimers of warranty and limitations of liability specific to any
|
||||
jurisdiction.
|
||||
|
||||
4. Inability to Comply Due to Statute or Regulation
|
||||
---------------------------------------------------
|
||||
|
||||
If it is impossible for You to comply with any of the terms of this
|
||||
License with respect to some or all of the Covered Software due to
|
||||
statute, judicial order, or regulation then You must: (a) comply with
|
||||
the terms of this License to the maximum extent possible; and (b)
|
||||
describe the limitations and the code they affect. Such description must
|
||||
be placed in a text file included with all distributions of the Covered
|
||||
Software under this License. Except to the extent prohibited by statute
|
||||
or regulation, such description must be sufficiently detailed for a
|
||||
recipient of ordinary skill to be able to understand it.
|
||||
|
||||
5. Termination
|
||||
--------------
|
||||
|
||||
5.1. The rights granted under this License will terminate automatically
|
||||
if You fail to comply with any of its terms. However, if You become
|
||||
compliant, then the rights granted under this License from a particular
|
||||
Contributor are reinstated (a) provisionally, unless and until such
|
||||
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||
ongoing basis, if such Contributor fails to notify You of the
|
||||
non-compliance by some reasonable means prior to 60 days after You have
|
||||
come back into compliance. Moreover, Your grants from a particular
|
||||
Contributor are reinstated on an ongoing basis if such Contributor
|
||||
notifies You of the non-compliance by some reasonable means, this is the
|
||||
first time You have received notice of non-compliance with this License
|
||||
from such Contributor, and You become compliant prior to 30 days after
|
||||
Your receipt of the notice.
|
||||
|
||||
5.2. If You initiate litigation against any entity by asserting a patent
|
||||
infringement claim (excluding declaratory judgment actions,
|
||||
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||
directly or indirectly infringes any patent, then the rights granted to
|
||||
You by any and all Contributors for the Covered Software under Section
|
||||
2.1 of this License shall terminate.
|
||||
|
||||
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||
end user license agreements (excluding distributors and resellers) which
|
||||
have been validly granted by You or Your distributors under this License
|
||||
prior to termination shall survive termination.
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 6. Disclaimer of Warranty *
|
||||
* ------------------------- *
|
||||
* *
|
||||
* Covered Software is provided under this License on an "as is" *
|
||||
* basis, without warranty of any kind, either expressed, implied, or *
|
||||
* statutory, including, without limitation, warranties that the *
|
||||
* Covered Software is free of defects, merchantable, fit for a *
|
||||
* particular purpose or non-infringing. The entire risk as to the *
|
||||
* quality and performance of the Covered Software is with You. *
|
||||
* Should any Covered Software prove defective in any respect, You *
|
||||
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||
* essential part of this License. No use of any Covered Software is *
|
||||
* authorized under this License except under this disclaimer. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
************************************************************************
|
||||
* *
|
||||
* 7. Limitation of Liability *
|
||||
* -------------------------- *
|
||||
* *
|
||||
* Under no circumstances and under no legal theory, whether tort *
|
||||
* (including negligence), contract, or otherwise, shall any *
|
||||
* Contributor, or anyone who distributes Covered Software as *
|
||||
* permitted above, be liable to You for any direct, indirect, *
|
||||
* special, incidental, or consequential damages of any character *
|
||||
* including, without limitation, damages for lost profits, loss of *
|
||||
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||
* and all other commercial damages or losses, even if such party *
|
||||
* shall have been informed of the possibility of such damages. This *
|
||||
* limitation of liability shall not apply to liability for death or *
|
||||
* personal injury resulting from such party's negligence to the *
|
||||
* extent applicable law prohibits such limitation. Some *
|
||||
* jurisdictions do not allow the exclusion or limitation of *
|
||||
* incidental or consequential damages, so this exclusion and *
|
||||
* limitation may not apply to You. *
|
||||
* *
|
||||
************************************************************************
|
||||
|
||||
8. Litigation
|
||||
-------------
|
||||
|
||||
Any litigation relating to this License may be brought only in the
|
||||
courts of a jurisdiction where the defendant maintains its principal
|
||||
place of business and such litigation shall be governed by laws of that
|
||||
jurisdiction, without reference to its conflict-of-law provisions.
|
||||
Nothing in this Section shall prevent a party's ability to bring
|
||||
cross-claims or counter-claims.
|
||||
|
||||
9. Miscellaneous
|
||||
----------------
|
||||
|
||||
This License represents the complete agreement concerning the subject
|
||||
matter hereof. If any provision of this License is held to be
|
||||
unenforceable, such provision shall be reformed only to the extent
|
||||
necessary to make it enforceable. Any law or regulation which provides
|
||||
that the language of a contract shall be construed against the drafter
|
||||
shall not be used to construe this License against a Contributor.
|
||||
|
||||
10. Versions of the License
|
||||
---------------------------
|
||||
|
||||
10.1. New Versions
|
||||
|
||||
Mozilla Foundation is the license steward. Except as provided in Section
|
||||
10.3, no one other than the license steward has the right to modify or
|
||||
publish new versions of this License. Each version will be given a
|
||||
distinguishing version number.
|
||||
|
||||
10.2. Effect of New Versions
|
||||
|
||||
You may distribute the Covered Software under the terms of the version
|
||||
of the License under which You originally received the Covered Software,
|
||||
or under the terms of any subsequent version published by the license
|
||||
steward.
|
||||
|
||||
10.3. Modified Versions
|
||||
|
||||
If you create software not governed by this License, and you want to
|
||||
create a new license for such software, you may create and use a
|
||||
modified version of this License if you rename the license and remove
|
||||
any references to the name of the license steward (except to note that
|
||||
such modified license differs from this License).
|
||||
|
||||
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||
Licenses
|
||||
|
||||
If You choose to distribute Source Code Form that is Incompatible With
|
||||
Secondary Licenses under the terms of this version of the License, the
|
||||
notice described in Exhibit B of this License must be attached.
|
||||
|
||||
Exhibit A - Source Code Form License Notice
|
||||
-------------------------------------------
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
file in a relevant directory) where a recipient would be likely to look
|
||||
for such a notice.
|
||||
|
||||
You may add additional accurate notices of copyright ownership.
|
||||
|
||||
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||
---------------------------------------------------------
|
||||
|
||||
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||
defined by the Mozilla Public License, v. 2.0.
|
||||
@@ -37,19 +37,14 @@ $ tox --devenv env
|
||||
$ . env/bin/activate
|
||||
```
|
||||
|
||||
It is recommended to use IPython, because it supports using `await` directly
|
||||
from the REPL.
|
||||
|
||||
```
|
||||
$ pip install ipython
|
||||
$ PATH="../target/debug:$PATH" ipython
|
||||
...
|
||||
In [1]: from deltachat_rpc_client import *
|
||||
In [2]: rpc = Rpc()
|
||||
In [3]: await rpc.start()
|
||||
In [4]: dc = DeltaChat(rpc)
|
||||
In [5]: system_info = await dc.get_system_info()
|
||||
In [6]: system_info["level"]
|
||||
Out[6]: 'awesome'
|
||||
In [7]: await rpc.close()
|
||||
$ python
|
||||
>>> from deltachat_rpc_client import *
|
||||
>>> rpc = Rpc()
|
||||
>>> rpc.start()
|
||||
>>> dc = DeltaChat(rpc)
|
||||
>>> system_info = dc.get_system_info()
|
||||
>>> system_info["level"]
|
||||
'awesome'
|
||||
>>> rpc.close()
|
||||
```
|
||||
|
||||
@@ -4,23 +4,21 @@
|
||||
it will echo back any text send to it, it also will print to console all Delta Chat core events.
|
||||
Pass --help to the CLI to see available options.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from deltachat_rpc_client import events, run_bot_cli
|
||||
|
||||
hooks = events.HookCollection()
|
||||
|
||||
|
||||
@hooks.on(events.RawEvent)
|
||||
async def log_event(event):
|
||||
def log_event(event):
|
||||
print(event)
|
||||
|
||||
|
||||
@hooks.on(events.NewMessage)
|
||||
async def echo(event):
|
||||
def echo(event):
|
||||
snapshot = event.message_snapshot
|
||||
await snapshot.chat.send_text(snapshot.text)
|
||||
snapshot.chat.send_text(snapshot.text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run_bot_cli(hooks))
|
||||
run_bot_cli(hooks)
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
it will echo back any message that has non-empty text and also supports the /help command.
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from threading import Thread
|
||||
|
||||
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
|
||||
|
||||
@@ -13,7 +13,7 @@ hooks = events.HookCollection()
|
||||
|
||||
|
||||
@hooks.on(events.RawEvent)
|
||||
async def log_event(event):
|
||||
def log_event(event):
|
||||
if event.type == EventType.INFO:
|
||||
logging.info(event.msg)
|
||||
elif event.type == EventType.WARNING:
|
||||
@@ -21,54 +21,54 @@ async def log_event(event):
|
||||
|
||||
|
||||
@hooks.on(events.RawEvent(EventType.ERROR))
|
||||
async def log_error(event):
|
||||
def log_error(event):
|
||||
logging.error(event.msg)
|
||||
|
||||
|
||||
@hooks.on(events.MemberListChanged)
|
||||
async def on_memberlist_changed(event):
|
||||
def on_memberlist_changed(event):
|
||||
logging.info("member %s was %s", event.member, "added" if event.member_added else "removed")
|
||||
|
||||
|
||||
@hooks.on(events.GroupImageChanged)
|
||||
async def on_group_image_changed(event):
|
||||
def on_group_image_changed(event):
|
||||
logging.info("group image %s", "deleted" if event.image_deleted else "changed")
|
||||
|
||||
|
||||
@hooks.on(events.GroupNameChanged)
|
||||
async def on_group_name_changed(event):
|
||||
def on_group_name_changed(event):
|
||||
logging.info("group name changed, old name: %s", event.old_name)
|
||||
|
||||
|
||||
@hooks.on(events.NewMessage(func=lambda e: not e.command))
|
||||
async def echo(event):
|
||||
def echo(event):
|
||||
snapshot = event.message_snapshot
|
||||
if snapshot.text or snapshot.file:
|
||||
await snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
|
||||
snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
|
||||
|
||||
|
||||
@hooks.on(events.NewMessage(command="/help"))
|
||||
async def help_command(event):
|
||||
def help_command(event):
|
||||
snapshot = event.message_snapshot
|
||||
await snapshot.chat.send_text("Send me any message and I will echo it back")
|
||||
snapshot.chat.send_text("Send me any message and I will echo it back")
|
||||
|
||||
|
||||
async def main():
|
||||
async with Rpc() as rpc:
|
||||
def main():
|
||||
with Rpc() as rpc:
|
||||
deltachat = DeltaChat(rpc)
|
||||
system_info = await deltachat.get_system_info()
|
||||
system_info = deltachat.get_system_info()
|
||||
logging.info("Running deltachat core %s", system_info.deltachat_core_version)
|
||||
|
||||
accounts = await deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else await deltachat.add_account()
|
||||
accounts = deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else deltachat.add_account()
|
||||
|
||||
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]))
|
||||
await bot.run_forever()
|
||||
if not bot.is_configured():
|
||||
configure_thread = Thread(run=bot.configure, kwargs={"email": sys.argv[1], "password": sys.argv[2]})
|
||||
configure_thread.start()
|
||||
bot.run_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
asyncio.run(main())
|
||||
main()
|
||||
|
||||
@@ -2,45 +2,44 @@
|
||||
"""
|
||||
Example echo bot without using hooks
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId
|
||||
|
||||
|
||||
async def main():
|
||||
async with Rpc() as rpc:
|
||||
def main():
|
||||
with Rpc() as rpc:
|
||||
deltachat = DeltaChat(rpc)
|
||||
system_info = await deltachat.get_system_info()
|
||||
system_info = deltachat.get_system_info()
|
||||
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
|
||||
|
||||
accounts = await deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else await deltachat.add_account()
|
||||
accounts = deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else deltachat.add_account()
|
||||
|
||||
await account.set_config("bot", "1")
|
||||
if not await account.is_configured():
|
||||
account.set_config("bot", "1")
|
||||
if not account.is_configured():
|
||||
logging.info("Account is not configured, configuring")
|
||||
await account.set_config("addr", sys.argv[1])
|
||||
await account.set_config("mail_pw", sys.argv[2])
|
||||
await account.configure()
|
||||
account.set_config("addr", sys.argv[1])
|
||||
account.set_config("mail_pw", sys.argv[2])
|
||||
account.configure()
|
||||
logging.info("Configured")
|
||||
else:
|
||||
logging.info("Account is already configured")
|
||||
await deltachat.start_io()
|
||||
deltachat.start_io()
|
||||
|
||||
async def process_messages():
|
||||
for message in await account.get_next_messages():
|
||||
snapshot = await message.get_snapshot()
|
||||
def process_messages():
|
||||
for message in account.get_next_messages():
|
||||
snapshot = message.get_snapshot()
|
||||
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
|
||||
await snapshot.chat.send_text(snapshot.text)
|
||||
await snapshot.message.mark_seen()
|
||||
snapshot.chat.send_text(snapshot.text)
|
||||
snapshot.message.mark_seen()
|
||||
|
||||
# Process old messages.
|
||||
await process_messages()
|
||||
process_messages()
|
||||
|
||||
while True:
|
||||
event = await account.wait_for_event()
|
||||
event = account.wait_for_event()
|
||||
if event["type"] == EventType.INFO:
|
||||
logging.info("%s", event["msg"])
|
||||
elif event["type"] == EventType.WARNING:
|
||||
@@ -49,9 +48,9 @@ async def main():
|
||||
logging.error("%s", event["msg"])
|
||||
elif event["type"] == EventType.INCOMING_MSG:
|
||||
logging.info("Got an incoming message")
|
||||
await process_messages()
|
||||
process_messages()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
asyncio.run(main())
|
||||
main()
|
||||
|
||||
@@ -5,9 +5,19 @@ build-backend = "setuptools.build_meta"
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
dependencies = [
|
||||
"aiohttp",
|
||||
"aiodns"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
"Operating System :: MacOS :: MacOS X",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.8",
|
||||
"Programming Language :: Python :: 3.9",
|
||||
"Programming Language :: Python :: 3.10",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Communications :: Email"
|
||||
]
|
||||
dynamic = [
|
||||
"version"
|
||||
@@ -61,3 +71,6 @@ line-length = 120
|
||||
|
||||
[tool.isort]
|
||||
profile = "black"
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
log_cli = true
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Delta Chat asynchronous high-level API"""
|
||||
"""Delta Chat JSON-RPC high-level API"""
|
||||
from ._utils import AttrDict, run_bot_cli, run_client_cli
|
||||
from .account import Account
|
||||
from .chat import Chat
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import re
|
||||
import sys
|
||||
from threading import Thread
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -43,7 +43,7 @@ class AttrDict(dict):
|
||||
super().__setattr__(attr, val)
|
||||
|
||||
|
||||
async def run_client_cli(
|
||||
def run_client_cli(
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs,
|
||||
@@ -54,10 +54,10 @@ async def run_client_cli(
|
||||
"""
|
||||
from .client import Client
|
||||
|
||||
await _run_cli(Client, hooks, argv, **kwargs)
|
||||
_run_cli(Client, hooks, argv, **kwargs)
|
||||
|
||||
|
||||
async def run_bot_cli(
|
||||
def run_bot_cli(
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs,
|
||||
@@ -68,10 +68,10 @@ async def run_bot_cli(
|
||||
"""
|
||||
from .client import Bot
|
||||
|
||||
await _run_cli(Bot, hooks, argv, **kwargs)
|
||||
_run_cli(Bot, hooks, argv, **kwargs)
|
||||
|
||||
|
||||
async def _run_cli(
|
||||
def _run_cli(
|
||||
client_type: Type["Client"],
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
@@ -93,20 +93,20 @@ async def _run_cli(
|
||||
parser.add_argument("--password", action="store", help="password")
|
||||
args = parser.parse_args(argv[1:])
|
||||
|
||||
async with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
|
||||
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
|
||||
deltachat = DeltaChat(rpc)
|
||||
core_version = (await deltachat.get_system_info()).deltachat_core_version
|
||||
accounts = await deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else await deltachat.add_account()
|
||||
core_version = (deltachat.get_system_info()).deltachat_core_version
|
||||
accounts = deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else deltachat.add_account()
|
||||
|
||||
client = client_type(account, hooks)
|
||||
client.logger.debug("Running deltachat core %s", core_version)
|
||||
if not await client.is_configured():
|
||||
if not 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))
|
||||
await client.run_forever()
|
||||
configure_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password})
|
||||
configure_thread.start()
|
||||
client.run_forever()
|
||||
|
||||
|
||||
def extract_addr(text: str) -> str:
|
||||
|
||||
@@ -24,63 +24,63 @@ class Account:
|
||||
def _rpc(self) -> "Rpc":
|
||||
return self.manager.rpc
|
||||
|
||||
async def wait_for_event(self) -> AttrDict:
|
||||
def wait_for_event(self) -> AttrDict:
|
||||
"""Wait until the next event and return it."""
|
||||
return AttrDict(await self._rpc.wait_for_event(self.id))
|
||||
return AttrDict(self._rpc.wait_for_event(self.id))
|
||||
|
||||
async def remove(self) -> None:
|
||||
def remove(self) -> None:
|
||||
"""Remove the account."""
|
||||
await self._rpc.remove_account(self.id)
|
||||
self._rpc.remove_account(self.id)
|
||||
|
||||
async def start_io(self) -> None:
|
||||
def start_io(self) -> None:
|
||||
"""Start the account I/O."""
|
||||
await self._rpc.start_io(self.id)
|
||||
self._rpc.start_io(self.id)
|
||||
|
||||
async def stop_io(self) -> None:
|
||||
def stop_io(self) -> None:
|
||||
"""Stop the account I/O."""
|
||||
await self._rpc.stop_io(self.id)
|
||||
self._rpc.stop_io(self.id)
|
||||
|
||||
async def get_info(self) -> AttrDict:
|
||||
def get_info(self) -> AttrDict:
|
||||
"""Return dictionary of this account configuration parameters."""
|
||||
return AttrDict(await self._rpc.get_info(self.id))
|
||||
return AttrDict(self._rpc.get_info(self.id))
|
||||
|
||||
async def get_size(self) -> int:
|
||||
def get_size(self) -> int:
|
||||
"""Get the combined filesize of an account in bytes."""
|
||||
return await self._rpc.get_account_file_size(self.id)
|
||||
return self._rpc.get_account_file_size(self.id)
|
||||
|
||||
async def is_configured(self) -> bool:
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True if this account is configured."""
|
||||
return await self._rpc.is_configured(self.id)
|
||||
return self._rpc.is_configured(self.id)
|
||||
|
||||
async def set_config(self, key: str, value: Optional[str] = None) -> None:
|
||||
def set_config(self, key: str, value: Optional[str] = None) -> None:
|
||||
"""Set configuration value."""
|
||||
await self._rpc.set_config(self.id, key, value)
|
||||
self._rpc.set_config(self.id, key, value)
|
||||
|
||||
async def get_config(self, key: str) -> Optional[str]:
|
||||
def get_config(self, key: str) -> Optional[str]:
|
||||
"""Get configuration value."""
|
||||
return await self._rpc.get_config(self.id, key)
|
||||
return self._rpc.get_config(self.id, key)
|
||||
|
||||
async def update_config(self, **kwargs) -> None:
|
||||
def update_config(self, **kwargs) -> None:
|
||||
"""update config values."""
|
||||
for key, value in kwargs.items():
|
||||
await self.set_config(key, value)
|
||||
self.set_config(key, value)
|
||||
|
||||
async def set_avatar(self, img_path: Optional[str] = None) -> None:
|
||||
def set_avatar(self, img_path: Optional[str] = None) -> None:
|
||||
"""Set self avatar.
|
||||
|
||||
Passing None will discard the currently set avatar.
|
||||
"""
|
||||
await self.set_config("selfavatar", img_path)
|
||||
self.set_config("selfavatar", img_path)
|
||||
|
||||
async def get_avatar(self) -> Optional[str]:
|
||||
def get_avatar(self) -> Optional[str]:
|
||||
"""Get self avatar."""
|
||||
return await self.get_config("selfavatar")
|
||||
return self.get_config("selfavatar")
|
||||
|
||||
async def configure(self) -> None:
|
||||
def configure(self) -> None:
|
||||
"""Configure an account."""
|
||||
await self._rpc.configure(self.id)
|
||||
self._rpc.configure(self.id)
|
||||
|
||||
async def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
||||
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
||||
"""Create a new Contact or return an existing one.
|
||||
|
||||
Calling this method will always result in the same
|
||||
@@ -94,24 +94,24 @@ class Account:
|
||||
if isinstance(obj, int):
|
||||
obj = Contact(self, obj)
|
||||
if isinstance(obj, Contact):
|
||||
obj = (await obj.get_snapshot()).address
|
||||
return Contact(self, await self._rpc.create_contact(self.id, obj, name))
|
||||
obj = obj.get_snapshot().address
|
||||
return Contact(self, self._rpc.create_contact(self.id, obj, name))
|
||||
|
||||
def get_contact_by_id(self, contact_id: int) -> Contact:
|
||||
"""Return Contact instance for the given contact ID."""
|
||||
return Contact(self, contact_id)
|
||||
|
||||
async def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
||||
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
||||
"""Check if an e-mail address belongs to a known and unblocked contact."""
|
||||
contact_id = await self._rpc.lookup_contact_id_by_addr(self.id, address)
|
||||
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
||||
return contact_id and Contact(self, contact_id)
|
||||
|
||||
async def get_blocked_contacts(self) -> List[AttrDict]:
|
||||
def get_blocked_contacts(self) -> List[AttrDict]:
|
||||
"""Return a list with snapshots of all blocked contacts."""
|
||||
contacts = await self._rpc.get_blocked_contacts(self.id)
|
||||
contacts = self._rpc.get_blocked_contacts(self.id)
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
|
||||
async def get_contacts(
|
||||
def get_contacts(
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
with_self: bool = False,
|
||||
@@ -133,9 +133,9 @@ class Account:
|
||||
flags |= ContactFlag.ADD_SELF
|
||||
|
||||
if snapshot:
|
||||
contacts = await self._rpc.get_contacts(self.id, flags, query)
|
||||
contacts = self._rpc.get_contacts(self.id, flags, query)
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
contacts = await self._rpc.get_contact_ids(self.id, flags, query)
|
||||
contacts = self._rpc.get_contact_ids(self.id, flags, query)
|
||||
return [Contact(self, contact_id) for contact_id in contacts]
|
||||
|
||||
@property
|
||||
@@ -143,7 +143,7 @@ class Account:
|
||||
"""This account's identity as a Contact."""
|
||||
return Contact(self, SpecialContactId.SELF)
|
||||
|
||||
async def get_chatlist(
|
||||
def get_chatlist(
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
contact: Optional[Contact] = None,
|
||||
@@ -175,29 +175,29 @@ class Account:
|
||||
if alldone_hint:
|
||||
flags |= ChatlistFlag.ADD_ALLDONE_HINT
|
||||
|
||||
entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
|
||||
entries = self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
|
||||
if not snapshot:
|
||||
return [Chat(self, entry) for entry in entries]
|
||||
|
||||
items = await self._rpc.get_chatlist_items_by_entries(self.id, entries)
|
||||
items = self._rpc.get_chatlist_items_by_entries(self.id, entries)
|
||||
chats = []
|
||||
for item in items.values():
|
||||
item["chat"] = Chat(self, item["id"])
|
||||
chats.append(AttrDict(item))
|
||||
return chats
|
||||
|
||||
async def create_group(self, name: str, protect: bool = False) -> Chat:
|
||||
def create_group(self, name: str, protect: bool = False) -> Chat:
|
||||
"""Create a new group chat.
|
||||
|
||||
After creation, the group has only self-contact as member and is in unpromoted state.
|
||||
"""
|
||||
return Chat(self, await self._rpc.create_group_chat(self.id, name, protect))
|
||||
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
|
||||
|
||||
def get_chat_by_id(self, chat_id: int) -> Chat:
|
||||
"""Return the Chat instance with the given ID."""
|
||||
return Chat(self, chat_id)
|
||||
|
||||
async def secure_join(self, qrdata: str) -> Chat:
|
||||
def secure_join(self, qrdata: str) -> Chat:
|
||||
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on
|
||||
another device.
|
||||
|
||||
@@ -208,54 +208,62 @@ class Account:
|
||||
|
||||
:param qrdata: The text of the scanned QR code.
|
||||
"""
|
||||
return Chat(self, await self._rpc.secure_join(self.id, qrdata))
|
||||
return Chat(self, self._rpc.secure_join(self.id, qrdata))
|
||||
|
||||
async def get_qr_code(self) -> Tuple[str, str]:
|
||||
def get_qr_code(self) -> Tuple[str, str]:
|
||||
"""Get Setup-Contact QR Code text and SVG data.
|
||||
|
||||
this data needs to be transferred to another Delta Chat account
|
||||
in a second channel, typically used by mobiles with QRcode-show + scan UX.
|
||||
"""
|
||||
return await self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
|
||||
return self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
|
||||
|
||||
def get_message_by_id(self, msg_id: int) -> Message:
|
||||
"""Return the Message instance with the given ID."""
|
||||
return Message(self, msg_id)
|
||||
|
||||
async def mark_seen_messages(self, messages: List[Message]) -> None:
|
||||
def mark_seen_messages(self, messages: List[Message]) -> None:
|
||||
"""Mark the given set of messages as seen."""
|
||||
await self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
|
||||
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
|
||||
|
||||
async def delete_messages(self, messages: List[Message]) -> None:
|
||||
def delete_messages(self, messages: List[Message]) -> None:
|
||||
"""Delete messages (local and remote)."""
|
||||
await self._rpc.delete_messages(self.id, [msg.id for msg in messages])
|
||||
self._rpc.delete_messages(self.id, [msg.id for msg in messages])
|
||||
|
||||
async def get_fresh_messages(self) -> List[Message]:
|
||||
def get_fresh_messages(self) -> List[Message]:
|
||||
"""Return the list of fresh messages, newest messages first.
|
||||
|
||||
This call is intended for displaying notifications.
|
||||
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
|
||||
to process oldest messages first.
|
||||
"""
|
||||
fresh_msg_ids = await self._rpc.get_fresh_msgs(self.id)
|
||||
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
|
||||
async def get_next_messages(self) -> List[Message]:
|
||||
def get_next_messages(self) -> List[Message]:
|
||||
"""Return a list of next messages."""
|
||||
next_msg_ids = await self._rpc.get_next_msgs(self.id)
|
||||
next_msg_ids = self._rpc.get_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
async def wait_next_messages(self) -> List[Message]:
|
||||
def wait_next_messages(self) -> List[Message]:
|
||||
"""Wait for new messages and return a list of them."""
|
||||
next_msg_ids = await self._rpc.wait_next_msgs(self.id)
|
||||
next_msg_ids = self._rpc.wait_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
async def get_fresh_messages_in_arrival_order(self) -> List[Message]:
|
||||
def get_fresh_messages_in_arrival_order(self) -> List[Message]:
|
||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||
warn(
|
||||
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id))
|
||||
fresh_msg_ids = sorted(self._rpc.get_fresh_msgs(self.id))
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
|
||||
def export_backup(self, path, passphrase: str = "") -> None:
|
||||
"""Export backup."""
|
||||
self._rpc.export_backup(self.id, str(path), passphrase)
|
||||
|
||||
def import_backup(self, path, passphrase: str = "") -> None:
|
||||
"""Import backup."""
|
||||
self._rpc.import_backup(self.id, str(path), passphrase)
|
||||
|
||||
@@ -25,7 +25,7 @@ class Chat:
|
||||
def _rpc(self) -> "Rpc":
|
||||
return self.account._rpc
|
||||
|
||||
async def delete(self) -> None:
|
||||
def delete(self) -> None:
|
||||
"""Delete this chat and all its messages.
|
||||
|
||||
Note:
|
||||
@@ -33,21 +33,21 @@ class Chat:
|
||||
- does not delete messages on server
|
||||
- the chat or contact is not blocked, new message will arrive
|
||||
"""
|
||||
await self._rpc.delete_chat(self.account.id, self.id)
|
||||
self._rpc.delete_chat(self.account.id, self.id)
|
||||
|
||||
async def block(self) -> None:
|
||||
def block(self) -> None:
|
||||
"""Block this chat."""
|
||||
await self._rpc.block_chat(self.account.id, self.id)
|
||||
self._rpc.block_chat(self.account.id, self.id)
|
||||
|
||||
async def accept(self) -> None:
|
||||
def accept(self) -> None:
|
||||
"""Accept this contact request chat."""
|
||||
await self._rpc.accept_chat(self.account.id, self.id)
|
||||
self._rpc.accept_chat(self.account.id, self.id)
|
||||
|
||||
async def leave(self) -> None:
|
||||
def leave(self) -> None:
|
||||
"""Leave this chat."""
|
||||
await self._rpc.leave_group(self.account.id, self.id)
|
||||
self._rpc.leave_group(self.account.id, self.id)
|
||||
|
||||
async def mute(self, duration: Optional[int] = None) -> None:
|
||||
def mute(self, duration: Optional[int] = None) -> None:
|
||||
"""Mute this chat, if a duration is not provided the chat is muted forever.
|
||||
|
||||
:param duration: mute duration from now in seconds. Must be greater than zero.
|
||||
@@ -57,59 +57,59 @@ class Chat:
|
||||
dur: Union[str, dict] = {"Until": duration}
|
||||
else:
|
||||
dur = "Forever"
|
||||
await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
|
||||
self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
|
||||
|
||||
async def unmute(self) -> None:
|
||||
def unmute(self) -> None:
|
||||
"""Unmute this chat."""
|
||||
await self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
|
||||
self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
|
||||
|
||||
async def pin(self) -> None:
|
||||
def pin(self) -> None:
|
||||
"""Pin this chat."""
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
|
||||
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
|
||||
|
||||
async def unpin(self) -> None:
|
||||
def unpin(self) -> None:
|
||||
"""Unpin this chat."""
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||
|
||||
async def archive(self) -> None:
|
||||
def archive(self) -> None:
|
||||
"""Archive this chat."""
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
|
||||
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
|
||||
|
||||
async def unarchive(self) -> None:
|
||||
def unarchive(self) -> None:
|
||||
"""Unarchive this chat."""
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||
|
||||
async def set_name(self, name: str) -> None:
|
||||
def set_name(self, name: str) -> None:
|
||||
"""Set name of this chat."""
|
||||
await self._rpc.set_chat_name(self.account.id, self.id, name)
|
||||
self._rpc.set_chat_name(self.account.id, self.id, name)
|
||||
|
||||
async def set_ephemeral_timer(self, timer: int) -> None:
|
||||
def set_ephemeral_timer(self, timer: int) -> None:
|
||||
"""Set ephemeral timer of this chat."""
|
||||
await self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
||||
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
||||
|
||||
async def get_encryption_info(self) -> str:
|
||||
def get_encryption_info(self) -> str:
|
||||
"""Return encryption info for this chat."""
|
||||
return await self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
||||
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
||||
|
||||
async def get_qr_code(self) -> Tuple[str, str]:
|
||||
def get_qr_code(self) -> Tuple[str, str]:
|
||||
"""Get Join-Group QR code text and SVG data."""
|
||||
return await self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
|
||||
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
|
||||
|
||||
async def get_basic_snapshot(self) -> AttrDict:
|
||||
def get_basic_snapshot(self) -> AttrDict:
|
||||
"""Get a chat snapshot with basic info about this chat."""
|
||||
info = await self._rpc.get_basic_chat_info(self.account.id, self.id)
|
||||
info = self._rpc.get_basic_chat_info(self.account.id, self.id)
|
||||
return AttrDict(chat=self, **info)
|
||||
|
||||
async def get_full_snapshot(self) -> AttrDict:
|
||||
def get_full_snapshot(self) -> AttrDict:
|
||||
"""Get a full snapshot of this chat."""
|
||||
info = await self._rpc.get_full_chat_by_id(self.account.id, self.id)
|
||||
info = self._rpc.get_full_chat_by_id(self.account.id, self.id)
|
||||
return AttrDict(chat=self, **info)
|
||||
|
||||
async def can_send(self) -> bool:
|
||||
def can_send(self) -> bool:
|
||||
"""Return true if messages can be sent to the chat."""
|
||||
return await self._rpc.can_send(self.account.id, self.id)
|
||||
return self._rpc.can_send(self.account.id, self.id)
|
||||
|
||||
async def send_message(
|
||||
def send_message(
|
||||
self,
|
||||
text: Optional[str] = None,
|
||||
html: Optional[str] = None,
|
||||
@@ -132,30 +132,30 @@ class Chat:
|
||||
"overrideSenderName": override_sender_name,
|
||||
"quotedMessageId": quoted_msg,
|
||||
}
|
||||
msg_id = await self._rpc.send_msg(self.account.id, self.id, draft)
|
||||
msg_id = self._rpc.send_msg(self.account.id, self.id, draft)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
async def send_text(self, text: str) -> Message:
|
||||
def send_text(self, text: str) -> Message:
|
||||
"""Send a text message and return the resulting Message instance."""
|
||||
msg_id = await self._rpc.misc_send_text_message(self.account.id, self.id, text)
|
||||
msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
async def send_videochat_invitation(self) -> Message:
|
||||
def send_videochat_invitation(self) -> Message:
|
||||
"""Send a videochat invitation and return the resulting Message instance."""
|
||||
msg_id = await self._rpc.send_videochat_invitation(self.account.id, self.id)
|
||||
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
async def send_sticker(self, path: str) -> Message:
|
||||
def send_sticker(self, path: str) -> Message:
|
||||
"""Send an sticker and return the resulting Message instance."""
|
||||
msg_id = await self._rpc.send_sticker(self.account.id, self.id, path)
|
||||
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
async def forward_messages(self, messages: List[Message]) -> None:
|
||||
def forward_messages(self, messages: List[Message]) -> None:
|
||||
"""Forward a list of messages to this chat."""
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
await self._rpc.forward_messages(self.account.id, msg_ids, self.id)
|
||||
self._rpc.forward_messages(self.account.id, msg_ids, self.id)
|
||||
|
||||
async def set_draft(
|
||||
def set_draft(
|
||||
self,
|
||||
text: Optional[str] = None,
|
||||
file: Optional[str] = None,
|
||||
@@ -164,15 +164,15 @@ class Chat:
|
||||
"""Set draft message."""
|
||||
if isinstance(quoted_msg, Message):
|
||||
quoted_msg = quoted_msg.id
|
||||
await self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
|
||||
self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
|
||||
|
||||
async def remove_draft(self) -> None:
|
||||
def remove_draft(self) -> None:
|
||||
"""Remove draft message."""
|
||||
await self._rpc.remove_draft(self.account.id, self.id)
|
||||
self._rpc.remove_draft(self.account.id, self.id)
|
||||
|
||||
async def get_draft(self) -> Optional[AttrDict]:
|
||||
def get_draft(self) -> Optional[AttrDict]:
|
||||
"""Get draft message."""
|
||||
snapshot = await self._rpc.get_draft(self.account.id, self.id)
|
||||
snapshot = self._rpc.get_draft(self.account.id, self.id)
|
||||
if not snapshot:
|
||||
return None
|
||||
snapshot = AttrDict(snapshot)
|
||||
@@ -181,61 +181,61 @@ class Chat:
|
||||
snapshot["message"] = Message(self.account, snapshot.id)
|
||||
return snapshot
|
||||
|
||||
async def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
|
||||
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> 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 = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
||||
return [Message(self.account, msg_id) for msg_id in msgs]
|
||||
|
||||
async def get_fresh_message_count(self) -> int:
|
||||
def get_fresh_message_count(self) -> int:
|
||||
"""Get number of fresh messages in this chat"""
|
||||
return await self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
||||
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
||||
|
||||
async def mark_noticed(self) -> None:
|
||||
def mark_noticed(self) -> None:
|
||||
"""Mark all messages in this chat as noticed."""
|
||||
await self._rpc.marknoticed_chat(self.account.id, self.id)
|
||||
self._rpc.marknoticed_chat(self.account.id, self.id)
|
||||
|
||||
async def add_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||
def add_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||
"""Add contacts to this group."""
|
||||
for cnt in contact:
|
||||
if isinstance(cnt, str):
|
||||
contact_id = (await self.account.create_contact(cnt)).id
|
||||
contact_id = 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)
|
||||
self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
|
||||
|
||||
async def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||
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
|
||||
contact_id = 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)
|
||||
self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
|
||||
|
||||
async def get_contacts(self) -> List[Contact]:
|
||||
def get_contacts(self) -> List[Contact]:
|
||||
"""Get the contacts belonging to this chat.
|
||||
|
||||
For single/direct chats self-address is not included.
|
||||
"""
|
||||
contacts = await self._rpc.get_chat_contacts(self.account.id, self.id)
|
||||
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
|
||||
return [Contact(self.account, contact_id) for contact_id in contacts]
|
||||
|
||||
async def set_image(self, path: str) -> None:
|
||||
def set_image(self, path: str) -> None:
|
||||
"""Set profile image of this chat.
|
||||
|
||||
:param path: Full path of the image to use as the group image.
|
||||
"""
|
||||
await self._rpc.set_chat_profile_image(self.account.id, self.id, path)
|
||||
self._rpc.set_chat_profile_image(self.account.id, self.id, path)
|
||||
|
||||
async def remove_image(self) -> None:
|
||||
def remove_image(self) -> None:
|
||||
"""Remove profile image of this chat."""
|
||||
await self._rpc.set_chat_profile_image(self.account.id, self.id, None)
|
||||
self._rpc.set_chat_profile_image(self.account.id, self.id, None)
|
||||
|
||||
async def get_locations(
|
||||
def get_locations(
|
||||
self,
|
||||
contact: Optional[Contact] = None,
|
||||
timestamp_from: Optional["datetime"] = None,
|
||||
@@ -246,7 +246,7 @@ class Chat:
|
||||
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
|
||||
contact_id = contact.id if contact else 0
|
||||
|
||||
result = await self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
|
||||
result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
|
||||
locations = []
|
||||
contacts: Dict[int, Contact] = {}
|
||||
for loc in result:
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
"""Event loop implementations offering high level event handling/hooking."""
|
||||
import inspect
|
||||
import logging
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
Callable,
|
||||
Coroutine,
|
||||
Dict,
|
||||
Iterable,
|
||||
Optional,
|
||||
@@ -78,22 +76,22 @@ class Client:
|
||||
)
|
||||
self._hooks.get(type(event), set()).remove((hook, event))
|
||||
|
||||
async def is_configured(self) -> bool:
|
||||
return await self.account.is_configured()
|
||||
def is_configured(self) -> bool:
|
||||
return self.account.is_configured()
|
||||
|
||||
async def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
await self.account.set_config("addr", email)
|
||||
await self.account.set_config("mail_pw", password)
|
||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
self.account.set_config("addr", email)
|
||||
self.account.set_config("mail_pw", password)
|
||||
for key, value in kwargs.items():
|
||||
await self.account.set_config(key, value)
|
||||
await self.account.configure()
|
||||
self.account.set_config(key, value)
|
||||
self.account.configure()
|
||||
self.logger.debug("Account configured")
|
||||
|
||||
async def run_forever(self) -> None:
|
||||
def run_forever(self) -> None:
|
||||
"""Process events forever."""
|
||||
await self.run_until(lambda _: False)
|
||||
self.run_until(lambda _: False)
|
||||
|
||||
async def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
|
||||
def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
|
||||
"""Process events until the given callable evaluates to True.
|
||||
|
||||
The callable should accept an AttrDict object representing the
|
||||
@@ -101,39 +99,37 @@ class Client:
|
||||
evaluates to True.
|
||||
"""
|
||||
self.logger.debug("Listening to incoming events...")
|
||||
if await self.is_configured():
|
||||
await self.account.start_io()
|
||||
await self._process_messages() # Process old messages.
|
||||
if self.is_configured():
|
||||
self.account.start_io()
|
||||
self._process_messages() # Process old messages.
|
||||
while True:
|
||||
event = await self.account.wait_for_event()
|
||||
event = self.account.wait_for_event()
|
||||
event["type"] = EventType(event.type)
|
||||
event["account"] = self.account
|
||||
await self._on_event(event)
|
||||
self._on_event(event)
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
await self._process_messages()
|
||||
self._process_messages()
|
||||
|
||||
stop = func(event)
|
||||
if inspect.isawaitable(stop):
|
||||
stop = await stop
|
||||
if stop:
|
||||
return event
|
||||
|
||||
async def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
|
||||
def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
|
||||
for hook, evfilter in self._hooks.get(filter_type, []):
|
||||
if await evfilter.filter(event):
|
||||
if evfilter.filter(event):
|
||||
try:
|
||||
await hook(event)
|
||||
hook(event)
|
||||
except Exception as ex:
|
||||
self.logger.exception(ex)
|
||||
|
||||
async def _parse_command(self, event: AttrDict) -> None:
|
||||
def _parse_command(self, event: AttrDict) -> None:
|
||||
cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
|
||||
parts = event.message_snapshot.text.split(maxsplit=1)
|
||||
payload = parts[1] if len(parts) > 1 else ""
|
||||
cmd = parts.pop(0)
|
||||
|
||||
if "@" in cmd:
|
||||
suffix = "@" + (await self.account.self_contact.get_snapshot()).address
|
||||
suffix = "@" + self.account.self_contact.get_snapshot().address
|
||||
if cmd.endswith(suffix):
|
||||
cmd = cmd[: -len(suffix)]
|
||||
else:
|
||||
@@ -153,32 +149,32 @@ class Client:
|
||||
|
||||
event["command"], event["payload"] = cmd, payload
|
||||
|
||||
async def _on_new_msg(self, snapshot: AttrDict) -> None:
|
||||
def _on_new_msg(self, snapshot: AttrDict) -> None:
|
||||
event = AttrDict(command="", payload="", message_snapshot=snapshot)
|
||||
if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX):
|
||||
await self._parse_command(event)
|
||||
await self._on_event(event, NewMessage)
|
||||
self._parse_command(event)
|
||||
self._on_event(event, NewMessage)
|
||||
|
||||
async def _handle_info_msg(self, snapshot: AttrDict) -> None:
|
||||
def _handle_info_msg(self, snapshot: AttrDict) -> None:
|
||||
event = AttrDict(message_snapshot=snapshot)
|
||||
|
||||
img_changed = parse_system_image_changed(snapshot.text)
|
||||
if img_changed:
|
||||
_, event["image_deleted"] = img_changed
|
||||
await self._on_event(event, GroupImageChanged)
|
||||
self._on_event(event, GroupImageChanged)
|
||||
return
|
||||
|
||||
title_changed = parse_system_title_changed(snapshot.text)
|
||||
if title_changed:
|
||||
_, event["old_name"] = title_changed
|
||||
await self._on_event(event, GroupNameChanged)
|
||||
self._on_event(event, GroupNameChanged)
|
||||
return
|
||||
|
||||
members_changed = parse_system_add_remove(snapshot.text)
|
||||
if members_changed:
|
||||
action, event["member"], _ = members_changed
|
||||
event["member_added"] = action == "added"
|
||||
await self._on_event(event, MemberListChanged)
|
||||
self._on_event(event, MemberListChanged)
|
||||
return
|
||||
|
||||
self.logger.warning(
|
||||
@@ -187,20 +183,20 @@ class Client:
|
||||
snapshot.text,
|
||||
)
|
||||
|
||||
async def _process_messages(self) -> None:
|
||||
def _process_messages(self) -> None:
|
||||
if self._should_process_messages:
|
||||
for message in await self.account.get_next_messages():
|
||||
snapshot = await message.get_snapshot()
|
||||
for message in self.account.get_next_messages():
|
||||
snapshot = message.get_snapshot()
|
||||
if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]:
|
||||
await self._on_new_msg(snapshot)
|
||||
self._on_new_msg(snapshot)
|
||||
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
|
||||
await self._handle_info_msg(snapshot)
|
||||
await snapshot.message.mark_seen()
|
||||
self._handle_info_msg(snapshot)
|
||||
snapshot.message.mark_seen()
|
||||
|
||||
|
||||
class Bot(Client):
|
||||
"""Simple bot implementation that listent to events of a single account."""
|
||||
|
||||
async def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
kwargs.setdefault("bot", "1")
|
||||
await super().configure(email, password, **kwargs)
|
||||
super().configure(email, password, **kwargs)
|
||||
|
||||
@@ -45,6 +45,7 @@ class EventType(str, Enum):
|
||||
MSG_DELIVERED = "MsgDelivered"
|
||||
MSG_FAILED = "MsgFailed"
|
||||
MSG_READ = "MsgRead"
|
||||
MSG_DELETED = "MsgDeleted"
|
||||
CHAT_MODIFIED = "ChatModified"
|
||||
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
|
||||
CONTACTS_CHANGED = "ContactsChanged"
|
||||
|
||||
@@ -24,39 +24,39 @@ class Contact:
|
||||
def _rpc(self) -> "Rpc":
|
||||
return self.account._rpc
|
||||
|
||||
async def block(self) -> None:
|
||||
def block(self) -> None:
|
||||
"""Block contact."""
|
||||
await self._rpc.block_contact(self.account.id, self.id)
|
||||
self._rpc.block_contact(self.account.id, self.id)
|
||||
|
||||
async def unblock(self) -> None:
|
||||
def unblock(self) -> None:
|
||||
"""Unblock contact."""
|
||||
await self._rpc.unblock_contact(self.account.id, self.id)
|
||||
self._rpc.unblock_contact(self.account.id, self.id)
|
||||
|
||||
async def delete(self) -> None:
|
||||
def delete(self) -> None:
|
||||
"""Delete contact."""
|
||||
await self._rpc.delete_contact(self.account.id, self.id)
|
||||
self._rpc.delete_contact(self.account.id, self.id)
|
||||
|
||||
async def set_name(self, name: str) -> None:
|
||||
def set_name(self, name: str) -> None:
|
||||
"""Change the name of this contact."""
|
||||
await self._rpc.change_contact_name(self.account.id, self.id, name)
|
||||
self._rpc.change_contact_name(self.account.id, self.id, name)
|
||||
|
||||
async def get_encryption_info(self) -> str:
|
||||
def get_encryption_info(self) -> str:
|
||||
"""Get a multi-line encryption info, containing your fingerprint and
|
||||
the fingerprint of the contact.
|
||||
"""
|
||||
return await self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
||||
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
||||
|
||||
async def get_snapshot(self) -> AttrDict:
|
||||
def get_snapshot(self) -> AttrDict:
|
||||
"""Return a dictionary with a snapshot of all contact properties."""
|
||||
snapshot = AttrDict(await self._rpc.get_contact(self.account.id, self.id))
|
||||
snapshot = AttrDict(self._rpc.get_contact(self.account.id, self.id))
|
||||
snapshot["contact"] = self
|
||||
return snapshot
|
||||
|
||||
async def create_chat(self) -> "Chat":
|
||||
def create_chat(self) -> "Chat":
|
||||
"""Create or get an existing 1:1 chat for this contact."""
|
||||
from .chat import Chat
|
||||
|
||||
return Chat(
|
||||
self.account,
|
||||
await self._rpc.create_chat_by_contact_id(self.account.id, self.id),
|
||||
self._rpc.create_chat_by_contact_id(self.account.id, self.id),
|
||||
)
|
||||
|
||||
@@ -16,34 +16,34 @@ class DeltaChat:
|
||||
def __init__(self, rpc: "Rpc") -> None:
|
||||
self.rpc = rpc
|
||||
|
||||
async def add_account(self) -> Account:
|
||||
def add_account(self) -> Account:
|
||||
"""Create a new account database."""
|
||||
account_id = await self.rpc.add_account()
|
||||
account_id = self.rpc.add_account()
|
||||
return Account(self, account_id)
|
||||
|
||||
async def get_all_accounts(self) -> List[Account]:
|
||||
def get_all_accounts(self) -> List[Account]:
|
||||
"""Return a list of all available accounts."""
|
||||
account_ids = await self.rpc.get_all_account_ids()
|
||||
account_ids = self.rpc.get_all_account_ids()
|
||||
return [Account(self, account_id) for account_id in account_ids]
|
||||
|
||||
async def start_io(self) -> None:
|
||||
def start_io(self) -> None:
|
||||
"""Start the I/O of all accounts."""
|
||||
await self.rpc.start_io_for_all_accounts()
|
||||
self.rpc.start_io_for_all_accounts()
|
||||
|
||||
async def stop_io(self) -> None:
|
||||
def stop_io(self) -> None:
|
||||
"""Stop the I/O of all accounts."""
|
||||
await self.rpc.stop_io_for_all_accounts()
|
||||
self.rpc.stop_io_for_all_accounts()
|
||||
|
||||
async def maybe_network(self) -> None:
|
||||
def maybe_network(self) -> None:
|
||||
"""Indicate that the network likely has come back or just that the network
|
||||
conditions might have changed.
|
||||
"""
|
||||
await self.rpc.maybe_network()
|
||||
self.rpc.maybe_network()
|
||||
|
||||
async def get_system_info(self) -> AttrDict:
|
||||
def get_system_info(self) -> AttrDict:
|
||||
"""Get information about the Delta Chat core in this system."""
|
||||
return AttrDict(await self.rpc.get_system_info())
|
||||
return AttrDict(self.rpc.get_system_info())
|
||||
|
||||
async def set_translations(self, translations: Dict[str, str]) -> None:
|
||||
def set_translations(self, translations: Dict[str, str]) -> None:
|
||||
"""Set stock translation strings."""
|
||||
await self.rpc.set_stock_strings(translations)
|
||||
self.rpc.set_stock_strings(translations)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""High-level classes for event processing and filtering."""
|
||||
import inspect
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
|
||||
@@ -24,7 +23,7 @@ def _tuple_of(obj, type_: type) -> tuple:
|
||||
class EventFilter(ABC):
|
||||
"""The base event filter.
|
||||
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
:param func: A Callable function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -43,16 +42,13 @@ class EventFilter(ABC):
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
async def _call_func(self, event) -> bool:
|
||||
def _call_func(self, event) -> bool:
|
||||
if not self.func:
|
||||
return True
|
||||
res = self.func(event)
|
||||
if inspect.isawaitable(res):
|
||||
return await res
|
||||
return res
|
||||
return self.func(event)
|
||||
|
||||
@abstractmethod
|
||||
async def filter(self, event):
|
||||
def filter(self, event):
|
||||
"""Return True-like value if the event passed the filter and should be
|
||||
used, or False-like value otherwise.
|
||||
"""
|
||||
@@ -62,7 +58,7 @@ class RawEvent(EventFilter):
|
||||
"""Matches raw core events.
|
||||
|
||||
:param types: The types of event to match.
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
:param func: A Callable function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -82,10 +78,10 @@ class RawEvent(EventFilter):
|
||||
return (self.types, self.func) == (other.types, other.func)
|
||||
return False
|
||||
|
||||
async def filter(self, event: "AttrDict") -> bool:
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
if self.types and event.type not in self.types:
|
||||
return False
|
||||
return await self._call_func(event)
|
||||
return self._call_func(event)
|
||||
|
||||
|
||||
class NewMessage(EventFilter):
|
||||
@@ -104,7 +100,7 @@ class NewMessage(EventFilter):
|
||||
:param is_info: If set to True only match info/system messages, if set to False
|
||||
only match messages that are not info/system messages. If omitted
|
||||
info/system messages as well as normal messages will be matched.
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
:param func: A Callable function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -159,7 +155,7 @@ class NewMessage(EventFilter):
|
||||
)
|
||||
return False
|
||||
|
||||
async def filter(self, event: "AttrDict") -> bool:
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
|
||||
return False
|
||||
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
|
||||
@@ -168,11 +164,9 @@ class NewMessage(EventFilter):
|
||||
return False
|
||||
if self.pattern:
|
||||
match = self.pattern(event.message_snapshot.text)
|
||||
if inspect.isawaitable(match):
|
||||
match = await match
|
||||
if not match:
|
||||
return False
|
||||
return await super()._call_func(event)
|
||||
return super()._call_func(event)
|
||||
|
||||
|
||||
class MemberListChanged(EventFilter):
|
||||
@@ -184,7 +178,7 @@ class MemberListChanged(EventFilter):
|
||||
:param added: If set to True only match if a member was added, if set to False
|
||||
only match if a member was removed. If omitted both, member additions
|
||||
and removals, will be matched.
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
:param func: A Callable function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -201,10 +195,10 @@ class MemberListChanged(EventFilter):
|
||||
return (self.added, self.func) == (other.added, other.func)
|
||||
return False
|
||||
|
||||
async def filter(self, event: "AttrDict") -> bool:
|
||||
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)
|
||||
return self._call_func(event)
|
||||
|
||||
|
||||
class GroupImageChanged(EventFilter):
|
||||
@@ -216,7 +210,7 @@ class GroupImageChanged(EventFilter):
|
||||
:param deleted: If set to True only match if the image was deleted, if set to False
|
||||
only match if a new image was set. If omitted both, image changes and
|
||||
removals, will be matched.
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
:param func: A Callable function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -233,10 +227,10 @@ class GroupImageChanged(EventFilter):
|
||||
return (self.deleted, self.func) == (other.deleted, other.func)
|
||||
return False
|
||||
|
||||
async def filter(self, event: "AttrDict") -> bool:
|
||||
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)
|
||||
return self._call_func(event)
|
||||
|
||||
|
||||
class GroupNameChanged(EventFilter):
|
||||
@@ -245,7 +239,7 @@ class GroupNameChanged(EventFilter):
|
||||
Warning: registering a handler for this event will cause the messages
|
||||
to be marked as read. Its usage is mainly intended for bots.
|
||||
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
:param func: A Callable function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -258,8 +252,8 @@ class GroupNameChanged(EventFilter):
|
||||
return self.func == other.func
|
||||
return False
|
||||
|
||||
async def filter(self, event: "AttrDict") -> bool:
|
||||
return await self._call_func(event)
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
return self._call_func(event)
|
||||
|
||||
|
||||
class HookCollection:
|
||||
|
||||
@@ -21,39 +21,39 @@ class Message:
|
||||
def _rpc(self) -> "Rpc":
|
||||
return self.account._rpc
|
||||
|
||||
async def send_reaction(self, *reaction: str):
|
||||
def send_reaction(self, *reaction: str):
|
||||
"""Send a reaction to this message."""
|
||||
await self._rpc.send_reaction(self.account.id, self.id, reaction)
|
||||
self._rpc.send_reaction(self.account.id, self.id, reaction)
|
||||
|
||||
async def get_snapshot(self) -> AttrDict:
|
||||
def get_snapshot(self) -> AttrDict:
|
||||
"""Get a snapshot with the properties of this message."""
|
||||
from .chat import Chat
|
||||
|
||||
snapshot = AttrDict(await self._rpc.get_message(self.account.id, self.id))
|
||||
snapshot = AttrDict(self._rpc.get_message(self.account.id, self.id))
|
||||
snapshot["chat"] = Chat(self.account, snapshot.chat_id)
|
||||
snapshot["sender"] = Contact(self.account, snapshot.from_id)
|
||||
snapshot["message"] = self
|
||||
return snapshot
|
||||
|
||||
async def get_reactions(self) -> Optional[AttrDict]:
|
||||
def get_reactions(self) -> Optional[AttrDict]:
|
||||
"""Get message reactions."""
|
||||
reactions = await self._rpc.get_message_reactions(self.account.id, self.id)
|
||||
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
|
||||
if reactions:
|
||||
return AttrDict(reactions)
|
||||
return None
|
||||
|
||||
async def mark_seen(self) -> None:
|
||||
def mark_seen(self) -> None:
|
||||
"""Mark the message as seen."""
|
||||
await self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||
self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||
|
||||
async def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
||||
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
||||
"""Send a webxdc status update. This message must be a webxdc."""
|
||||
if not isinstance(update, str):
|
||||
update = json.dumps(update)
|
||||
await self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
|
||||
self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
|
||||
|
||||
async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
|
||||
return json.loads(await self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
|
||||
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
|
||||
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
|
||||
|
||||
async def get_webxdc_info(self) -> dict:
|
||||
return await self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||
def get_webxdc_info(self) -> dict:
|
||||
return self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||
|
||||
@@ -1,71 +1,67 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
from typing import AsyncGenerator, List, Optional
|
||||
|
||||
import aiohttp
|
||||
import pytest_asyncio
|
||||
import pytest
|
||||
|
||||
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
|
||||
from .rpc import Rpc
|
||||
|
||||
|
||||
async def get_temp_credentials() -> dict:
|
||||
def get_temp_credentials() -> dict:
|
||||
url = os.getenv("DCC_NEW_TMP_EMAIL")
|
||||
assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set"
|
||||
|
||||
# Replace default 5 minute timeout with a 1 minute timeout.
|
||||
timeout = aiohttp.ClientTimeout(total=60)
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.post(url, timeout=timeout) as response:
|
||||
return json.loads(await response.text())
|
||||
request = urllib.request.Request(url, method="POST")
|
||||
with urllib.request.urlopen(request, timeout=60) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
class ACFactory:
|
||||
def __init__(self, deltachat: DeltaChat) -> None:
|
||||
self.deltachat = deltachat
|
||||
|
||||
async def get_unconfigured_account(self) -> Account:
|
||||
return await self.deltachat.add_account()
|
||||
def get_unconfigured_account(self) -> Account:
|
||||
return self.deltachat.add_account()
|
||||
|
||||
async def get_unconfigured_bot(self) -> Bot:
|
||||
return Bot(await self.get_unconfigured_account())
|
||||
def get_unconfigured_bot(self) -> Bot:
|
||||
return Bot(self.get_unconfigured_account())
|
||||
|
||||
async def new_preconfigured_account(self) -> Account:
|
||||
def new_preconfigured_account(self) -> Account:
|
||||
"""Make a new account with configuration options set, but configuration not started."""
|
||||
credentials = await get_temp_credentials()
|
||||
account = await self.get_unconfigured_account()
|
||||
await account.set_config("addr", credentials["email"])
|
||||
await account.set_config("mail_pw", credentials["password"])
|
||||
assert not await account.is_configured()
|
||||
credentials = get_temp_credentials()
|
||||
account = self.get_unconfigured_account()
|
||||
account.set_config("addr", credentials["email"])
|
||||
account.set_config("mail_pw", credentials["password"])
|
||||
assert not account.is_configured()
|
||||
return account
|
||||
|
||||
async def new_configured_account(self) -> Account:
|
||||
account = await self.new_preconfigured_account()
|
||||
await account.configure()
|
||||
assert await account.is_configured()
|
||||
def new_configured_account(self) -> Account:
|
||||
account = self.new_preconfigured_account()
|
||||
account.configure()
|
||||
assert account.is_configured()
|
||||
return account
|
||||
|
||||
async def new_configured_bot(self) -> Bot:
|
||||
credentials = await get_temp_credentials()
|
||||
bot = await self.get_unconfigured_bot()
|
||||
await bot.configure(credentials["email"], credentials["password"])
|
||||
def new_configured_bot(self) -> Bot:
|
||||
credentials = get_temp_credentials()
|
||||
bot = self.get_unconfigured_bot()
|
||||
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()
|
||||
def get_online_account(self) -> Account:
|
||||
account = self.new_configured_account()
|
||||
account.start_io()
|
||||
while True:
|
||||
event = await account.wait_for_event()
|
||||
print(event)
|
||||
event = account.wait_for_event()
|
||||
if event.type == EventType.IMAP_INBOX_IDLE:
|
||||
break
|
||||
return account
|
||||
|
||||
async def get_online_accounts(self, num: int) -> List[Account]:
|
||||
return await asyncio.gather(*[self.get_online_account() for _ in range(num)])
|
||||
def get_online_accounts(self, num: int) -> List[Account]:
|
||||
return [self.get_online_account() for _ in range(num)]
|
||||
|
||||
async def send_message(
|
||||
def send_message(
|
||||
self,
|
||||
to_account: Account,
|
||||
from_account: Optional[Account] = None,
|
||||
@@ -74,16 +70,16 @@ class ACFactory:
|
||||
group: Optional[str] = None,
|
||||
) -> Message:
|
||||
if not from_account:
|
||||
from_account = (await self.get_online_accounts(1))[0]
|
||||
to_contact = await from_account.create_contact(await to_account.get_config("addr"))
|
||||
from_account = (self.get_online_accounts(1))[0]
|
||||
to_contact = from_account.create_contact(to_account.get_config("addr"))
|
||||
if group:
|
||||
to_chat = await from_account.create_group(group)
|
||||
await to_chat.add_contact(to_contact)
|
||||
to_chat = from_account.create_group(group)
|
||||
to_chat.add_contact(to_contact)
|
||||
else:
|
||||
to_chat = await to_contact.create_chat()
|
||||
return await to_chat.send_message(text=text, file=file)
|
||||
to_chat = to_contact.create_chat()
|
||||
return to_chat.send_message(text=text, file=file)
|
||||
|
||||
async def process_message(
|
||||
def process_message(
|
||||
self,
|
||||
to_client: Client,
|
||||
from_account: Optional[Account] = None,
|
||||
@@ -91,7 +87,7 @@ class ACFactory:
|
||||
file: Optional[str] = None,
|
||||
group: Optional[str] = None,
|
||||
) -> AttrDict:
|
||||
await self.send_message(
|
||||
self.send_message(
|
||||
to_account=to_client.account,
|
||||
from_account=from_account,
|
||||
text=text,
|
||||
@@ -99,16 +95,16 @@ class ACFactory:
|
||||
group=group,
|
||||
)
|
||||
|
||||
return await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
|
||||
return to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def rpc(tmp_path) -> AsyncGenerator:
|
||||
@pytest.fixture()
|
||||
def rpc(tmp_path) -> AsyncGenerator:
|
||||
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
|
||||
async with rpc_server:
|
||||
with rpc_server:
|
||||
yield rpc_server
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def acfactory(rpc) -> AsyncGenerator:
|
||||
yield ACFactory(DeltaChat(rpc))
|
||||
@pytest.fixture()
|
||||
def acfactory(rpc) -> AsyncGenerator:
|
||||
return ACFactory(DeltaChat(rpc))
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from queue import Queue
|
||||
from threading import Event, Thread
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
@@ -10,7 +14,7 @@ class JsonRpcError(Exception):
|
||||
|
||||
class Rpc:
|
||||
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
||||
"""The given arguments will be passed to asyncio.create_subprocess_exec()"""
|
||||
"""The given arguments will be passed to subprocess.Popen()"""
|
||||
if accounts_dir:
|
||||
kwargs["env"] = {
|
||||
**kwargs.get("env", os.environ),
|
||||
@@ -18,95 +22,142 @@ class Rpc:
|
||||
}
|
||||
|
||||
self._kwargs = kwargs
|
||||
self.process: asyncio.subprocess.Process
|
||||
self.process: subprocess.Popen
|
||||
self.id: int
|
||||
self.event_queues: Dict[int, asyncio.Queue]
|
||||
# Map from request ID to `asyncio.Future` returning the response.
|
||||
self.request_events: Dict[int, asyncio.Future]
|
||||
self.event_queues: Dict[int, Queue]
|
||||
# Map from request ID to `threading.Event`.
|
||||
self.request_events: Dict[int, Event]
|
||||
# Map from request ID to the result.
|
||||
self.request_results: Dict[int, Any]
|
||||
self.request_queue: Queue[Any]
|
||||
self.closing: bool
|
||||
self.reader_task: asyncio.Task
|
||||
self.events_task: asyncio.Task
|
||||
self.reader_thread: Thread
|
||||
self.writer_thread: Thread
|
||||
self.events_thread: Thread
|
||||
|
||||
async def start(self) -> None:
|
||||
self.process = await asyncio.create_subprocess_exec(
|
||||
"deltachat-rpc-server",
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
**self._kwargs,
|
||||
)
|
||||
def start(self) -> None:
|
||||
if sys.version_info >= (3, 11):
|
||||
self.process = subprocess.Popen(
|
||||
"deltachat-rpc-server",
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
# Prevent subprocess from capturing SIGINT.
|
||||
process_group=0,
|
||||
**self._kwargs,
|
||||
)
|
||||
else:
|
||||
self.process = subprocess.Popen(
|
||||
"deltachat-rpc-server",
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
# `process_group` is not supported before Python 3.11.
|
||||
preexec_fn=os.setpgrp, # noqa: PLW1509
|
||||
**self._kwargs,
|
||||
)
|
||||
self.id = 0
|
||||
self.event_queues = {}
|
||||
self.request_events = {}
|
||||
self.request_results = {}
|
||||
self.request_queue = Queue()
|
||||
self.closing = False
|
||||
self.reader_task = asyncio.create_task(self.reader_loop())
|
||||
self.events_task = asyncio.create_task(self.events_loop())
|
||||
self.reader_thread = Thread(target=self.reader_loop)
|
||||
self.reader_thread.start()
|
||||
self.writer_thread = Thread(target=self.writer_loop)
|
||||
self.writer_thread.start()
|
||||
self.events_thread = Thread(target=self.events_loop)
|
||||
self.events_thread.start()
|
||||
|
||||
async def close(self) -> None:
|
||||
def close(self) -> None:
|
||||
"""Terminate RPC server process and wait until the reader loop finishes."""
|
||||
self.closing = True
|
||||
await self.stop_io_for_all_accounts()
|
||||
await self.events_task
|
||||
self.process.terminate()
|
||||
await self.reader_task
|
||||
self.stop_io_for_all_accounts()
|
||||
self.events_thread.join()
|
||||
self.process.stdin.close()
|
||||
self.reader_thread.join()
|
||||
self.request_queue.put(None)
|
||||
self.writer_thread.join()
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.start()
|
||||
def __enter__(self):
|
||||
self.start()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, _exc_type, _exc, _tb):
|
||||
await self.close()
|
||||
def __exit__(self, _exc_type, _exc, _tb):
|
||||
self.close()
|
||||
|
||||
async def reader_loop(self) -> None:
|
||||
while True:
|
||||
line = await self.process.stdout.readline() # noqa
|
||||
if not line: # EOF
|
||||
break
|
||||
response = json.loads(line)
|
||||
if "id" in response:
|
||||
fut = self.request_events.pop(response["id"])
|
||||
fut.set_result(response)
|
||||
else:
|
||||
print(response)
|
||||
def reader_loop(self) -> None:
|
||||
try:
|
||||
while True:
|
||||
line = self.process.stdout.readline()
|
||||
if not line: # EOF
|
||||
break
|
||||
response = json.loads(line)
|
||||
if "id" in response:
|
||||
response_id = response["id"]
|
||||
event = self.request_events.pop(response_id)
|
||||
self.request_results[response_id] = response
|
||||
event.set()
|
||||
else:
|
||||
logging.warning("Got a response without ID: %s", response)
|
||||
except Exception:
|
||||
# Log an exception if the reader loop dies.
|
||||
logging.exception("Exception in the reader loop")
|
||||
|
||||
async def get_queue(self, account_id: int) -> asyncio.Queue:
|
||||
def writer_loop(self) -> None:
|
||||
"""Writer loop ensuring only a single thread writes requests."""
|
||||
try:
|
||||
while True:
|
||||
request = self.request_queue.get()
|
||||
if not request:
|
||||
break
|
||||
data = (json.dumps(request) + "\n").encode()
|
||||
self.process.stdin.write(data)
|
||||
self.process.stdin.flush()
|
||||
|
||||
except Exception:
|
||||
# Log an exception if the writer loop dies.
|
||||
logging.exception("Exception in the writer loop")
|
||||
|
||||
def get_queue(self, account_id: int) -> Queue:
|
||||
if account_id not in self.event_queues:
|
||||
self.event_queues[account_id] = asyncio.Queue()
|
||||
self.event_queues[account_id] = Queue()
|
||||
return self.event_queues[account_id]
|
||||
|
||||
async def events_loop(self) -> None:
|
||||
def events_loop(self) -> None:
|
||||
"""Requests new events and distributes them between queues."""
|
||||
while True:
|
||||
if self.closing:
|
||||
return
|
||||
event = await self.get_next_event()
|
||||
account_id = event["contextId"]
|
||||
queue = await self.get_queue(account_id)
|
||||
await queue.put(event["event"])
|
||||
try:
|
||||
while True:
|
||||
if self.closing:
|
||||
return
|
||||
event = self.get_next_event()
|
||||
account_id = event["contextId"]
|
||||
queue = self.get_queue(account_id)
|
||||
queue.put(event["event"])
|
||||
except Exception:
|
||||
# Log an exception if the event loop dies.
|
||||
logging.exception("Exception in the event loop")
|
||||
|
||||
async def wait_for_event(self, account_id: int) -> Optional[dict]:
|
||||
def wait_for_event(self, account_id: int) -> Optional[dict]:
|
||||
"""Waits for the next event from the given account and returns it."""
|
||||
queue = await self.get_queue(account_id)
|
||||
return await queue.get()
|
||||
queue = self.get_queue(account_id)
|
||||
return queue.get()
|
||||
|
||||
def __getattr__(self, attr: str):
|
||||
async def method(*args, **kwargs) -> Any:
|
||||
def method(*args) -> Any:
|
||||
self.id += 1
|
||||
request_id = self.id
|
||||
|
||||
assert not (args and kwargs), "Mixing positional and keyword arguments"
|
||||
|
||||
request = {
|
||||
"jsonrpc": "2.0",
|
||||
"method": attr,
|
||||
"params": kwargs or args,
|
||||
"params": args,
|
||||
"id": self.id,
|
||||
}
|
||||
data = (json.dumps(request) + "\n").encode()
|
||||
self.process.stdin.write(data) # noqa
|
||||
loop = asyncio.get_running_loop()
|
||||
fut = loop.create_future()
|
||||
self.request_events[request_id] = fut
|
||||
response = await fut
|
||||
event = Event()
|
||||
self.request_events[request_id] = event
|
||||
self.request_queue.put(request)
|
||||
event.wait()
|
||||
|
||||
response = self.request_results.pop(request_id)
|
||||
if "error" in response:
|
||||
raise JsonRpcError(response["error"])
|
||||
if "result" in response:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
@@ -6,26 +6,26 @@ from deltachat_rpc_client import EventType, events
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_system_info(rpc) -> None:
|
||||
system_info = await rpc.get_system_info()
|
||||
def test_system_info(rpc) -> None:
|
||||
system_info = rpc.get_system_info()
|
||||
assert "arch" in system_info
|
||||
assert "deltachat_core_version" in system_info
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_sleep(rpc) -> None:
|
||||
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()
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||
sleep_5_future = executor.submit(rpc.sleep, 5.0)
|
||||
sleep_3_future = executor.submit(rpc.sleep, 3.0)
|
||||
done, pending = concurrent.futures.wait(
|
||||
[sleep_5_future, sleep_3_future],
|
||||
return_when=concurrent.futures.FIRST_COMPLETED,
|
||||
)
|
||||
assert sleep_3_future in done
|
||||
assert sleep_5_future in pending
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_email_address_validity(rpc) -> None:
|
||||
def test_email_address_validity(rpc) -> None:
|
||||
valid_addresses = [
|
||||
"email@example.com",
|
||||
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
|
||||
@@ -33,16 +33,15 @@ async def test_email_address_validity(rpc) -> None:
|
||||
invalid_addresses = ["email@", "example.com", "emai221"]
|
||||
|
||||
for addr in valid_addresses:
|
||||
assert await rpc.check_email_validity(addr)
|
||||
assert rpc.check_email_validity(addr)
|
||||
for addr in invalid_addresses:
|
||||
assert not await rpc.check_email_validity(addr)
|
||||
assert not rpc.check_email_validity(addr)
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_acfactory(acfactory) -> None:
|
||||
account = await acfactory.new_configured_account()
|
||||
def test_acfactory(acfactory) -> None:
|
||||
account = acfactory.new_configured_account()
|
||||
while True:
|
||||
event = await account.wait_for_event()
|
||||
event = account.wait_for_event()
|
||||
if event.type == EventType.CONFIGURE_PROGRESS:
|
||||
assert event.progress != 0 # Progress 0 indicates error.
|
||||
if event.progress == 1000: # Success
|
||||
@@ -52,238 +51,241 @@ async def test_acfactory(acfactory) -> None:
|
||||
print("Successful configuration")
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_configure_starttls(acfactory) -> None:
|
||||
account = await acfactory.new_preconfigured_account()
|
||||
def test_configure_starttls(acfactory) -> None:
|
||||
account = acfactory.new_preconfigured_account()
|
||||
|
||||
# Use STARTTLS
|
||||
await account.set_config("mail_security", "2")
|
||||
await account.set_config("send_security", "2")
|
||||
await account.configure()
|
||||
assert await account.is_configured()
|
||||
account.set_config("mail_security", "2")
|
||||
account.set_config("send_security", "2")
|
||||
account.configure()
|
||||
assert account.is_configured()
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_account(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
def test_account(acfactory) -> None:
|
||||
alice, bob = 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()
|
||||
await alice_chat_bob.send_text("Hello!")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
event = bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
break
|
||||
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = await message.get_snapshot()
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.chat_id == chat_id
|
||||
assert snapshot.text == "Hello!"
|
||||
await bob.mark_seen_messages([message])
|
||||
bob.mark_seen_messages([message])
|
||||
|
||||
assert alice != bob
|
||||
assert repr(alice)
|
||||
assert (await alice.get_info()).level
|
||||
assert await alice.get_size()
|
||||
assert await alice.is_configured()
|
||||
assert not await alice.get_avatar()
|
||||
assert await alice.get_contact_by_addr(bob_addr) == alice_contact_bob
|
||||
assert await alice.get_contacts()
|
||||
assert await alice.get_contacts(snapshot=True)
|
||||
assert alice.get_info().level
|
||||
assert alice.get_size()
|
||||
assert alice.is_configured()
|
||||
assert not alice.get_avatar()
|
||||
assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob
|
||||
assert alice.get_contacts()
|
||||
assert alice.get_contacts(snapshot=True)
|
||||
assert alice.self_contact
|
||||
assert await alice.get_chatlist()
|
||||
assert await alice.get_chatlist(snapshot=True)
|
||||
assert await alice.get_qr_code()
|
||||
assert await alice.get_fresh_messages()
|
||||
assert await alice.get_next_messages()
|
||||
assert alice.get_chatlist()
|
||||
assert alice.get_chatlist(snapshot=True)
|
||||
assert alice.get_qr_code()
|
||||
assert alice.get_fresh_messages()
|
||||
assert alice.get_next_messages()
|
||||
|
||||
group = await alice.create_group("test group")
|
||||
await group.add_contact(alice_contact_bob)
|
||||
group_msg = await group.send_message(text="hello")
|
||||
# Test sending empty message.
|
||||
assert len(bob.wait_next_messages()) == 0
|
||||
alice_chat_bob.send_text("")
|
||||
messages = bob.wait_next_messages()
|
||||
assert len(messages) == 1
|
||||
message = messages[0]
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.text == ""
|
||||
bob.mark_seen_messages([message])
|
||||
|
||||
group = alice.create_group("test group")
|
||||
group.add_contact(alice_contact_bob)
|
||||
group_msg = group.send_message(text="hello")
|
||||
assert group_msg == alice.get_message_by_id(group_msg.id)
|
||||
assert group == alice.get_chat_by_id(group.id)
|
||||
await alice.delete_messages([group_msg])
|
||||
alice.delete_messages([group_msg])
|
||||
|
||||
await alice.set_config("selfstatus", "test")
|
||||
assert await alice.get_config("selfstatus") == "test"
|
||||
await alice.update_config(selfstatus="test2")
|
||||
assert await alice.get_config("selfstatus") == "test2"
|
||||
alice.set_config("selfstatus", "test")
|
||||
assert alice.get_config("selfstatus") == "test"
|
||||
alice.update_config(selfstatus="test2")
|
||||
assert alice.get_config("selfstatus") == "test2"
|
||||
|
||||
assert not await alice.get_blocked_contacts()
|
||||
await alice_contact_bob.block()
|
||||
blocked_contacts = await alice.get_blocked_contacts()
|
||||
assert not alice.get_blocked_contacts()
|
||||
alice_contact_bob.block()
|
||||
blocked_contacts = alice.get_blocked_contacts()
|
||||
assert blocked_contacts
|
||||
assert blocked_contacts[0].contact == alice_contact_bob
|
||||
|
||||
await bob.remove()
|
||||
await alice.stop_io()
|
||||
bob.remove()
|
||||
alice.stop_io()
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_chat(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
def test_chat(acfactory) -> None:
|
||||
alice, bob = 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()
|
||||
await alice_chat_bob.send_text("Hello!")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
event = bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
break
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = await message.get_snapshot()
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.chat_id == chat_id
|
||||
assert snapshot.text == "Hello!"
|
||||
bob_chat_alice = bob.get_chat_by_id(chat_id)
|
||||
|
||||
assert alice_chat_bob != bob_chat_alice
|
||||
assert repr(alice_chat_bob)
|
||||
await alice_chat_bob.delete()
|
||||
assert not await bob_chat_alice.can_send()
|
||||
await bob_chat_alice.accept()
|
||||
assert await bob_chat_alice.can_send()
|
||||
await bob_chat_alice.block()
|
||||
bob_chat_alice = await snapshot.sender.create_chat()
|
||||
await bob_chat_alice.mute()
|
||||
await bob_chat_alice.unmute()
|
||||
await bob_chat_alice.pin()
|
||||
await bob_chat_alice.unpin()
|
||||
await bob_chat_alice.archive()
|
||||
await bob_chat_alice.unarchive()
|
||||
alice_chat_bob.delete()
|
||||
assert not bob_chat_alice.can_send()
|
||||
bob_chat_alice.accept()
|
||||
assert bob_chat_alice.can_send()
|
||||
bob_chat_alice.block()
|
||||
bob_chat_alice = snapshot.sender.create_chat()
|
||||
bob_chat_alice.mute()
|
||||
bob_chat_alice.unmute()
|
||||
bob_chat_alice.pin()
|
||||
bob_chat_alice.unpin()
|
||||
bob_chat_alice.archive()
|
||||
bob_chat_alice.unarchive()
|
||||
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
|
||||
await bob_chat_alice.set_name("test")
|
||||
await bob_chat_alice.set_ephemeral_timer(300)
|
||||
await bob_chat_alice.get_encryption_info()
|
||||
bob_chat_alice.set_name("test")
|
||||
bob_chat_alice.set_ephemeral_timer(300)
|
||||
bob_chat_alice.get_encryption_info()
|
||||
|
||||
group = await alice.create_group("test group")
|
||||
await group.add_contact(alice_contact_bob)
|
||||
await group.get_qr_code()
|
||||
group = alice.create_group("test group")
|
||||
group.add_contact(alice_contact_bob)
|
||||
group.get_qr_code()
|
||||
|
||||
snapshot = await group.get_basic_snapshot()
|
||||
snapshot = group.get_basic_snapshot()
|
||||
assert snapshot.name == "test group"
|
||||
await group.set_name("new name")
|
||||
snapshot = await group.get_full_snapshot()
|
||||
group.set_name("new name")
|
||||
snapshot = group.get_full_snapshot()
|
||||
assert snapshot.name == "new name"
|
||||
|
||||
msg = await group.send_message(text="hi")
|
||||
assert (await msg.get_snapshot()).text == "hi"
|
||||
await group.forward_messages([msg])
|
||||
msg = group.send_message(text="hi")
|
||||
assert (msg.get_snapshot()).text == "hi"
|
||||
group.forward_messages([msg])
|
||||
|
||||
await group.set_draft(text="test draft")
|
||||
draft = await group.get_draft()
|
||||
group.set_draft(text="test draft")
|
||||
draft = group.get_draft()
|
||||
assert draft.text == "test draft"
|
||||
await group.remove_draft()
|
||||
assert not await group.get_draft()
|
||||
group.remove_draft()
|
||||
assert not group.get_draft()
|
||||
|
||||
assert await group.get_messages()
|
||||
await group.get_fresh_message_count()
|
||||
await group.mark_noticed()
|
||||
assert await group.get_contacts()
|
||||
await group.remove_contact(alice_chat_bob)
|
||||
await group.get_locations()
|
||||
assert group.get_messages()
|
||||
group.get_fresh_message_count()
|
||||
group.mark_noticed()
|
||||
assert group.get_contacts()
|
||||
group.remove_contact(alice_chat_bob)
|
||||
group.get_locations()
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_contact(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
def test_contact(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = await bob.get_config("addr")
|
||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
|
||||
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
|
||||
assert repr(alice_contact_bob)
|
||||
await alice_contact_bob.block()
|
||||
await alice_contact_bob.unblock()
|
||||
await alice_contact_bob.set_name("new name")
|
||||
await alice_contact_bob.get_encryption_info()
|
||||
snapshot = await alice_contact_bob.get_snapshot()
|
||||
alice_contact_bob.block()
|
||||
alice_contact_bob.unblock()
|
||||
alice_contact_bob.set_name("new name")
|
||||
alice_contact_bob.get_encryption_info()
|
||||
snapshot = alice_contact_bob.get_snapshot()
|
||||
assert snapshot.address == bob_addr
|
||||
await alice_contact_bob.create_chat()
|
||||
alice_contact_bob.create_chat()
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_message(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
def test_message(acfactory) -> None:
|
||||
alice, bob = 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()
|
||||
await alice_chat_bob.send_text("Hello!")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
event = bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
break
|
||||
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = await message.get_snapshot()
|
||||
snapshot = 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
|
||||
await snapshot.chat.send_text("hi")
|
||||
await snapshot.chat.accept()
|
||||
await snapshot.chat.send_text("hi")
|
||||
snapshot.chat.send_text("hi")
|
||||
snapshot.chat.accept()
|
||||
snapshot.chat.send_text("hi")
|
||||
|
||||
await message.mark_seen()
|
||||
await message.send_reaction("😎")
|
||||
reactions = await message.get_reactions()
|
||||
message.mark_seen()
|
||||
message.send_reaction("😎")
|
||||
reactions = message.get_reactions()
|
||||
assert reactions
|
||||
snapshot = await message.get_snapshot()
|
||||
snapshot = message.get_snapshot()
|
||||
assert reactions == snapshot.reactions
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_is_bot(acfactory) -> None:
|
||||
def test_is_bot(acfactory) -> None:
|
||||
"""Test that we can recognize messages submitted by bots."""
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
alice, bob = 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()
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
|
||||
# Alice becomes a bot.
|
||||
await alice.set_config("bot", "1")
|
||||
await alice_chat_bob.send_text("Hello!")
|
||||
alice.set_config("bot", "1")
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
event = 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()
|
||||
snapshot = 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:
|
||||
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()
|
||||
user = (acfactory.get_online_accounts(1))[0]
|
||||
bot = acfactory.new_configured_bot()
|
||||
bot2 = acfactory.new_configured_bot()
|
||||
|
||||
assert await bot.is_configured()
|
||||
assert await bot.account.get_config("bot") == "1"
|
||||
assert bot.is_configured()
|
||||
assert bot.account.get_config("bot") == "1"
|
||||
|
||||
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
|
||||
bot.add_hook(*hook)
|
||||
event = 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()
|
||||
event = acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
|
||||
snapshot = 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)
|
||||
@@ -295,43 +297,52 @@ async def test_bot(acfactory) -> None:
|
||||
hook = track, events.NewMessage(r"hello")
|
||||
bot.add_hook(*hook)
|
||||
bot.add_hook(track, events.NewMessage(command="/help"))
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
event = acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
mock.hook.assert_called_with(event.msg_id)
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!")
|
||||
event = 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")
|
||||
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!")
|
||||
acfactory.process_message(from_account=user, to_client=bot, text="hey!")
|
||||
assert len(mock.hook.mock_calls) == 2
|
||||
bot.remove_hook(*hook)
|
||||
|
||||
mock.hook.reset_mock()
|
||||
await acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="/help")
|
||||
acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
event = acfactory.process_message(from_account=user, to_client=bot, text="/help")
|
||||
mock.hook.assert_called_once_with(event.msg_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_wait_next_messages(acfactory) -> None:
|
||||
alice = await acfactory.new_configured_account()
|
||||
def test_wait_next_messages(acfactory) -> None:
|
||||
alice = acfactory.new_configured_account()
|
||||
|
||||
# Create a bot account so it does not receive device messages in the beginning.
|
||||
bot = await acfactory.new_preconfigured_account()
|
||||
await bot.set_config("bot", "1")
|
||||
await bot.configure()
|
||||
bot = acfactory.new_preconfigured_account()
|
||||
bot.set_config("bot", "1")
|
||||
bot.configure()
|
||||
|
||||
# There are no old messages and the call returns immediately.
|
||||
assert not await bot.wait_next_messages()
|
||||
assert not bot.wait_next_messages()
|
||||
|
||||
# Bot starts waiting for messages.
|
||||
next_messages_task = asyncio.create_task(bot.wait_next_messages())
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
# Bot starts waiting for messages.
|
||||
next_messages_task = executor.submit(bot.wait_next_messages)
|
||||
|
||||
bot_addr = await bot.get_config("addr")
|
||||
alice_contact_bot = await alice.create_contact(bot_addr, "Bob")
|
||||
alice_chat_bot = await alice_contact_bot.create_chat()
|
||||
await alice_chat_bot.send_text("Hello!")
|
||||
bot_addr = bot.get_config("addr")
|
||||
alice_contact_bot = alice.create_contact(bot_addr, "Bob")
|
||||
alice_chat_bot = alice_contact_bot.create_chat()
|
||||
alice_chat_bot.send_text("Hello!")
|
||||
|
||||
next_messages = await next_messages_task
|
||||
assert len(next_messages) == 1
|
||||
snapshot = await next_messages[0].get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
next_messages = next_messages_task.result()
|
||||
assert len(next_messages) == 1
|
||||
snapshot = next_messages[0].get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
|
||||
|
||||
def test_import_export(acfactory, tmp_path) -> None:
|
||||
alice = acfactory.new_configured_account()
|
||||
alice.export_backup(tmp_path)
|
||||
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.import_backup(files[0])
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import pytest
|
||||
from deltachat_rpc_client import EventType
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_webxdc(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
def test_webxdc(acfactory) -> None:
|
||||
alice, bob = 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()
|
||||
await alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
event = bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
||||
message = bob.get_message_by_id(event.msg_id)
|
||||
break
|
||||
|
||||
webxdc_info = await message.get_webxdc_info()
|
||||
webxdc_info = message.get_webxdc_info()
|
||||
assert webxdc_info == {
|
||||
"document": None,
|
||||
"icon": "icon.png",
|
||||
@@ -28,20 +26,20 @@ async def test_webxdc(acfactory) -> None:
|
||||
"summary": None,
|
||||
}
|
||||
|
||||
status_updates = await message.get_webxdc_status_updates()
|
||||
status_updates = message.get_webxdc_status_updates()
|
||||
assert status_updates == []
|
||||
|
||||
await bob_chat_alice.accept()
|
||||
await message.send_webxdc_status_update({"payload": 42}, "")
|
||||
await message.send_webxdc_status_update({"payload": "Second update"}, "description")
|
||||
bob_chat_alice.accept()
|
||||
message.send_webxdc_status_update({"payload": 42}, "")
|
||||
message.send_webxdc_status_update({"payload": "Second update"}, "description")
|
||||
|
||||
status_updates = await message.get_webxdc_status_updates()
|
||||
status_updates = message.get_webxdc_status_updates()
|
||||
assert status_updates == [
|
||||
{"payload": 42, "serial": 1, "max_serial": 2},
|
||||
{"payload": "Second update", "serial": 2, "max_serial": 2},
|
||||
]
|
||||
|
||||
status_updates = await message.get_webxdc_status_updates(1)
|
||||
status_updates = message.get_webxdc_status_updates(1)
|
||||
assert status_updates == [
|
||||
{"payload": "Second update", "serial": 2, "max_serial": 2},
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@ envlist =
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
pytest {posargs}
|
||||
pytest -n6 {posargs}
|
||||
setenv =
|
||||
# Avoid stack overflow when Rust core is built without optimizations.
|
||||
RUST_MIN_STACK=8388608
|
||||
@@ -14,10 +14,8 @@ passenv =
|
||||
DCC_NEW_TMP_EMAIL
|
||||
deps =
|
||||
pytest
|
||||
pytest-asyncio
|
||||
pytest-timeout
|
||||
aiohttp
|
||||
aiodns
|
||||
pytest-xdist
|
||||
|
||||
[testenv:lint]
|
||||
skipsdist = True
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.115.0"
|
||||
version = "1.126.1"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
@@ -17,11 +17,11 @@ anyhow = "1"
|
||||
env_logger = { version = "0.10.0" }
|
||||
futures-lite = "1.13.0"
|
||||
log = "0.4"
|
||||
serde_json = "1.0.96"
|
||||
serde_json = "1.0.99"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.28.0", features = ["io-std"] }
|
||||
tokio = { version = "1.29.1", features = ["io-std"] }
|
||||
tokio-util = "0.7.8"
|
||||
yerpc = { version = "0.4.0", features = ["anyhow_expose"] }
|
||||
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Delta Chat core RPC server.
|
||||
//!
|
||||
//! It speaks JSON Lines over stdio.
|
||||
use std::env;
|
||||
///! Delta Chat core RPC server.
|
||||
///!
|
||||
///! It speaks JSON Lines over stdio.
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
|
||||
18
deny.toml
18
deny.toml
@@ -2,9 +2,7 @@
|
||||
unmaintained = "allow"
|
||||
ignore = [
|
||||
"RUSTSEC-2020-0071",
|
||||
|
||||
# Only affects windows if using non-default allocator (and unmaintained).
|
||||
"RUSTSEC-2021-0145",
|
||||
"RUSTSEC-2022-0093",
|
||||
]
|
||||
|
||||
[bans]
|
||||
@@ -17,8 +15,6 @@ skip = [
|
||||
{ name = "base64", version = "<0.21" },
|
||||
{ name = "bitflags", version = "1.3.2" },
|
||||
{ name = "block-buffer", version = "<0.10" },
|
||||
{ name = "clap_lex", version = "0.2.4" },
|
||||
{ name = "clap", version = "3.2.23" },
|
||||
{ name = "convert_case", version = "0.4.0" },
|
||||
{ name = "curve25519-dalek", version = "3.2.0" },
|
||||
{ name = "darling_core", version = "<0.14" },
|
||||
@@ -28,12 +24,12 @@ skip = [
|
||||
{ name = "digest", version = "<0.10" },
|
||||
{ name = "ed25519-dalek", version = "1.0.1" },
|
||||
{ name = "ed25519", version = "1.5.3" },
|
||||
{ name = "env_logger", version = "<0.10" },
|
||||
{ name = "getrandom", version = "<0.2" },
|
||||
{ name = "hermit-abi", version = "<0.3" },
|
||||
{ name = "humantime", version = "<2.1" },
|
||||
{ name = "hashbrown", version = "<0.14.0" },
|
||||
{ name = "idna", version = "<0.3" },
|
||||
{ name = "libm", version = "0.1.4" },
|
||||
{ name = "indexmap", version = "<2.0.0" },
|
||||
{ name = "linux-raw-sys", version = "0.3.8" },
|
||||
{ name = "num-derive", version = "0.3.3" },
|
||||
{ name = "pem-rfc7468", version = "0.6.0" },
|
||||
{ name = "pkcs8", version = "0.9.0" },
|
||||
{ name = "quick-error", version = "<2.0" },
|
||||
@@ -42,9 +38,11 @@ skip = [
|
||||
{ name = "rand", version = "<0.8" },
|
||||
{ name = "redox_syscall", version = "0.2.16" },
|
||||
{ name = "regex-syntax", version = "0.6.29" },
|
||||
{ name = "rustix", version = "0.37.21" },
|
||||
{ name = "sec1", version = "0.3.0" },
|
||||
{ name = "sha2", version = "<0.10" },
|
||||
{ name = "signature", version = "1.6.4" },
|
||||
{ name = "socket2", version = "0.4.9" },
|
||||
{ name = "spin", version = "<0.9.6" },
|
||||
{ name = "spki", version = "0.6.0" },
|
||||
{ name = "syn", version = "1.0.109" },
|
||||
@@ -57,8 +55,10 @@ skip = [
|
||||
{ name = "windows-sys", version = "<0.48" },
|
||||
{ name = "windows-targets", version = "<0.48" },
|
||||
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
|
||||
{ name = "windows", version = "0.32.0" },
|
||||
{ name = "windows_x86_64_gnu", version = "<0.48" },
|
||||
{ name = "windows_x86_64_msvc", version = "<0.48" },
|
||||
{ name = "winreg", version = "0.10.1" },
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -108,7 +108,7 @@ The most obvious alternative would be to create a new contact with the new addre
|
||||
#### Upsides:
|
||||
- With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
|
||||
- (Also, less important: Slightly faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
|
||||
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't wast that much development time.)
|
||||
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't waste that much development time.)
|
||||
|
||||
[full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161)
|
||||
|
||||
|
||||
128
fuzz/Cargo.lock
generated
128
fuzz/Cargo.lock
generated
@@ -2,12 +2,6 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "Inflector"
|
||||
version = "0.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
|
||||
|
||||
[[package]]
|
||||
name = "abao"
|
||||
version = "0.2.0"
|
||||
@@ -60,19 +54,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.20"
|
||||
version = "1.0.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cc936419f96fa211c1b9166887b38e5e40b19958e5b895be7c1f93adec7071ac"
|
||||
checksum = "43f6cb1bf222025340178f382c426f13757b2960e89779dfcb319c32542a5a41"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aliasable"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
|
||||
|
||||
[[package]]
|
||||
name = "alloc-no-stdlib"
|
||||
version = "2.0.4"
|
||||
@@ -189,12 +177,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-imap"
|
||||
version = "0.7.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8379e2f1cdeb79afd2006932d7e8f64993fc0f7386d0ebc37231c90b05968c25"
|
||||
checksum = "da93622739d458dd9a6abc1abf0e38e81965a5824a3b37f9500437c82a8bb572"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"async-native-tls 0.4.0",
|
||||
"base64 0.21.0",
|
||||
"byte-pool",
|
||||
"chrono",
|
||||
@@ -203,25 +190,13 @@ dependencies = [
|
||||
"log",
|
||||
"nom",
|
||||
"once_cell",
|
||||
"ouroboros",
|
||||
"pin-utils",
|
||||
"self_cell",
|
||||
"stop-token",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-native-tls"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d57d4cec3c647232e1094dc013546c0b33ce785d8aeb251e1f20dfaf8a9a13fe"
|
||||
dependencies = [
|
||||
"native-tls",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"url",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "async-native-tls"
|
||||
version = "0.5.0"
|
||||
@@ -556,9 +531,9 @@ checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba"
|
||||
|
||||
[[package]]
|
||||
name = "byte-pool"
|
||||
version = "0.2.3"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8c7230ddbb427b1094d477d821a99f3f54d36333178eeb806e279bcdcecf0ca"
|
||||
checksum = "c2f1b21189f50b5625efa6227cf45e9d4cfdc2e73582df2b879e9689e78a7158"
|
||||
dependencies = [
|
||||
"crossbeam-queue",
|
||||
"stable_deref_trait",
|
||||
@@ -951,12 +926,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.112.6"
|
||||
version = "1.117.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
"async-imap",
|
||||
"async-native-tls 0.5.0",
|
||||
"async-native-tls",
|
||||
"async-smtp",
|
||||
"async_zip",
|
||||
"backtrace",
|
||||
@@ -979,6 +954,7 @@ dependencies = [
|
||||
"lettre_email",
|
||||
"libc",
|
||||
"mailparse 0.14.0",
|
||||
"mime",
|
||||
"num-derive",
|
||||
"num-traits",
|
||||
"num_cpus",
|
||||
@@ -1244,7 +1220,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e9c280362032ea4203659fc489832d0204ef09f247a0506f170dafcac08c369"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"signature 1.6.4",
|
||||
"signature 2.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1701,9 +1677,9 @@ checksum = "00f5fb52a06bdcadeb54e8d3671f8888a39697dcb0b81b23b55174030427f4eb"
|
||||
|
||||
[[package]]
|
||||
name = "futures-lite"
|
||||
version = "1.12.0"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7694489acd39452c77daa48516b894c153f192c3578d5a839b62c58099fcbf48"
|
||||
checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce"
|
||||
dependencies = [
|
||||
"fastrand",
|
||||
"futures-core",
|
||||
@@ -2397,9 +2373,9 @@ checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.16"
|
||||
version = "0.3.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
||||
|
||||
[[package]]
|
||||
name = "minimal-lexical"
|
||||
@@ -2651,9 +2627,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "once_cell"
|
||||
version = "1.17.0"
|
||||
version = "1.18.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f61fba1741ea2b3d6a1e3178721804bb716a68a6aeba1149b5d52e3d464ea66"
|
||||
checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
@@ -2663,9 +2639,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5"
|
||||
|
||||
[[package]]
|
||||
name = "openssl"
|
||||
version = "0.10.48"
|
||||
version = "0.10.55"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "518915b97df115dd36109bfa429a48b8f737bd05508cf9588977b599648926d2"
|
||||
checksum = "345df152bc43501c5eb9e4654ff05f794effb78d4efe3d53abc158baddc0703d"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"cfg-if",
|
||||
@@ -2704,11 +2680,10 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "openssl-sys"
|
||||
version = "0.9.83"
|
||||
version = "0.9.90"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "666416d899cf077260dac8698d60a60b435a46d57e82acb1be3d0dad87284e5b"
|
||||
checksum = "374533b0e45f3a7ced10fcaeccca020e66656bc03dac384f852e4e5a7a8104a6"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"cc",
|
||||
"libc",
|
||||
"openssl-src",
|
||||
@@ -2716,29 +2691,6 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ouroboros"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dfbb50b356159620db6ac971c6d5c9ab788c9cc38a6f49619fca2a27acb062ca"
|
||||
dependencies = [
|
||||
"aliasable",
|
||||
"ouroboros_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ouroboros_macro"
|
||||
version = "0.15.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4a0d9d1a6191c4f391f87219d1ea42b23f09ee84d64763cd05ee6ea88d9f384d"
|
||||
dependencies = [
|
||||
"Inflector",
|
||||
"proc-macro-error",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 1.0.107",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
@@ -2865,9 +2817,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "percent-encoding"
|
||||
version = "2.2.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
|
||||
checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94"
|
||||
|
||||
[[package]]
|
||||
name = "pgp"
|
||||
@@ -3316,13 +3268,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "regex"
|
||||
version = "1.7.0"
|
||||
version = "1.8.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e076559ef8e241f2ae3479e36f97bd5741c0330689e217ad51ce2c76808b868a"
|
||||
checksum = "d0ab3ca65655bb1e41f2a8c8cd662eb4fb035e67c3f78da1d61dffe89d07300f"
|
||||
dependencies = [
|
||||
"aho-corasick",
|
||||
"memchr",
|
||||
"regex-syntax",
|
||||
"regex-syntax 0.7.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3331,7 +3283,7 @@ version = "0.1.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
|
||||
dependencies = [
|
||||
"regex-syntax",
|
||||
"regex-syntax 0.6.28",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -3341,10 +3293,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.16"
|
||||
name = "regex-syntax"
|
||||
version = "0.7.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254"
|
||||
checksum = "436b050e76ed2903236f032a59761c1eb99e1b0aead2c257922771dab1fc8c78"
|
||||
|
||||
[[package]]
|
||||
name = "reqwest"
|
||||
version = "0.11.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cde824a14b7c14f85caff81225f411faacc04a2013f41670f41443742b1c1c55"
|
||||
dependencies = [
|
||||
"base64 0.21.0",
|
||||
"bytes",
|
||||
@@ -3690,6 +3648,12 @@ dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "self_cell"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4c309e515543e67811222dbc9e3dd7e1056279b782e1dacffe4242b718734fb6"
|
||||
|
||||
[[package]]
|
||||
name = "semver"
|
||||
version = "1.0.17"
|
||||
@@ -4249,9 +4213,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.11"
|
||||
version = "0.1.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d660770404473ccd7bc9f8b28494a811bc18542b915c0855c51e8f419d5223ce"
|
||||
checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
@@ -4275,9 +4239,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-util"
|
||||
version = "0.7.7"
|
||||
version = "0.7.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5427d89453009325de0d8f342c9490009f76e999cb7672d77e46267448f7e6b2"
|
||||
checksum = "806fe8c2c87eccc8b3267cbae29ed3ab2d0bd37fca70ab622e46aaa9375ddb7d"
|
||||
dependencies = [
|
||||
"bytes",
|
||||
"futures-core",
|
||||
|
||||
@@ -48,6 +48,7 @@ module.exports = {
|
||||
DC_EVENT_LOCATION_CHANGED: 2035,
|
||||
DC_EVENT_MSGS_CHANGED: 2000,
|
||||
DC_EVENT_MSGS_NOTICED: 2008,
|
||||
DC_EVENT_MSG_DELETED: 2016,
|
||||
DC_EVENT_MSG_DELIVERED: 2010,
|
||||
DC_EVENT_MSG_FAILED: 2012,
|
||||
DC_EVENT_MSG_READ: 2015,
|
||||
@@ -89,6 +90,7 @@ module.exports = {
|
||||
DC_KEY_GEN_DEFAULT: 0,
|
||||
DC_KEY_GEN_ED25519: 2,
|
||||
DC_KEY_GEN_RSA2048: 1,
|
||||
DC_KEY_GEN_RSA4096: 3,
|
||||
DC_LP_AUTH_NORMAL: 4,
|
||||
DC_LP_AUTH_OAUTH2: 2,
|
||||
DC_MEDIA_QUALITY_BALANCED: 0,
|
||||
|
||||
@@ -22,6 +22,7 @@ module.exports = {
|
||||
2010: 'DC_EVENT_MSG_DELIVERED',
|
||||
2012: 'DC_EVENT_MSG_FAILED',
|
||||
2015: 'DC_EVENT_MSG_READ',
|
||||
2016: 'DC_EVENT_MSG_DELETED',
|
||||
2020: 'DC_EVENT_CHAT_MODIFIED',
|
||||
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
|
||||
2030: 'DC_EVENT_CONTACTS_CHANGED',
|
||||
|
||||
@@ -48,6 +48,7 @@ export enum C {
|
||||
DC_EVENT_LOCATION_CHANGED = 2035,
|
||||
DC_EVENT_MSGS_CHANGED = 2000,
|
||||
DC_EVENT_MSGS_NOTICED = 2008,
|
||||
DC_EVENT_MSG_DELETED = 2016,
|
||||
DC_EVENT_MSG_DELIVERED = 2010,
|
||||
DC_EVENT_MSG_FAILED = 2012,
|
||||
DC_EVENT_MSG_READ = 2015,
|
||||
@@ -89,6 +90,7 @@ export enum C {
|
||||
DC_KEY_GEN_DEFAULT = 0,
|
||||
DC_KEY_GEN_ED25519 = 2,
|
||||
DC_KEY_GEN_RSA2048 = 1,
|
||||
DC_KEY_GEN_RSA4096 = 3,
|
||||
DC_LP_AUTH_NORMAL = 4,
|
||||
DC_LP_AUTH_OAUTH2 = 2,
|
||||
DC_MEDIA_QUALITY_BALANCED = 0,
|
||||
@@ -307,6 +309,7 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
2010: 'DC_EVENT_MSG_DELIVERED',
|
||||
2012: 'DC_EVENT_MSG_FAILED',
|
||||
2015: 'DC_EVENT_MSG_READ',
|
||||
2016: 'DC_EVENT_MSG_DELETED',
|
||||
2020: 'DC_EVENT_CHAT_MODIFIED',
|
||||
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
|
||||
2030: 'DC_EVENT_CONTACTS_CHANGED',
|
||||
|
||||
@@ -36,7 +36,7 @@ export class Context extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens a stanalone context (without an account manager)
|
||||
/** Opens a standalone context (without an account manager)
|
||||
* automatically starts the event handler */
|
||||
static open(cwd: string): Context {
|
||||
const dbFile = join(cwd, 'db.sqlite')
|
||||
|
||||
@@ -446,7 +446,8 @@ describe('Offline Tests with unconfigured account', function () {
|
||||
context.setChatProfileImage(chatId, imagePath)
|
||||
const blobPath = context.getChat(chatId).getProfileImage()
|
||||
expect(blobPath.startsWith(blobs)).to.be.true
|
||||
expect(blobPath.endsWith(image)).to.be.true
|
||||
expect(blobPath.includes('image')).to.be.true
|
||||
expect(blobPath.endsWith('.jpeg')).to.be.true
|
||||
|
||||
context.setChatProfileImage(chatId, null)
|
||||
expect(context.getChat(chatId).getProfileImage()).to.be.equal(
|
||||
|
||||
@@ -60,5 +60,5 @@
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.115.0"
|
||||
"version": "1.126.1"
|
||||
}
|
||||
|
||||
@@ -357,7 +357,7 @@ Exhibit A - Source Code Form License Notice
|
||||
|
||||
This Source Code Form is subject to the terms of the Mozilla Public
|
||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||
|
||||
If it is not possible or desirable to put the notice in a particular
|
||||
file, then You may include the notice in a location (such as a LICENSE
|
||||
|
||||
@@ -2,34 +2,34 @@
|
||||
high level API reference
|
||||
========================
|
||||
|
||||
- :class:`deltachat.account.Account` (your main entry point, creates the
|
||||
- :class:`deltachat.Account` (your main entry point, creates the
|
||||
other classes)
|
||||
- :class:`deltachat.contact.Contact`
|
||||
- :class:`deltachat.chat.Chat`
|
||||
- :class:`deltachat.message.Message`
|
||||
- :class:`deltachat.Contact`
|
||||
- :class:`deltachat.Chat`
|
||||
- :class:`deltachat.Message`
|
||||
|
||||
Account
|
||||
-------
|
||||
|
||||
.. autoclass:: deltachat.account.Account
|
||||
.. autoclass:: deltachat.Account
|
||||
:members:
|
||||
|
||||
|
||||
Contact
|
||||
-------
|
||||
|
||||
.. autoclass:: deltachat.contact.Contact
|
||||
.. autoclass:: deltachat.Contact
|
||||
:members:
|
||||
|
||||
Chat
|
||||
----
|
||||
|
||||
.. autoclass:: deltachat.chat.Chat
|
||||
.. autoclass:: deltachat.Chat
|
||||
:members:
|
||||
|
||||
Message
|
||||
-------
|
||||
|
||||
.. autoclass:: deltachat.message.Message
|
||||
.. autoclass:: deltachat.Message
|
||||
:members:
|
||||
|
||||
|
||||
@@ -32,25 +32,13 @@ class GroupTrackingPlugin:
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, actor, message):
|
||||
print(
|
||||
"ac_member_added {} to chat {} from {}".format(
|
||||
contact.addr,
|
||||
chat.id,
|
||||
actor or message.get_sender_contact().addr,
|
||||
),
|
||||
)
|
||||
print(f"ac_member_added {contact.addr} to chat {chat.id} from {actor or message.get_sender_contact().addr}")
|
||||
for member in chat.get_contacts():
|
||||
print(f"chat member: {member.addr}")
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_removed(self, chat, contact, actor, message):
|
||||
print(
|
||||
"ac_member_removed {} from chat {} by {}".format(
|
||||
contact.addr,
|
||||
chat.id,
|
||||
actor or message.get_sender_contact().addr,
|
||||
),
|
||||
)
|
||||
print(f"ac_member_removed {contact.addr} from chat {chat.id} by {actor or message.get_sender_contact().addr}")
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
|
||||
@@ -24,8 +24,6 @@ def test_echo_quit_plugin(acfactory, lp):
|
||||
lp.sec("creating a temp account to contact the bot")
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
botproc.await_resync()
|
||||
|
||||
lp.sec("sending a message to the bot")
|
||||
bot_contact = ac1.create_contact(botproc.addr)
|
||||
bot_chat = bot_contact.create_chat()
|
||||
@@ -54,8 +52,6 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
ac1.add_account_plugin(FFIEventLogger(ac1))
|
||||
ac2.add_account_plugin(FFIEventLogger(ac2))
|
||||
|
||||
botproc.await_resync()
|
||||
|
||||
lp.sec("creating bot test group with bot")
|
||||
bot_contact = ac1.create_contact(botproc.addr)
|
||||
ch = ac1.create_group_chat("bot test group")
|
||||
|
||||
@@ -24,3 +24,5 @@ ignore_missing_imports = True
|
||||
[mypy-imap_tools.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-distutils.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
@@ -11,10 +11,11 @@ authors = [
|
||||
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Communications :: Chat",
|
||||
"Topic :: Communications :: Email",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
]
|
||||
@@ -33,6 +34,7 @@ dynamic = [
|
||||
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
|
||||
"Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues"
|
||||
"Documentation" = "https://py.delta.chat/"
|
||||
"Mastodon" = "https://chaos.social/@delta"
|
||||
|
||||
[project.entry-points.pytest11]
|
||||
"deltachat.testplugin" = "deltachat.testplugin"
|
||||
|
||||
@@ -6,7 +6,7 @@ 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 TYPE_CHECKING, Any, Dict, Generator, List, Optional, Union
|
||||
|
||||
from . import const, hookspec
|
||||
from .capi import ffi, lib
|
||||
@@ -195,7 +195,7 @@ class Account:
|
||||
assert res != ffi.NULL, f"config value not found for: {name!r}"
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None:
|
||||
def _preconfigure_keypair(self, addr: str, secret: str) -> None:
|
||||
"""See dc_preconfigure_keypair() in deltachat.h.
|
||||
|
||||
In other words, you don't need this.
|
||||
@@ -203,7 +203,7 @@ class Account:
|
||||
res = lib.dc_preconfigure_keypair(
|
||||
self._dc_context,
|
||||
as_dc_charpointer(addr),
|
||||
as_dc_charpointer(public),
|
||||
ffi.NULL,
|
||||
as_dc_charpointer(secret),
|
||||
)
|
||||
if res == 0:
|
||||
@@ -427,7 +427,7 @@ class Account:
|
||||
|
||||
assert dc_chatlist != ffi.NULL
|
||||
chatlist = []
|
||||
for i in range(0, lib.dc_chatlist_get_cnt(dc_chatlist)):
|
||||
for i in range(lib.dc_chatlist_get_cnt(dc_chatlist)):
|
||||
chat_id = lib.dc_chatlist_get_chat_id(dc_chatlist, i)
|
||||
chatlist.append(Chat(self, chat_id))
|
||||
return chatlist
|
||||
@@ -617,18 +617,18 @@ class Account:
|
||||
# meta API for start/stop and event based processing
|
||||
#
|
||||
|
||||
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False):
|
||||
from .events import FFIEventLogger
|
||||
|
||||
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False, displayname=None):
|
||||
"""get the account running, configure it if necessary. add plugins if provided.
|
||||
|
||||
:param addr: the email address of the account
|
||||
:param password: the password of the account
|
||||
:param account_plugins: a list of plugins to add
|
||||
:param show_ffi: show low level ffi events
|
||||
:param displayname: the display name of the account
|
||||
"""
|
||||
from .events import FFIEventLogger
|
||||
|
||||
if show_ffi:
|
||||
self.set_config("displayname", "bot")
|
||||
log = FFIEventLogger(self)
|
||||
self.add_account_plugin(log)
|
||||
|
||||
@@ -644,6 +644,8 @@ class Account:
|
||||
configtracker = self.configure()
|
||||
configtracker.wait_finish()
|
||||
|
||||
if displayname:
|
||||
self.set_config("displayname", displayname)
|
||||
# start IO threads and configure if necessary
|
||||
self.start_io()
|
||||
|
||||
|
||||
@@ -71,13 +71,16 @@ class Contact:
|
||||
"""Unblock this contact. Messages from this contact will be retrieved (again)."""
|
||||
return lib.dc_block_contact(self.account._dc_context, self.id, False)
|
||||
|
||||
def is_verified(self):
|
||||
def is_verified(self) -> bool:
|
||||
"""Return True if the contact is verified."""
|
||||
return lib.dc_contact_is_verified(self._dc_contact)
|
||||
return lib.dc_contact_is_verified(self._dc_contact) == 2
|
||||
|
||||
def get_verifier(self, contact):
|
||||
def get_verifier(self, contact) -> Optional["Contact"]:
|
||||
"""Return the address of the contact that verified the contact."""
|
||||
return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact))
|
||||
verifier_id = lib.dc_contact_get_verifier_id(contact._dc_contact)
|
||||
if verifier_id == 0:
|
||||
return None
|
||||
return Contact(self.account, verifier_id)
|
||||
|
||||
def get_profile_image(self) -> Optional[str]:
|
||||
"""Get contact profile image.
|
||||
|
||||
@@ -15,7 +15,7 @@ def as_dc_charpointer(obj):
|
||||
|
||||
|
||||
def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]:
|
||||
for i in range(0, lib.dc_array_get_cnt(dc_array_t)):
|
||||
for i in range(lib.dc_array_get_cnt(dc_array_t)):
|
||||
yield constructor(lib.dc_array_get_id(dc_array_t, i))
|
||||
|
||||
|
||||
|
||||
@@ -9,11 +9,11 @@ from contextlib import contextmanager
|
||||
from queue import Empty, Queue
|
||||
|
||||
from . import const
|
||||
from .account import Account
|
||||
from .capi import ffi, lib
|
||||
from .cutil import from_optional_dc_charpointer
|
||||
from .hookspec import account_hookimpl
|
||||
from .message import map_system_message
|
||||
from .account import Account
|
||||
|
||||
|
||||
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
||||
|
||||
@@ -486,6 +486,9 @@ class Message:
|
||||
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
|
||||
return lib.dc_msg_get_download_state(dc_msg)
|
||||
|
||||
def download_full(self) -> None:
|
||||
lib.dc_download_full_msg(self.account._dc_context, self.id)
|
||||
|
||||
|
||||
# some code for handling DC_MSG_* view types
|
||||
|
||||
@@ -507,8 +510,7 @@ def get_viewtype_code_from_name(view_type_name):
|
||||
if code is not None:
|
||||
return code
|
||||
raise ValueError(
|
||||
"message typecode not found for {!r}, "
|
||||
"available {!r}".format(view_type_name, list(_view_type_mapping.keys())),
|
||||
f"message typecode not found for {view_type_name!r}, available {list(_view_type_mapping.keys())!r}",
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ import threading
|
||||
import time
|
||||
import weakref
|
||||
from queue import Queue
|
||||
from typing import Callable, List, Optional, Dict, Set
|
||||
from typing import Callable, Dict, List, Optional, Set
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
@@ -137,6 +137,9 @@ def pytest_report_header(config, startdir):
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def testprocess(request):
|
||||
"""Return live account configuration manager.
|
||||
|
||||
The returned object is a :class:`TestProcess` object."""
|
||||
return TestProcess(pytestconfig=request.config)
|
||||
|
||||
|
||||
@@ -231,6 +234,8 @@ def write_dict_to_dir(dic, target_dir):
|
||||
|
||||
@pytest.fixture()
|
||||
def data(request):
|
||||
"""Test data."""
|
||||
|
||||
class Data:
|
||||
def __init__(self) -> None:
|
||||
# trying to find test data heuristically
|
||||
@@ -473,10 +478,9 @@ class ACFactory:
|
||||
except IndexError:
|
||||
pass
|
||||
else:
|
||||
fname_pub = self.data.read_path(f"key/{keyname}-public.asc")
|
||||
fname_sec = self.data.read_path(f"key/{keyname}-secret.asc")
|
||||
if fname_pub and fname_sec:
|
||||
account._preconfigure_keypair(addr, fname_pub, fname_sec)
|
||||
if fname_sec:
|
||||
account._preconfigure_keypair(addr, fname_sec)
|
||||
return True
|
||||
print(f"WARN: could not use preconfigured keys for {addr!r}")
|
||||
|
||||
@@ -614,6 +618,7 @@ class ACFactory:
|
||||
|
||||
@pytest.fixture()
|
||||
def acfactory(request, tmpdir, testprocess, data):
|
||||
"""Account factory."""
|
||||
am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testprocess, data=data)
|
||||
yield am
|
||||
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
|
||||
@@ -676,21 +681,17 @@ class BotProcess:
|
||||
print("+++IGN:", line)
|
||||
ignored.append(line)
|
||||
|
||||
def await_resync(self):
|
||||
self.fnmatch_lines(
|
||||
"""
|
||||
*Resync: collected * message IDs in folder INBOX*
|
||||
""",
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def tmp_db_path(tmpdir):
|
||||
"""Return a path inside the temporary directory where the database can be created."""
|
||||
return tmpdir.join("test.db").strpath
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def lp():
|
||||
"""Log printer fixture."""
|
||||
|
||||
class Printer:
|
||||
def sec(self, msg: str) -> None:
|
||||
print()
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from queue import Queue
|
||||
from threading import Event
|
||||
from typing import List, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING, List
|
||||
|
||||
from .hookspec import Global, account_hookimpl
|
||||
|
||||
|
||||
@@ -15,6 +15,6 @@ class TestEmpty:
|
||||
def test_prepare_setup_measurings(self, acfactory):
|
||||
acfactory.get_online_accounts(BENCH_NUM)
|
||||
|
||||
@pytest.mark.parametrize("num", range(0, BENCH_NUM + 1))
|
||||
@pytest.mark.parametrize("num", range(BENCH_NUM + 1))
|
||||
def test_setup_online_accounts(self, acfactory, num):
|
||||
acfactory.get_online_accounts(num)
|
||||
|
||||
@@ -8,7 +8,7 @@ import pytest
|
||||
import deltachat
|
||||
|
||||
|
||||
def test_db_busy_error(acfactory, tmpdir):
|
||||
def test_db_busy_error(acfactory):
|
||||
starttime = time.time()
|
||||
log_lock = threading.RLock()
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import deltachat as dc
|
||||
|
||||
|
||||
class TestGroupStressTests:
|
||||
@@ -149,9 +150,8 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac2: Check that ac2 verified ac1")
|
||||
# If we verified the contact ourselves then verifier addr == contact addr
|
||||
ac2_ac1_contact = ac2.get_contacts()[0]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
|
||||
|
||||
lp.sec("ac2: send message and let ac1 read it")
|
||||
chat2.send_text("world")
|
||||
@@ -176,9 +176,9 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
|
||||
lp.sec("ac2: Check that ac1 verified ac3 for ac2")
|
||||
ac2_ac1_contact = ac2.get_contacts()[0]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
|
||||
ac2_ac3_contact = ac2.get_contacts()[1]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact) == ac1_addr
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr
|
||||
|
||||
lp.sec("ac2: send message and let ac3 read it")
|
||||
chat2.send_text("hi")
|
||||
@@ -494,7 +494,7 @@ def test_multidevice_sync_seen(acfactory, lp):
|
||||
assert "Expires: " in ac1_clone_message.get_message_info()
|
||||
|
||||
|
||||
def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp):
|
||||
def test_see_new_verified_member_after_going_online(acfactory, tmp_path, lp):
|
||||
"""The test for the bug #3836:
|
||||
- Alice has two devices, the second is offline.
|
||||
- Alice creates a verified group and sends a QR invitation to Bob.
|
||||
@@ -507,9 +507,10 @@ def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp):
|
||||
for ac in [ac1, ac1_offl]:
|
||||
ac.set_config("bcc_self", "1")
|
||||
acfactory.bring_accounts_online()
|
||||
dir = tmpdir.mkdir("exportdir")
|
||||
ac1.export_self_keys(dir.strpath)
|
||||
ac1_offl.import_self_keys(dir.strpath)
|
||||
dir = tmp_path / "exportdir"
|
||||
dir.mkdir()
|
||||
ac1.export_self_keys(str(dir))
|
||||
ac1_offl.import_self_keys(str(dir))
|
||||
ac1_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
@@ -538,10 +539,8 @@ def test_see_new_verified_member_after_going_online(acfactory, tmpdir, lp):
|
||||
assert msg_in.text == msg_out.text
|
||||
assert msg_in.get_sender_contact().addr == ac2_addr
|
||||
|
||||
ac1.set_config("bcc_self", "0")
|
||||
|
||||
|
||||
def test_use_new_verified_group_after_going_online(acfactory, tmpdir, lp):
|
||||
def test_use_new_verified_group_after_going_online(acfactory, tmp_path, 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.
|
||||
@@ -556,9 +555,10 @@ def test_use_new_verified_group_after_going_online(acfactory, tmpdir, lp):
|
||||
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)
|
||||
dir = tmp_path / "exportdir"
|
||||
dir.mkdir()
|
||||
ac2.export_self_keys(str(dir))
|
||||
ac2_offl.import_self_keys(str(dir))
|
||||
ac2_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
@@ -587,4 +587,67 @@ def test_use_new_verified_group_after_going_online(acfactory, tmpdir, lp):
|
||||
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
|
||||
assert msg_in.text == msg_out.text
|
||||
|
||||
ac2.set_config("bcc_self", "0")
|
||||
|
||||
def test_verified_group_vs_delete_server_after(acfactory, tmp_path, lp):
|
||||
"""Test for the issue #4346:
|
||||
- User is added to a verified group.
|
||||
- First device of the user downloads "member added" from the group.
|
||||
- First device removes "member added" from the server.
|
||||
- Some new messages are sent to the group.
|
||||
- Second device comes online, receives these new messages. The result is a verified group with unverified members.
|
||||
- First device re-gossips Autocrypt keys to the group.
|
||||
- Now the seconds device has all members verified.
|
||||
"""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac2_offl = acfactory.new_online_configuring_account(cloned_from=ac2)
|
||||
for ac in [ac2, ac2_offl]:
|
||||
ac.set_config("bcc_self", "1")
|
||||
ac2.set_config("delete_server_after", "1")
|
||||
ac2.set_config("gossip_period", "0") # Re-gossip in every message
|
||||
acfactory.bring_accounts_online()
|
||||
dir = tmp_path / "exportdir"
|
||||
dir.mkdir()
|
||||
ac2.export_self_keys(str(dir))
|
||||
ac2_offl.import_self_keys(str(dir))
|
||||
ac2_offl.stop_io()
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat1 = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat1.is_protected()
|
||||
qr = chat1.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
# Wait for "Member Me (<addr>) added by <addr>." message.
|
||||
msg_in = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in.is_system_message()
|
||||
|
||||
lp.sec("ac2: waiting for 'member added' to be deleted on the server")
|
||||
ac2._evtracker.get_matching("DC_EVENT_IMAP_MESSAGE_DELETED")
|
||||
|
||||
lp.sec("ac1: sending 'hi' to the group")
|
||||
ac2.set_config("delete_server_after", "0")
|
||||
chat1.send_text("hi")
|
||||
|
||||
lp.sec("ac2_offl: going online, checking the 'hi' message")
|
||||
ac2_offl.start_io()
|
||||
msg_in = ac2_offl._evtracker.wait_next_incoming_message()
|
||||
assert not msg_in.is_system_message()
|
||||
assert msg_in.text.startswith("[Sender of this message is not verified:")
|
||||
ac2_offl_ac1_contact = msg_in.get_sender_contact()
|
||||
assert ac2_offl_ac1_contact.addr == ac1.get_config("addr")
|
||||
assert not ac2_offl_ac1_contact.is_verified()
|
||||
chat2_offl = msg_in.chat
|
||||
assert chat2_offl.is_protected()
|
||||
|
||||
lp.sec("ac2: sending message re-gossiping Autocrypt keys")
|
||||
chat2.send_text("hi2")
|
||||
|
||||
lp.sec("ac2_offl: receiving message")
|
||||
ev = ac2_offl._evtracker.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||
msg_in = ac2_offl.get_message_by_id(ev.data2)
|
||||
assert not msg_in.is_system_message()
|
||||
assert msg_in.text == "hi2"
|
||||
assert msg_in.chat == chat2_offl
|
||||
assert msg_in.get_sender_contact().addr == ac2.get_config("addr")
|
||||
assert ac2_offl_ac1_contact.is_verified()
|
||||
|
||||
@@ -2,18 +2,18 @@ import os
|
||||
import queue
|
||||
import sys
|
||||
import time
|
||||
import base64
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import pytest
|
||||
from imap_tools import AND, U
|
||||
|
||||
from deltachat import const
|
||||
from deltachat.hookspec import account_hookimpl
|
||||
from deltachat.message import Message
|
||||
import deltachat as dc
|
||||
from deltachat import account_hookimpl, Message
|
||||
from deltachat.tracker import ImexTracker
|
||||
|
||||
|
||||
def test_basic_imap_api(acfactory, tmpdir):
|
||||
def test_basic_imap_api(acfactory, tmp_path):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat12 = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
@@ -28,7 +28,7 @@ def test_basic_imap_api(acfactory, tmpdir):
|
||||
imap2.mark_all_read()
|
||||
assert imap2.get_unread_cnt() == 0
|
||||
|
||||
imap2.dump_imap_structures(tmpdir, logfile=sys.stdout)
|
||||
imap2.dump_imap_structures(tmp_path, logfile=sys.stdout)
|
||||
imap2.shutdown()
|
||||
|
||||
|
||||
@@ -36,8 +36,8 @@ def test_basic_imap_api(acfactory, tmpdir):
|
||||
def test_configure_generate_key(acfactory, lp):
|
||||
# A slow test which will generate new keys.
|
||||
acfactory.remove_preconfigured_keys()
|
||||
ac1 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_RSA2048))
|
||||
ac2 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_ED25519))
|
||||
ac1 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_RSA2048))
|
||||
ac2 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_ED25519))
|
||||
acfactory.bring_accounts_online()
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
@@ -72,35 +72,37 @@ def test_configure_canceled(acfactory):
|
||||
pass
|
||||
|
||||
|
||||
def test_configure_unref(tmpdir):
|
||||
def test_configure_unref(tmp_path):
|
||||
"""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)
|
||||
path = tmp_path / "test_configure_unref"
|
||||
path.mkdir()
|
||||
dc_context = lib.dc_context_new(ffi.NULL, str(path / "dc.db").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):
|
||||
def test_export_import_self_keys(acfactory, tmp_path, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
dir = tmpdir.mkdir("exportdir")
|
||||
export_files = ac1.export_self_keys(dir.strpath)
|
||||
dir = tmp_path / "exportdir"
|
||||
dir.mkdir()
|
||||
export_files = ac1.export_self_keys(str(dir))
|
||||
assert len(export_files) == 2
|
||||
for x in export_files:
|
||||
assert x.startswith(dir.strpath)
|
||||
assert x.startswith(str(dir))
|
||||
(key_id,) = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*")
|
||||
ac1._evtracker.consume_events()
|
||||
|
||||
lp.sec("exported keys (private and public)")
|
||||
for name in os.listdir(dir.strpath):
|
||||
lp.indent(dir.strpath + os.sep + name)
|
||||
for name in dir.iterdir():
|
||||
lp.indent(str(dir / name))
|
||||
lp.sec("importing into existing account")
|
||||
ac2.import_self_keys(dir.strpath)
|
||||
ac2.import_self_keys(str(dir))
|
||||
(key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*")
|
||||
assert key_id2 == key_id
|
||||
|
||||
@@ -156,62 +158,65 @@ def test_one_account_send_bcc_setting(acfactory, lp):
|
||||
assert len(list(ac1.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_send_file_twice_unicode_filename_mangling(tmpdir, acfactory, lp):
|
||||
def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
basename = "somedäüta.html.zip"
|
||||
p = os.path.join(tmpdir.strpath, basename)
|
||||
with open(p, "w") as f:
|
||||
f.write("some data")
|
||||
basename = "somedäüta"
|
||||
ext = ".html.zip"
|
||||
p = tmp_path / (basename + ext)
|
||||
p.write_text("some data")
|
||||
|
||||
def send_and_receive_message():
|
||||
lp.sec("ac1: prepare and send attachment + text to ac2")
|
||||
msg1 = Message.new_empty(ac1, "file")
|
||||
msg1.set_text("withfile")
|
||||
msg1.set_file(p)
|
||||
msg1.set_file(str(p))
|
||||
chat.send_msg(msg1)
|
||||
|
||||
lp.sec("ac2: receive message")
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
|
||||
return ac2.get_message_by_id(ev.data2)
|
||||
|
||||
msg = send_and_receive_message()
|
||||
assert msg.text == "withfile"
|
||||
assert open(msg.filename).read() == "some data"
|
||||
assert msg.filename.endswith(basename)
|
||||
msg.filename.index(basename)
|
||||
assert msg.filename.endswith(ext)
|
||||
|
||||
msg2 = send_and_receive_message()
|
||||
assert msg2.text == "withfile"
|
||||
assert open(msg2.filename).read() == "some data"
|
||||
assert msg2.filename.endswith("html.zip")
|
||||
msg2.filename.index(basename)
|
||||
assert msg2.filename.endswith(ext)
|
||||
assert msg.filename != msg2.filename
|
||||
|
||||
|
||||
def test_send_file_html_attachment(tmpdir, acfactory, lp):
|
||||
def test_send_file_html_attachment(tmp_path, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
basename = "test.html"
|
||||
basename = "test"
|
||||
ext = ".html"
|
||||
content = "<html><body>text</body>data"
|
||||
|
||||
p = os.path.join(tmpdir.strpath, basename)
|
||||
with open(p, "w") as f:
|
||||
# write wrong html to see if core tries to parse it
|
||||
# (it shouldn't as it's a file attachment)
|
||||
f.write(content)
|
||||
p = tmp_path / (basename + ext)
|
||||
# write wrong html to see if core tries to parse it
|
||||
# (it shouldn't as it's a file attachment)
|
||||
p.write_text(content)
|
||||
|
||||
lp.sec("ac1: prepare and send attachment + text to ac2")
|
||||
chat.send_file(p, mime_type="text/html")
|
||||
chat.send_file(str(p), mime_type="text/html")
|
||||
|
||||
lp.sec("ac2: receive message")
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
|
||||
msg = ac2.get_message_by_id(ev.data2)
|
||||
|
||||
assert open(msg.filename).read() == content
|
||||
assert msg.filename.endswith(basename)
|
||||
msg.filename.index(basename)
|
||||
assert msg.filename.endswith(ext)
|
||||
|
||||
|
||||
def test_html_message(acfactory, lp):
|
||||
@@ -324,6 +329,59 @@ def test_webxdc_message(acfactory, data, lp):
|
||||
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1
|
||||
|
||||
|
||||
def test_webxdc_huge_update(acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
msg1 = Message.new_empty(ac1, "webxdc")
|
||||
msg1.set_text("message1")
|
||||
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
|
||||
msg1 = chat.send_msg(msg1)
|
||||
assert msg1.is_webxdc()
|
||||
assert msg1.filename
|
||||
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.is_webxdc()
|
||||
|
||||
payload = "A" * 1000
|
||||
assert msg1.send_status_update({"payload": payload}, "some test data")
|
||||
ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
|
||||
update = msg2.get_status_updates()[0]
|
||||
assert update["payload"] == payload
|
||||
|
||||
|
||||
def test_webxdc_download_on_demand(acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
acfactory.introduce_each_other([ac1, ac2])
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
|
||||
msg1 = Message.new_empty(ac1, "webxdc")
|
||||
msg1.set_text("message1")
|
||||
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
|
||||
msg1 = chat.send_msg(msg1)
|
||||
assert msg1.is_webxdc()
|
||||
assert msg1.filename
|
||||
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.is_webxdc()
|
||||
|
||||
lp.sec("ac2 sets download limit")
|
||||
ac2.set_config("download_limit", "100")
|
||||
assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(50000))}, "some test data")
|
||||
ac2_update = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_update.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
|
||||
assert not msg2.get_status_updates()
|
||||
|
||||
ac2_update.download_full()
|
||||
ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
|
||||
assert msg2.get_status_updates()
|
||||
|
||||
# Get a event notifying that the message disappeared from the chat.
|
||||
msgs_changed_event = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
assert msgs_changed_event.data1 == msg2.chat.id
|
||||
assert msgs_changed_event.data2 == 0
|
||||
|
||||
|
||||
def test_mvbox_sentbox_threads(acfactory, lp):
|
||||
lp.sec("ac1: start with mvbox thread")
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=True)
|
||||
@@ -351,7 +409,42 @@ def test_move_works(acfactory):
|
||||
|
||||
# Message is downloaded
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
|
||||
|
||||
|
||||
def test_move_avoids_loop(acfactory):
|
||||
"""Test that the message is only moved once.
|
||||
|
||||
This is to avoid busy loop if moved message reappears in the Inbox
|
||||
or some scanned folder later.
|
||||
For example, this happens on servers that alias `INBOX.DeltaChat` to `DeltaChat` folder,
|
||||
so the message moved to `DeltaChat` appears as a new message in the `INBOX.DeltaChat` folder.
|
||||
We do not want to move this message from `INBOX.DeltaChat` to `DeltaChat` again.
|
||||
"""
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
|
||||
acfactory.bring_accounts_online()
|
||||
ac1_chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
ac1_chat.send_text("Message 1")
|
||||
|
||||
# Message is moved to the DeltaChat folder and downloaded.
|
||||
ac2_msg1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_msg1.text == "Message 1"
|
||||
|
||||
# Move the message to the INBOX again.
|
||||
ac2.direct_imap.select_folder("DeltaChat")
|
||||
ac2.direct_imap.conn.move(["*"], "INBOX")
|
||||
|
||||
ac1_chat.send_text("Message 2")
|
||||
ac2_msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_msg2.text == "Message 2"
|
||||
|
||||
# Check that Message 1 is still in the INBOX folder
|
||||
# and Message 2 is in the DeltaChat folder.
|
||||
ac2.direct_imap.select_folder("INBOX")
|
||||
assert len(ac2.direct_imap.get_all_messages()) == 1
|
||||
ac2.direct_imap.select_folder("DeltaChat")
|
||||
assert len(ac2.direct_imap.get_all_messages()) == 1
|
||||
|
||||
|
||||
def test_move_works_on_self_sent(acfactory):
|
||||
@@ -396,9 +489,12 @@ def test_forward_messages(acfactory, lp):
|
||||
lp.sec("ac2: check new chat has a forwarded message")
|
||||
assert chat3.is_promoted()
|
||||
messages = chat3.get_messages()
|
||||
assert len(messages) == 1
|
||||
msg = messages[-1]
|
||||
assert msg.is_forwarded()
|
||||
ac2.delete_messages(messages)
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSG_DELETED")
|
||||
assert ev.data2 == messages[0].id
|
||||
assert not chat3.get_messages()
|
||||
|
||||
|
||||
@@ -531,8 +627,8 @@ def test_send_and_receive_message_markseen(acfactory, lp):
|
||||
lp.step("1")
|
||||
for _i in range(2):
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
|
||||
assert ev.data1 > const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev.data2 > const.DC_MSG_ID_LAST_SPECIAL
|
||||
assert ev.data1 > dc.const.DC_CHAT_ID_LAST_SPECIAL
|
||||
assert ev.data2 > dc.const.DC_MSG_ID_LAST_SPECIAL
|
||||
lp.step("2")
|
||||
|
||||
# Check that ac1 marks the read receipt as read.
|
||||
@@ -703,7 +799,7 @@ def test_mdn_asymmetric(acfactory, lp):
|
||||
assert len(chat.get_messages()) == 1
|
||||
|
||||
# Wait for the message to be marked as seen on IMAP.
|
||||
ac1._evtracker.get_info_contains("Marked messages 1 in folder DeltaChat as seen.")
|
||||
ac1._evtracker.get_info_contains("Marked messages [0-9]+ in folder DeltaChat as seen.")
|
||||
|
||||
# MDN is received even though MDNs are already disabled
|
||||
assert msg_out.is_out_mdn_received()
|
||||
@@ -1204,7 +1300,7 @@ def test_quote_encrypted(acfactory, lp):
|
||||
assert msg_in.is_encrypted() == quoted_msg.is_encrypted()
|
||||
|
||||
|
||||
def test_quote_attachment(tmpdir, acfactory, lp):
|
||||
def test_quote_attachment(tmp_path, acfactory, lp):
|
||||
"""Test that replies with an attachment and a quote are received correctly."""
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -1219,15 +1315,14 @@ def test_quote_attachment(tmpdir, acfactory, lp):
|
||||
assert received_message.text == "hi"
|
||||
|
||||
basename = "attachment.txt"
|
||||
p = os.path.join(tmpdir.strpath, basename)
|
||||
with open(p, "w") as f:
|
||||
f.write("data to send")
|
||||
p = tmp_path / basename
|
||||
p.write_text("data to send")
|
||||
|
||||
lp.sec("ac2 sends a reply to ac1")
|
||||
chat2 = received_message.create_chat()
|
||||
reply = Message.new_empty(ac2, "file")
|
||||
reply.set_text("message reply")
|
||||
reply.set_file(p)
|
||||
reply.set_file(str(p))
|
||||
reply.quote = received_message
|
||||
chat2.send_msg(reply)
|
||||
|
||||
@@ -1334,7 +1429,7 @@ def test_send_and_receive_image(acfactory, lp, data):
|
||||
assert m == msg_in
|
||||
|
||||
|
||||
def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir):
|
||||
def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
|
||||
"""See https://github.com/deltachat/deltachat-core-rust/issues/3688 "Partially downloaded
|
||||
messages are received out of order".
|
||||
|
||||
@@ -1369,10 +1464,9 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir):
|
||||
lp.sec("sending small+large messages from ac1 to ac2")
|
||||
msgs = []
|
||||
msgs.append(chat.send_text("hi"))
|
||||
path = tmpdir.join("large")
|
||||
with open(path, "wb") as fout:
|
||||
fout.write(os.urandom(download_limit + 1))
|
||||
msgs.append(chat.send_file(path.strpath))
|
||||
path = tmp_path / "large"
|
||||
path.write_bytes(os.urandom(download_limit + 1))
|
||||
msgs.append(chat.send_file(str(path)))
|
||||
|
||||
lp.sec("sending a reaction to the large message from ac1 to ac2")
|
||||
react_str = "\N{THUMBS UP SIGN}"
|
||||
@@ -1385,7 +1479,7 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmpdir):
|
||||
lp.sec("wait for ac2 to receive a reaction")
|
||||
msg2 = ac2._evtracker.wait_next_reactions_changed()
|
||||
assert msg2.get_sender_contact().addr == ac1_addr
|
||||
assert msg2.download_state == const.DC_DOWNLOAD_AVAILABLE
|
||||
assert msg2.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
|
||||
assert reactions_queue.get() == msg2
|
||||
reactions = msg2.get_reactions()
|
||||
contacts = reactions.get_contacts()
|
||||
@@ -1431,7 +1525,7 @@ def test_reactions_for_a_reordering_move(acfactory, lp):
|
||||
assert reactions.get_by_contact(contacts[0]) == react_str
|
||||
|
||||
|
||||
def test_import_export_online_all(acfactory, tmpdir, data, lp):
|
||||
def test_import_export_online_all(acfactory, tmp_path, data, lp):
|
||||
(ac1,) = acfactory.get_online_accounts(1)
|
||||
|
||||
lp.sec("create some chat content")
|
||||
@@ -1443,10 +1537,10 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
|
||||
chat1.send_image(original_image_path)
|
||||
|
||||
# Add another 100KB file that ensures that the progress is smooth enough
|
||||
path = tmpdir.join("attachment.txt")
|
||||
with open(path, "w") as file:
|
||||
path = tmp_path / "attachment.txt"
|
||||
with path.open("w") as file:
|
||||
file.truncate(100000)
|
||||
chat1.send_file(path.strpath)
|
||||
chat1.send_file(str(path))
|
||||
|
||||
def assert_account_is_proper(ac):
|
||||
contacts = ac.get_contacts(query="some1")
|
||||
@@ -1464,12 +1558,13 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
|
||||
|
||||
assert_account_is_proper(ac1)
|
||||
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
|
||||
lp.sec(f"export all to {backupdir}")
|
||||
with ac1.temp_plugin(ImexTracker()) as imex_tracker:
|
||||
ac1.stop_io()
|
||||
ac1.imex(backupdir.strpath, const.DC_IMEX_EXPORT_BACKUP)
|
||||
ac1.imex(str(backupdir), dc.const.DC_IMEX_EXPORT_BACKUP)
|
||||
|
||||
# check progress events for export
|
||||
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
|
||||
@@ -1487,7 +1582,7 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
|
||||
lp.sec("get latest backup file")
|
||||
path2 = ac2.get_latest_backupfile(backupdir.strpath)
|
||||
path2 = ac2.get_latest_backupfile(str(backupdir))
|
||||
assert path2 == path
|
||||
|
||||
lp.sec("import backup and check it's proper")
|
||||
@@ -1505,10 +1600,10 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
|
||||
|
||||
lp.sec(f"Second-time export all to {backupdir}")
|
||||
ac1.stop_io()
|
||||
path2 = ac1.export_all(backupdir.strpath)
|
||||
path2 = ac1.export_all(str(backupdir))
|
||||
assert os.path.exists(path2)
|
||||
assert path2 != path
|
||||
assert ac2.get_latest_backupfile(backupdir.strpath) == path2
|
||||
assert ac2.get_latest_backupfile(str(backupdir)) == path2
|
||||
|
||||
|
||||
def test_ac_setup_message(acfactory, lp):
|
||||
@@ -1584,6 +1679,67 @@ def test_qr_join_chat(acfactory, lp):
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
|
||||
def test_qr_new_group_unblocked(acfactory, lp):
|
||||
"""Regression test for a bug intoduced in core v1.113.0.
|
||||
ac2 scans a verified group QR code created by ac1.
|
||||
This results in creation of a blocked 1:1 chat with ac1 on ac2,
|
||||
but ac1 contact is not blocked on ac2.
|
||||
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
|
||||
ac2 should receive a message and create a contact request for the group.
|
||||
Due to a bug previously ac2 created a blocked group.
|
||||
"""
|
||||
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1_chat = ac1.create_group_chat("Group for joining", verified=True)
|
||||
qr = ac1_chat.get_join_qr()
|
||||
ac2.qr_join_chat(qr)
|
||||
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
ac1_new_chat = ac1.create_group_chat("Another group")
|
||||
ac1_new_chat.add_contact(ac2)
|
||||
# Receive "Member added" message.
|
||||
ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
ac1_new_chat.send_text("Hello!")
|
||||
ac2_msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac2_msg.text == "Hello!"
|
||||
assert ac2_msg.chat.is_contact_request()
|
||||
|
||||
|
||||
def test_qr_email_capitalization(acfactory, lp):
|
||||
"""Regression test for a bug
|
||||
that resulted in failure to propagate verification via gossip in a verified group
|
||||
when the database already contained the contact with a different email address capitalization.
|
||||
"""
|
||||
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
# ac1 adds ac2 as a contact with an email address in uppercase.
|
||||
ac2_addr_uppercase = ac2.get_config("addr").upper()
|
||||
lp.sec(f"ac1 creates a contact for ac2 ({ac2_addr_uppercase})")
|
||||
ac1.create_contact(ac2_addr_uppercase)
|
||||
|
||||
lp.sec("ac3 creates a verified group with a QR code")
|
||||
chat = ac3.create_group_chat("hello", verified=True)
|
||||
qr = chat.get_join_qr()
|
||||
|
||||
lp.sec("ac1 joins a verified group via a QR code")
|
||||
ac1_chat = ac1.qr_join_chat(qr)
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "Member Me ({}) added by {}.".format(ac1.get_config("addr"), ac3.get_config("addr"))
|
||||
assert len(ac1_chat.get_contacts()) == 2
|
||||
|
||||
lp.sec("ac2 joins a verified group via a QR code")
|
||||
ac2.qr_join_chat(qr)
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
# ac1 should see both ac3 and ac2 as verified.
|
||||
assert len(ac1_chat.get_contacts()) == 3
|
||||
for contact in ac1_chat.get_contacts():
|
||||
assert contact.is_verified()
|
||||
|
||||
|
||||
def test_set_get_contact_avatar(acfactory, data, lp):
|
||||
lp.sec("configuring ac1 and ac2")
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
@@ -1768,13 +1924,15 @@ def test_set_get_group_image(acfactory, data, lp):
|
||||
lp.sec("ac1: add ac2 to promoted group chat")
|
||||
chat.add_contact(ac2) # sends one message
|
||||
|
||||
lp.sec("ac2: wait for receiving member added message from ac1")
|
||||
msg1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg1.is_system_message() # Member added
|
||||
|
||||
lp.sec("ac1: send a first message to ac2")
|
||||
chat.send_text("hi") # sends another message
|
||||
assert chat.is_promoted()
|
||||
|
||||
lp.sec("ac2: wait for receiving message from ac1")
|
||||
msg1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg1.is_system_message() # Member added
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "hi"
|
||||
assert msg1.chat.id == msg2.chat.id
|
||||
@@ -1800,15 +1958,15 @@ def test_connectivity(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
ac1.set_config("scan_all_folders_debounce_secs", "0")
|
||||
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTED)
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
lp.sec("Test stop_io() and start_io()")
|
||||
ac1.stop_io()
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
|
||||
ac1.start_io()
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
|
||||
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTING, const.DC_CONNECTIVITY_CONNECTED)
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
|
||||
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTING, dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
lp.sec(
|
||||
"Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, "
|
||||
@@ -1829,8 +1987,8 @@ def test_connectivity(acfactory, lp):
|
||||
|
||||
ac2.create_chat(ac1).send_text("Hi 2")
|
||||
|
||||
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTED, const.DC_CONNECTIVITY_WORKING)
|
||||
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_WORKING, const.DC_CONNECTIVITY_CONNECTED)
|
||||
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTED, dc.const.DC_CONNECTIVITY_WORKING)
|
||||
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED)
|
||||
|
||||
msgs = ac1.create_chat(ac2).get_messages()
|
||||
assert len(msgs) == 2
|
||||
@@ -1840,7 +1998,7 @@ def test_connectivity(acfactory, lp):
|
||||
|
||||
ac1.maybe_network()
|
||||
while 1:
|
||||
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
|
||||
assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
|
||||
if ac1.all_work_done():
|
||||
break
|
||||
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
@@ -1855,7 +2013,7 @@ def test_connectivity(acfactory, lp):
|
||||
ac1.maybe_network()
|
||||
|
||||
while 1:
|
||||
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
|
||||
assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
|
||||
if ac1.all_work_done():
|
||||
break
|
||||
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
||||
@@ -1864,10 +2022,10 @@ def test_connectivity(acfactory, lp):
|
||||
|
||||
ac1.set_config("configured_mail_pw", "abc")
|
||||
ac1.stop_io()
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
ac1.start_io()
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
|
||||
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
|
||||
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
|
||||
|
||||
|
||||
def test_fetch_deleted_msg(acfactory, lp):
|
||||
@@ -2350,9 +2508,9 @@ def test_archived_muted_chat(acfactory, lp):
|
||||
lp.sec("wait for ac2 to receive DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK")
|
||||
while 1:
|
||||
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
if ev.data1 == const.DC_CHAT_ID_ARCHIVED_LINK:
|
||||
if ev.data1 == dc.const.DC_CHAT_ID_ARCHIVED_LINK:
|
||||
assert ev.data2 == 0
|
||||
archive = ac2.get_chat_by_id(const.DC_CHAT_ID_ARCHIVED_LINK)
|
||||
archive = ac2.get_chat_by_id(dc.const.DC_CHAT_ID_ARCHIVED_LINK)
|
||||
assert archive.count_fresh_messages() == 1
|
||||
assert chat2.count_fresh_messages() == 1
|
||||
break
|
||||
|
||||
@@ -30,28 +30,28 @@ def wait_msgs_changed(account, msgs_list):
|
||||
|
||||
|
||||
class TestOnlineInCreation:
|
||||
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
|
||||
def test_increation_not_blobdir(self, tmp_path, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
lp.sec("Creating in-creation file outside of blobdir")
|
||||
assert tmpdir.strpath != ac1.get_blobdir()
|
||||
src = tmpdir.join("file.txt").ensure(file=1)
|
||||
assert str(tmp_path) != ac1.get_blobdir()
|
||||
src = tmp_path / "file.txt"
|
||||
src.touch()
|
||||
with pytest.raises(Exception):
|
||||
chat.prepare_message_file(src.strpath)
|
||||
chat.prepare_message_file(str(src))
|
||||
|
||||
def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp):
|
||||
def test_no_increation_copies_to_blobdir(self, tmp_path, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
lp.sec("Creating file outside of blobdir")
|
||||
assert tmpdir.strpath != ac1.get_blobdir()
|
||||
src = tmpdir.join("file.txt")
|
||||
src.write("hello there\n")
|
||||
chat.send_file(src.strpath)
|
||||
|
||||
blob_src = os.path.join(ac1.get_blobdir(), "file.txt")
|
||||
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
|
||||
assert str(tmp_path) != ac1.get_blobdir()
|
||||
src = tmp_path / "file.txt"
|
||||
src.write_text("hello there\n")
|
||||
msg = chat.send_file(str(src))
|
||||
assert msg.filename.startswith(os.path.join(ac1.get_blobdir(), "file"))
|
||||
assert msg.filename.endswith(".txt")
|
||||
|
||||
def test_forward_increation(self, acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
@@ -4,12 +4,11 @@ from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from deltachat import Account, const
|
||||
import deltachat as dc
|
||||
from deltachat.capi import ffi, lib
|
||||
from deltachat.cutil import iter_array
|
||||
from deltachat.hookspec import account_hookimpl
|
||||
from deltachat.message import Message
|
||||
from deltachat.tracker import ImexFailed
|
||||
from deltachat import Account, account_hookimpl, Message
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
@@ -52,26 +51,25 @@ def test_parse_system_add_remove(msgtext, res):
|
||||
|
||||
|
||||
class TestOfflineAccountBasic:
|
||||
def test_wrong_db(self, tmpdir):
|
||||
p = tmpdir.join("hello.db")
|
||||
p.write("123")
|
||||
account = Account(p.strpath)
|
||||
def test_wrong_db(self, tmp_path):
|
||||
p = tmp_path / "hello.db"
|
||||
p.write_text("123")
|
||||
account = Account(str(p))
|
||||
assert not account.is_open()
|
||||
|
||||
def test_os_name(self, tmpdir):
|
||||
p = tmpdir.join("hello.db")
|
||||
def test_os_name(self, tmp_path):
|
||||
p = tmp_path / "hello.db"
|
||||
# we can't easily test if os_name is used in X-Mailer
|
||||
# outgoing messages without a full Online test
|
||||
# but we at least check Account accepts the arg
|
||||
ac1 = Account(p.strpath, os_name="solarpunk")
|
||||
ac1 = Account(str(p), os_name="solarpunk")
|
||||
ac1.get_info()
|
||||
|
||||
def test_preconfigure_keypair(self, acfactory, data):
|
||||
ac = acfactory.get_unconfigured_account()
|
||||
alice_public = data.read_path("key/alice-public.asc")
|
||||
alice_secret = data.read_path("key/alice-secret.asc")
|
||||
assert alice_public and alice_secret
|
||||
ac._preconfigure_keypair("alice@example.org", alice_public, alice_secret)
|
||||
assert alice_secret
|
||||
ac._preconfigure_keypair("alice@example.org", alice_secret)
|
||||
|
||||
def test_getinfo(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
@@ -299,13 +297,13 @@ class TestOfflineChat:
|
||||
assert not d["draft"] if chat.get_draft() is None else chat.get_draft()
|
||||
|
||||
def test_group_chat_creation_with_translation(self, ac1):
|
||||
ac1.set_stock_translation(const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s")
|
||||
ac1.set_stock_translation(dc.const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s")
|
||||
ac1._evtracker.consume_events()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s")
|
||||
ac1.set_stock_translation(dc.const.DC_STR_FILE, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
|
||||
ac1.set_stock_translation(dc.const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(500, "xyz %1$s")
|
||||
@@ -481,6 +479,19 @@ class TestOfflineChat:
|
||||
contact2 = ac1.create_contact("display1 <x@example.org>", "real")
|
||||
assert contact2.name == "real"
|
||||
|
||||
def test_send_lots_of_offline_msgs(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac1.set_config("configured_mail_server", "example.org")
|
||||
ac1.set_config("configured_mail_user", "example.org")
|
||||
ac1.set_config("configured_mail_pw", "example.org")
|
||||
ac1.set_config("configured_send_server", "example.org")
|
||||
ac1.set_config("configured_send_user", "example.org")
|
||||
ac1.set_config("configured_send_pw", "example.org")
|
||||
ac1.start_io()
|
||||
chat = ac1.create_contact("some1@example.org", name="some1").create_chat()
|
||||
for i in range(50):
|
||||
chat.send_text("hello")
|
||||
|
||||
def test_create_chat_simple(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
@@ -496,22 +507,22 @@ class TestOfflineChat:
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
|
||||
def test_import_export_on_unencrypted_acct(self, acfactory, tmpdir):
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
def test_import_export_on_unencrypted_acct(self, acfactory, tmp_path):
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
bin = tmpdir.join("some.bin")
|
||||
with bin.open("w") as f:
|
||||
f.write("\00123" * 10000)
|
||||
msg = chat.send_file(bin.strpath)
|
||||
bin = tmp_path / "some.bin"
|
||||
bin.write_bytes(b"\00123" * 10000)
|
||||
msg = chat.send_file(str(bin))
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
assert not backupdir.listdir()
|
||||
assert not list(backupdir.iterdir())
|
||||
ac1.stop_io()
|
||||
path = ac1.export_all(backupdir.strpath)
|
||||
path = ac1.export_all(str(backupdir))
|
||||
assert os.path.exists(path)
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
ac2.import_all(path)
|
||||
@@ -525,27 +536,27 @@ class TestOfflineChat:
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_import_export_on_encrypted_acct(self, acfactory, tmpdir):
|
||||
def test_import_export_on_encrypted_acct(self, acfactory, tmp_path):
|
||||
passphrase1 = "passphrase1"
|
||||
passphrase2 = "passphrase2"
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
ac1 = acfactory.get_pseudo_configured_account(passphrase=passphrase1)
|
||||
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
bin = tmpdir.join("some.bin")
|
||||
with bin.open("w") as f:
|
||||
f.write("\00123" * 10000)
|
||||
msg = chat.send_file(bin.strpath)
|
||||
bin = tmp_path / "some.bin"
|
||||
bin.write_bytes(b"\00123" * 10000)
|
||||
msg = chat.send_file(str(bin))
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
|
||||
assert not backupdir.listdir()
|
||||
assert not list(backupdir.iterdir())
|
||||
ac1.stop_io()
|
||||
|
||||
path = ac1.export_all(backupdir.strpath)
|
||||
path = ac1.export_all(str(backupdir))
|
||||
assert os.path.exists(path)
|
||||
|
||||
ac2 = acfactory.get_unconfigured_account(closed=True)
|
||||
@@ -580,27 +591,27 @@ class TestOfflineChat:
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_import_export_with_passphrase(self, acfactory, tmpdir):
|
||||
def test_import_export_with_passphrase(self, acfactory, tmp_path):
|
||||
passphrase = "test_passphrase"
|
||||
wrong_passphrase = "wrong_passprase"
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
bin = tmpdir.join("some.bin")
|
||||
with bin.open("w") as f:
|
||||
f.write("\00123" * 10000)
|
||||
msg = chat.send_file(bin.strpath)
|
||||
bin = tmp_path / "some.bin"
|
||||
bin.write_bytes(b"\00123" * 10000)
|
||||
msg = chat.send_file(str(bin))
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
|
||||
assert not backupdir.listdir()
|
||||
assert not list(backupdir.iterdir())
|
||||
ac1.stop_io()
|
||||
|
||||
path = ac1.export_all(backupdir.strpath, passphrase)
|
||||
path = ac1.export_all(str(backupdir), passphrase)
|
||||
assert os.path.exists(path)
|
||||
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
@@ -619,7 +630,7 @@ class TestOfflineChat:
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmpdir):
|
||||
def test_import_encrypted_bak_into_encrypted_acct(self, acfactory, tmp_path):
|
||||
"""
|
||||
Test that account passphrase isn't lost if backup failed to be imported.
|
||||
See https://github.com/deltachat/deltachat-core-rust/issues/3379
|
||||
@@ -627,24 +638,24 @@ class TestOfflineChat:
|
||||
acct_passphrase = "passphrase1"
|
||||
bak_passphrase = "passphrase2"
|
||||
wrong_passphrase = "wrong_passprase"
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
backupdir = tmp_path / "backup"
|
||||
backupdir.mkdir()
|
||||
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
bin = tmpdir.join("some.bin")
|
||||
with bin.open("w") as f:
|
||||
f.write("\00123" * 10000)
|
||||
msg = chat.send_file(bin.strpath)
|
||||
bin = tmp_path / "some.bin"
|
||||
bin.write_bytes(b"\00123" * 10000)
|
||||
msg = chat.send_file(str(bin))
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
|
||||
assert not backupdir.listdir()
|
||||
assert not list(backupdir.iterdir())
|
||||
ac1.stop_io()
|
||||
|
||||
path = ac1.export_all(backupdir.strpath, bak_passphrase)
|
||||
path = ac1.export_all(str(backupdir), bak_passphrase)
|
||||
assert os.path.exists(path)
|
||||
|
||||
ac2 = acfactory.get_unconfigured_account(closed=True)
|
||||
@@ -805,7 +816,7 @@ class TestOfflineChat:
|
||||
|
||||
lp.sec("check message count of only system messages (without daymarkers)")
|
||||
dc_array = ffi.gc(
|
||||
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, const.DC_GCM_INFO_ONLY, 0),
|
||||
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, dc.const.DC_GCM_INFO_ONLY, 0),
|
||||
lib.dc_array_unref,
|
||||
)
|
||||
assert len(list(iter_array(dc_array, lambda x: x))) == 2
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import os
|
||||
import json
|
||||
from queue import Queue
|
||||
|
||||
from deltachat import capi, const, cutil, register_global_plugin
|
||||
import deltachat as dc
|
||||
from deltachat import capi, cutil, register_global_plugin
|
||||
from deltachat.capi import ffi, lib
|
||||
from deltachat.hookspec import global_hookimpl
|
||||
from deltachat.testplugin import (
|
||||
@@ -65,16 +66,17 @@ class TestACSetup:
|
||||
assert pc._account2state[ac1] == pc.IDLEREADY
|
||||
assert pc._account2state[ac2] == pc.IDLEREADY
|
||||
|
||||
def test_store_and_retrieve_configured_account_cache(self, acfactory, tmpdir):
|
||||
def test_store_and_retrieve_configured_account_cache(self, acfactory, tmp_path):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
holder = acfactory._acsetup.testprocess
|
||||
assert holder.cache_maybe_store_configured_db_files(ac1)
|
||||
assert not holder.cache_maybe_store_configured_db_files(ac1)
|
||||
acdir = tmpdir.mkdir("newaccount")
|
||||
acdir = tmp_path / "newaccount"
|
||||
acdir.mkdir()
|
||||
addr = ac1.get_config("addr")
|
||||
target_db_path = acdir.join("db").strpath
|
||||
assert holder.cache_maybe_retrieve_configured_db_files(addr, target_db_path)
|
||||
assert len(os.listdir(acdir)) >= 2
|
||||
target_db_path = acdir / "db"
|
||||
assert holder.cache_maybe_retrieve_configured_db_files(addr, str(target_db_path))
|
||||
assert sum(1 for _ in acdir.iterdir()) >= 2
|
||||
|
||||
|
||||
def test_liveconfig_caching(acfactory, monkeypatch):
|
||||
@@ -112,40 +114,40 @@ def test_dc_close_events(acfactory):
|
||||
shutdowns.get(timeout=2)
|
||||
|
||||
|
||||
def test_wrong_db(tmpdir):
|
||||
p = tmpdir.join("hello.db")
|
||||
def test_wrong_db(tmp_path):
|
||||
p = tmp_path / "hello.db"
|
||||
# write an invalid database file
|
||||
p.write("x123" * 10)
|
||||
p.write_bytes(b"x123" * 10)
|
||||
|
||||
context = lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
|
||||
context = lib.dc_context_new(ffi.NULL, str(p).encode("ascii"), ffi.NULL)
|
||||
assert not lib.dc_context_is_open(context)
|
||||
|
||||
|
||||
def test_empty_blobdir(tmpdir):
|
||||
db_fname = tmpdir.join("hello.db")
|
||||
def test_empty_blobdir(tmp_path):
|
||||
db_fname = tmp_path / "hello.db"
|
||||
# Apparently some client code expects this to be the same as passing NULL.
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), b""),
|
||||
lib.dc_context_new(ffi.NULL, str(db_fname).encode("ascii"), b""),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
assert ctx != ffi.NULL
|
||||
|
||||
|
||||
def test_event_defines():
|
||||
assert const.DC_EVENT_INFO == 100
|
||||
assert const.DC_CONTACT_ID_SELF
|
||||
assert dc.const.DC_EVENT_INFO == 100
|
||||
assert dc.const.DC_CONTACT_ID_SELF
|
||||
|
||||
|
||||
def test_sig():
|
||||
sig = capi.lib.dc_event_has_string_data
|
||||
assert not sig(const.DC_EVENT_MSGS_CHANGED)
|
||||
assert sig(const.DC_EVENT_INFO)
|
||||
assert sig(const.DC_EVENT_WARNING)
|
||||
assert sig(const.DC_EVENT_ERROR)
|
||||
assert sig(const.DC_EVENT_SMTP_CONNECTED)
|
||||
assert sig(const.DC_EVENT_IMAP_CONNECTED)
|
||||
assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT)
|
||||
assert sig(const.DC_EVENT_IMEX_FILE_WRITTEN)
|
||||
assert not sig(dc.const.DC_EVENT_MSGS_CHANGED)
|
||||
assert sig(dc.const.DC_EVENT_INFO)
|
||||
assert sig(dc.const.DC_EVENT_WARNING)
|
||||
assert sig(dc.const.DC_EVENT_ERROR)
|
||||
assert sig(dc.const.DC_EVENT_SMTP_CONNECTED)
|
||||
assert sig(dc.const.DC_EVENT_IMAP_CONNECTED)
|
||||
assert sig(dc.const.DC_EVENT_SMTP_MESSAGE_SENT)
|
||||
assert sig(dc.const.DC_EVENT_IMEX_FILE_WRITTEN)
|
||||
|
||||
|
||||
def test_markseen_invalid_message_ids(acfactory):
|
||||
@@ -174,10 +176,10 @@ def test_provider_info_none():
|
||||
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL
|
||||
|
||||
|
||||
def test_get_info_open(tmpdir):
|
||||
db_fname = tmpdir.join("test.db")
|
||||
def test_get_info_open(tmp_path):
|
||||
db_fname = tmp_path / "test.db"
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), ffi.NULL),
|
||||
lib.dc_context_new(ffi.NULL, str(db_fname).encode("ascii"), ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
|
||||
@@ -218,17 +220,33 @@ def test_logged_ac_process_ffi_failure(acfactory):
|
||||
assert "Traceback" in res
|
||||
|
||||
|
||||
def test_jsonrpc_blocking_call(tmpdir):
|
||||
accounts_fname = tmpdir.join("accounts")
|
||||
def test_jsonrpc_blocking_call(tmp_path):
|
||||
accounts_fname = tmp_path / "accounts"
|
||||
accounts = ffi.gc(
|
||||
lib.dc_accounts_new(ffi.NULL, accounts_fname.strpath.encode("ascii")),
|
||||
lib.dc_accounts_new(ffi.NULL, str(accounts_fname).encode("ascii")),
|
||||
lib.dc_accounts_unref,
|
||||
)
|
||||
jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref)
|
||||
res = from_optional_dc_charpointer(
|
||||
lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice@example.org"]'),
|
||||
res = json.loads(
|
||||
from_optional_dc_charpointer(
|
||||
lib.dc_jsonrpc_blocking_call(
|
||||
jsonrpc,
|
||||
json.dumps(
|
||||
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice@example.org"], "id": "123"},
|
||||
).encode("utf-8"),
|
||||
),
|
||||
),
|
||||
)
|
||||
assert res == "true"
|
||||
assert res == {"jsonrpc": "2.0", "id": "123", "result": True}
|
||||
|
||||
res = from_optional_dc_charpointer(lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice"]'))
|
||||
assert res == "false"
|
||||
res = json.loads(
|
||||
from_optional_dc_charpointer(
|
||||
lib.dc_jsonrpc_blocking_call(
|
||||
jsonrpc,
|
||||
json.dumps(
|
||||
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice"], "id": "456"},
|
||||
).encode("utf-8"),
|
||||
),
|
||||
),
|
||||
)
|
||||
assert res == {"jsonrpc": "2.0", "id": "456", "result": False}
|
||||
|
||||
@@ -25,6 +25,9 @@ deps =
|
||||
pytest-xdist
|
||||
pdbpp
|
||||
requests
|
||||
# urllib3 2.0 does not work in manylinux2014 containers.
|
||||
# https://github.com/deltachat/deltachat-core-rust/issues/4788
|
||||
urllib3<2
|
||||
|
||||
[testenv:.pkg]
|
||||
passenv =
|
||||
|
||||
@@ -1 +1 @@
|
||||
2023-05-12
|
||||
2023-10-24
|
||||
@@ -1,3 +0,0 @@
|
||||
pre-release-commit-message = "chore({{crate_name}}): release {{version}}"
|
||||
pro-release-commit-message = "chore({{crate_name}}): starting development cycle for {{next_version}}"
|
||||
no-dev-version = true
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user