mirror of
https://github.com/chatmail/core.git
synced 2026-04-03 05:52:10 +03:00
Compare commits
448 Commits
blob-api
...
fix_node_p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
008fd1cf5f | ||
|
|
33b10fa719 | ||
|
|
6d189dab98 | ||
|
|
380d7e66b5 | ||
|
|
eb80fa4eb0 | ||
|
|
88b470e8e5 | ||
|
|
99f8785475 | ||
|
|
47db1349a9 | ||
|
|
1f8b04bd42 | ||
|
|
d790a5fba9 | ||
|
|
2fc0a0964b | ||
|
|
715664273b | ||
|
|
a9f707c398 | ||
|
|
8168bcece5 | ||
|
|
c9beaa2aa1 | ||
|
|
b238c7e743 | ||
|
|
e9511ebfc3 | ||
|
|
a786a1427d | ||
|
|
961612370d | ||
|
|
bd5b9573f6 | ||
|
|
e603a10ab4 | ||
|
|
984346d682 | ||
|
|
f4cecb61bc | ||
|
|
cffa101419 | ||
|
|
1d7d201b5b | ||
|
|
c0e4e12138 | ||
|
|
5a85255be9 | ||
|
|
60d3960f3a | ||
|
|
7bcb03f1ec | ||
|
|
01ef053a11 | ||
|
|
8988c775fe | ||
|
|
c8bfa98b6b | ||
|
|
34053c7608 | ||
|
|
785667ec07 | ||
|
|
9a0a3c4b00 | ||
|
|
e56c261c73 | ||
|
|
32a9db6922 | ||
|
|
77498c17a5 | ||
|
|
54c07f89f2 | ||
|
|
cd4d265055 | ||
|
|
81ee69010d | ||
|
|
138d5b7a02 | ||
|
|
95a54a43ff | ||
|
|
ca59cbc898 | ||
|
|
21f72ad845 | ||
|
|
40fd98d580 | ||
|
|
0422d751d8 | ||
|
|
e44f68db83 | ||
|
|
88cbf42c39 | ||
|
|
45157f79a3 | ||
|
|
f5157392b6 | ||
|
|
f631ec3a7c | ||
|
|
a9b2750ec9 | ||
|
|
7fbdb42695 | ||
|
|
026dd4480e | ||
|
|
b373e7acba | ||
|
|
d2ca54c167 | ||
|
|
e070284a09 | ||
|
|
e4e02ea3c0 | ||
|
|
a947980eb6 | ||
|
|
f11c3dd3e3 | ||
|
|
394067be63 | ||
|
|
2494613583 | ||
|
|
77c60e7450 | ||
|
|
04dd2d93d0 | ||
|
|
5e5710ecce | ||
|
|
c1b33a66c4 | ||
|
|
0e3165d724 | ||
|
|
3e12eab0b5 | ||
|
|
87365e4a43 | ||
|
|
3e16a47ff2 | ||
|
|
874054148e | ||
|
|
5e39a13bf6 | ||
|
|
720c1b5eca | ||
|
|
c7c1a04c6a | ||
|
|
fd5b224ba0 | ||
|
|
0b80ade3ae | ||
|
|
e1c3e95307 | ||
|
|
904e8966c0 | ||
|
|
3a10f0155f | ||
|
|
4c9cc4f3d4 | ||
|
|
48f2c4e14b | ||
|
|
f41df327a9 | ||
|
|
3f9e3038b7 | ||
|
|
c75c95afa9 | ||
|
|
d4e0009b89 | ||
|
|
b97b374487 | ||
|
|
e27345e489 | ||
|
|
032e644b2b | ||
|
|
b7ac81701a | ||
|
|
d59aa35b2f | ||
|
|
4c7c4e2a81 | ||
|
|
521fa58b75 | ||
|
|
a2e5c60683 | ||
|
|
5ef152fd84 | ||
|
|
e2ba338923 | ||
|
|
aae4f0bb7b | ||
|
|
43e3f8f08b | ||
|
|
9cc2fd555f | ||
|
|
c10dc7b25b | ||
|
|
9e1770316a | ||
|
|
0e595c9801 | ||
|
|
6ae9e43183 | ||
|
|
18126b42cb | ||
|
|
2b233fd810 | ||
|
|
a4f5d2b9b2 | ||
|
|
d29c09caf3 | ||
|
|
bc809986e7 | ||
|
|
5ee2f3696d | ||
|
|
df5eb546e7 | ||
|
|
684351c753 | ||
|
|
a8342e37b9 | ||
|
|
3b6fc9959f | ||
|
|
3ffc985968 | ||
|
|
369609b26c | ||
|
|
d033dcf395 | ||
|
|
5ef2a85c10 | ||
|
|
8c0bc9080c | ||
|
|
6bf2c5415f | ||
|
|
2f31033a88 | ||
|
|
ceaed0f552 | ||
|
|
e9963ecc0d | ||
|
|
9ef0b43c36 | ||
|
|
801d636eb5 | ||
|
|
dfbfd4fe74 | ||
|
|
7455989729 | ||
|
|
8b2b9e1093 | ||
|
|
a8cf05ea5d | ||
|
|
969508ae36 | ||
|
|
f581ecc805 | ||
|
|
a63464765c | ||
|
|
e9fe8ce118 | ||
|
|
1fa892c239 | ||
|
|
1afbbbc737 | ||
|
|
66c4de2607 | ||
|
|
213e67dea2 | ||
|
|
86c884fe1e | ||
|
|
a4d5c8cd2f | ||
|
|
330665afbe | ||
|
|
be1d87c3c3 | ||
|
|
14ab3c8651 | ||
|
|
9c04ed483e | ||
|
|
a8a5e184ab | ||
|
|
c571595980 | ||
|
|
80f2ccc9ed | ||
|
|
b2e9e57859 | ||
|
|
3c75b36148 | ||
|
|
bcd8e330cb | ||
|
|
f75f8ad76d | ||
|
|
574b78cf31 | ||
|
|
92f0e8472b | ||
|
|
69bd5d2ab0 | ||
|
|
6eaf04107d | ||
|
|
d80fdb20c7 | ||
|
|
f618c87ee5 | ||
|
|
0721c22073 | ||
|
|
aba066b4d1 | ||
|
|
2562c726e6 | ||
|
|
6e3ec71c10 | ||
|
|
2932c1ed35 | ||
|
|
149b31a960 | ||
|
|
f1d09e4127 | ||
|
|
10bdbc95cd | ||
|
|
26c38070ec | ||
|
|
494a7f1db9 | ||
|
|
bba721654b | ||
|
|
36a17b0592 | ||
|
|
b7294d46cf | ||
|
|
d8977b5046 | ||
|
|
963bb7f7cf | ||
|
|
7d3a08599e | ||
|
|
345a4bc504 | ||
|
|
7bfdf2e2f5 | ||
|
|
d9ac5d88e9 | ||
|
|
2cf11bb2ea | ||
|
|
e29d008914 | ||
|
|
de91063fbe | ||
|
|
3d95272707 | ||
|
|
3c20d0902e | ||
|
|
f2c1e5c6e5 | ||
|
|
feb354725a | ||
|
|
35c0434dc7 | ||
|
|
918ee47c79 | ||
|
|
a8ab7c9c04 | ||
|
|
332892b468 | ||
|
|
6392300311 | ||
|
|
16d201faca | ||
|
|
ea0cf67f98 | ||
|
|
612132b7c8 | ||
|
|
26470c6047 | ||
|
|
93d3522f67 | ||
|
|
c6d901d799 | ||
|
|
4880f9ff32 | ||
|
|
aaa42a3412 | ||
|
|
3e5e852e20 | ||
|
|
0a3f44bd73 | ||
|
|
d4fed5f5f7 | ||
|
|
dce7b90fc2 | ||
|
|
4f94bdff3f | ||
|
|
ce1f2a6fd4 | ||
|
|
da292bb9b2 | ||
|
|
326a75d0e8 | ||
|
|
e47860bc2e | ||
|
|
6212151562 | ||
|
|
8c2b9f9901 | ||
|
|
e9a733a789 | ||
|
|
b2fe723570 | ||
|
|
33ba8dabe0 | ||
|
|
0842e54f52 | ||
|
|
08d34e41c6 | ||
|
|
e93c9f74c9 | ||
|
|
1ab81256e9 | ||
|
|
cb19de57bb | ||
|
|
e678e7df8f | ||
|
|
8487eefe46 | ||
|
|
86da1aa429 | ||
|
|
48b580b59e | ||
|
|
8b568d796e | ||
|
|
4b5af85094 | ||
|
|
8d0be06f45 | ||
|
|
26ae8accd4 | ||
|
|
321e3e27de | ||
|
|
7d26968bb3 | ||
|
|
83464a882e | ||
|
|
1e94ad25e1 | ||
|
|
a3ba19db96 | ||
|
|
d9e9c849e1 | ||
|
|
c162c23d9e | ||
|
|
90fd1c300f | ||
|
|
902a9cc812 | ||
|
|
c51e1805fa | ||
|
|
7a2b9e85e7 | ||
|
|
547c40cd52 | ||
|
|
e2d631097d | ||
|
|
cc55be0b0a | ||
|
|
64927190bd | ||
|
|
24515126fe | ||
|
|
7a56a93028 | ||
|
|
ea7fc3a171 | ||
|
|
ae36a26045 | ||
|
|
d6c9f5c64b | ||
|
|
c4f4f4295b | ||
|
|
a997322efb | ||
|
|
799688af76 | ||
|
|
260e95d027 | ||
|
|
f9ee70aa2e | ||
|
|
50f13cb84b | ||
|
|
fc7e08bb49 | ||
|
|
06ed3e5dfd | ||
|
|
4d792ad57b | ||
|
|
4fa78bfca0 | ||
|
|
2012833cb3 | ||
|
|
e48eef7e32 | ||
|
|
74ac9c3a92 | ||
|
|
a907d789d6 | ||
|
|
fc46c0b49c | ||
|
|
fef7862045 | ||
|
|
d9441a6bdd | ||
|
|
332cb0896b | ||
|
|
d1b0c28924 | ||
|
|
dce958aac4 | ||
|
|
438940219e | ||
|
|
f28fcec81d | ||
|
|
586d027f86 | ||
|
|
bd4fb7486d | ||
|
|
f9cd2b8f36 | ||
|
|
62e22236b7 | ||
|
|
8b157f427a | ||
|
|
f165c1d9b0 | ||
|
|
500e2d62a0 | ||
|
|
a06e8677ac | ||
|
|
b4d5783928 | ||
|
|
3ce7f45503 | ||
|
|
b436c2761a | ||
|
|
b586d3bb0e | ||
|
|
63688a2f95 | ||
|
|
379cb1b2e0 | ||
|
|
78429492f1 | ||
|
|
9875047674 | ||
|
|
5014b0a9cb | ||
|
|
ef841b1aa3 | ||
|
|
368f27ffbc | ||
|
|
0e50bc1443 | ||
|
|
7c4a6ddcdf | ||
|
|
7ab6d95b6c | ||
|
|
6c32b89906 | ||
|
|
056e659a20 | ||
|
|
62baff665c | ||
|
|
7c5eb0ae37 | ||
|
|
36bce6c468 | ||
|
|
65df02163d | ||
|
|
19a32cdfd3 | ||
|
|
d708f386a1 | ||
|
|
0f837f4bed | ||
|
|
242e8e2bb3 | ||
|
|
1d56b24b67 | ||
|
|
bb9138708a | ||
|
|
34f5510f1f | ||
|
|
6c6d47c89c | ||
|
|
196075c031 | ||
|
|
2e5e8f73c6 | ||
|
|
ada5d38272 | ||
|
|
c4b0f773db | ||
|
|
276daf631e | ||
|
|
fb19b58147 | ||
|
|
13a5e3cf6f | ||
|
|
1caf3caf1b | ||
|
|
564370f79a | ||
|
|
24e749a2c9 | ||
|
|
cccdc51ad4 | ||
|
|
99ddce6c3e | ||
|
|
f68088cfb5 | ||
|
|
c8f56d748a | ||
|
|
a43fc47bb6 | ||
|
|
8c1bfac53b | ||
|
|
97853c3660 | ||
|
|
f304a30193 | ||
|
|
7eadca3959 | ||
|
|
8aa6decbf9 | ||
|
|
7cf4bcaca2 | ||
|
|
9ccd9c3e0e | ||
|
|
c6773a6303 | ||
|
|
e858a32aa1 | ||
|
|
99f2680e2c | ||
|
|
7a9a323bac | ||
|
|
62aa234352 | ||
|
|
0cb9e7922a | ||
|
|
e73107006e | ||
|
|
ca389cc6fc | ||
|
|
60ec7f0cbf | ||
|
|
d342d59e65 | ||
|
|
2690fa2da5 | ||
|
|
e411c394ca | ||
|
|
d69f3ba225 | ||
|
|
739807b1a9 | ||
|
|
d029ea7f3f | ||
|
|
11098cb869 | ||
|
|
f6807d6b22 | ||
|
|
7fc9bacf54 | ||
|
|
57ea4c1d92 | ||
|
|
bcdd15ef3a | ||
|
|
5f939c3123 | ||
|
|
2446fc44ad | ||
|
|
9ba8dd91df | ||
|
|
10e1cdbc52 | ||
|
|
46eceb38d5 | ||
|
|
81de882e2f | ||
|
|
593e07cdff | ||
|
|
8ca54f616e | ||
|
|
f7f899f0a4 | ||
|
|
05a3c0c89b | ||
|
|
f21691c122 | ||
|
|
836e26d8d0 | ||
|
|
8a7c1fe4cb | ||
|
|
7f43d3bb37 | ||
|
|
11b975ab19 | ||
|
|
315e4215d9 | ||
|
|
e35e6c44cf | ||
|
|
260cb78e3a | ||
|
|
a1f04d2129 | ||
|
|
9b562eebcd | ||
|
|
1d175c4557 | ||
|
|
f755070080 | ||
|
|
1c6c72a0fe | ||
|
|
1755f2ea3d | ||
|
|
498cc6c80b | ||
|
|
8d3227a92b | ||
|
|
c6d855084e | ||
|
|
827b3f8aeb | ||
|
|
fb95573000 | ||
|
|
f026bd455f | ||
|
|
f0b92a5757 | ||
|
|
d7c6f1e63b | ||
|
|
1e9e308df3 | ||
|
|
c9a70f149d | ||
|
|
d4ff47b6ac | ||
|
|
8b4b241403 | ||
|
|
6dcd6947d7 | ||
|
|
327328412a | ||
|
|
42f9ef00b9 | ||
|
|
8c2ea0fa26 | ||
|
|
14e9afaf42 | ||
|
|
a3a101641a | ||
|
|
8bd93fe495 | ||
|
|
56df22bca7 | ||
|
|
5f32a6738a | ||
|
|
1c081935fb | ||
|
|
8fd4d00776 | ||
|
|
7d04ea58c3 | ||
|
|
2cc84a0f0d | ||
|
|
8f715532cb | ||
|
|
5a77df7cc5 | ||
|
|
59658f2b0b | ||
|
|
0b983906da | ||
|
|
cd1f164d18 | ||
|
|
e2a6ac6625 | ||
|
|
8e8c10c438 | ||
|
|
7ff25f282e | ||
|
|
b8dc608032 | ||
|
|
d7e699320b | ||
|
|
ef333da770 | ||
|
|
575a389b08 | ||
|
|
9bc0824be6 | ||
|
|
b656a60234 | ||
|
|
bd988d805c | ||
|
|
7ad7ccb8fe | ||
|
|
e30c535f18 | ||
|
|
de7706f622 | ||
|
|
de20e4c9dd | ||
|
|
41f9314e2a | ||
|
|
2280ce349a | ||
|
|
7aa05e1c9f | ||
|
|
69d174c9e8 | ||
|
|
3c38fa6b70 | ||
|
|
728c8b4663 | ||
|
|
fab9563cd5 | ||
|
|
20fe2473e1 | ||
|
|
3815062c11 | ||
|
|
581ea9fda0 | ||
|
|
bfa641cea8 | ||
|
|
29c58efeb3 | ||
|
|
27eb82c556 | ||
|
|
652d67a20f | ||
|
|
ce0984f02f | ||
|
|
b3e3b1e245 | ||
|
|
c69ee180af | ||
|
|
bba3a25371 | ||
|
|
095b358aca | ||
|
|
833e5f46cc | ||
|
|
3e0ce0e07a | ||
|
|
1f31dd12fc | ||
|
|
f63efc29bf | ||
|
|
3e394f14e8 | ||
|
|
ff6ffa1656 | ||
|
|
304c259a57 | ||
|
|
630754b52e | ||
|
|
afd8c0d879 | ||
|
|
6316ee7c9b | ||
|
|
b6b8d11881 | ||
|
|
8753fd5887 | ||
|
|
5aaafb5ac1 | ||
|
|
a043557c44 | ||
|
|
4af4914e32 | ||
|
|
e35b3f1e80 | ||
|
|
ff8859b9db | ||
|
|
937ff5a378 | ||
|
|
5b131cf77c | ||
|
|
ce6ec64069 |
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -3,7 +3,7 @@ updates:
|
||||
- package-ecosystem: "cargo"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
interval: "monthly"
|
||||
commit-message:
|
||||
prefix: "cargo"
|
||||
open-pull-requests-limit: 10
|
||||
open-pull-requests-limit: 50
|
||||
|
||||
26
.github/mergeable.yml
vendored
Normal file
26
.github/mergeable.yml
vendored
Normal file
@@ -0,0 +1,26 @@
|
||||
version: 2
|
||||
mergeable:
|
||||
- when: pull_request.*
|
||||
name: "Changelog check"
|
||||
validate:
|
||||
- do: or
|
||||
validate:
|
||||
- do: description
|
||||
must_include:
|
||||
regex: '#skip-changelog'
|
||||
- do: and
|
||||
validate:
|
||||
- do: dependent
|
||||
changed:
|
||||
file: 'src/**'
|
||||
required: ['CHANGELOG.md']
|
||||
- do: dependent
|
||||
changed:
|
||||
file: 'deltachat-ffi/**'
|
||||
required: ['CHANGELOG.md']
|
||||
fail:
|
||||
- do: checks
|
||||
status: 'action_required'
|
||||
payload:
|
||||
title: Changlog might need an update
|
||||
summary: "Check if CHANGELOG.md needs an update or add #skip-changelog to the PR description."
|
||||
10
.github/workflows/ci.yml
vendored
10
.github/workflows/ci.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --workspace --tests --examples
|
||||
args: --workspace --tests --examples --benches
|
||||
|
||||
docs:
|
||||
name: Rust doc comments
|
||||
@@ -77,19 +77,19 @@ jobs:
|
||||
include:
|
||||
# Currently used Rust version, same as in `rust-toolchain` file.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.54.0
|
||||
rust: 1.60.0
|
||||
python: 3.9
|
||||
- os: windows-latest
|
||||
rust: 1.54.0
|
||||
rust: 1.60.0
|
||||
python: false # Python bindings compilation on Windows is not supported.
|
||||
|
||||
# Minimum Supported Rust Version = 1.51.0
|
||||
# Minimum Supported Rust Version = 1.56.0
|
||||
#
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.51.0
|
||||
rust: 1.56.0
|
||||
python: 3.7
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
||||
21
.github/workflows/dependabot.yml
vendored
Normal file
21
.github/workflows/dependabot.yml
vendored
Normal file
@@ -0,0 +1,21 @@
|
||||
name: Dependabot auto-approve
|
||||
on: pull_request
|
||||
|
||||
permissions:
|
||||
pull-requests: write
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
runs-on: ubuntu-latest
|
||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v1.1.1
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
- name: Approve a PR
|
||||
run: gh pr review --approve "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{github.event.pull_request.html_url}}
|
||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
||||
32
.github/workflows/node-delete-preview.yml
vendored
Normal file
32
.github/workflows/node-delete-preview.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# 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 Pullrequest ID
|
||||
id: getid
|
||||
run: |
|
||||
export PULLREQUEST_ID=$(jq .number < $GITHUB_EVENT_PATH)
|
||||
echo ::set-output name=prid::$PULLREQUEST_ID
|
||||
- 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/"
|
||||
34
.github/workflows/node-docs.yml
vendored
Normal file
34
.github/workflows/node-docs.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Generate & upload node.js documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
generate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: npm install and generate documentation
|
||||
run: |
|
||||
cd node
|
||||
npm i --ignore-scripts
|
||||
npx typedoc
|
||||
mv docs js
|
||||
|
||||
- name: Upload
|
||||
uses: horochx/deploy-via-scp@v1.0.1
|
||||
with:
|
||||
user: ${{ secrets.USERNAME }}
|
||||
key: ${{ secrets.KEY }}
|
||||
host: "delta.chat"
|
||||
port: 22
|
||||
local: "node/js"
|
||||
remote: "/var/www/html/"
|
||||
140
.github/workflows/node-package.yml
vendored
Normal file
140
.github/workflows/node-package.yml
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
name: 'node.js'
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
|
||||
jobs:
|
||||
prebuild:
|
||||
name: 'Prebuild'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-18.04, macos-latest, windows-latest]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: System info
|
||||
run: |
|
||||
rustc -vV
|
||||
rustup -vV
|
||||
cargo -vV
|
||||
npm --version
|
||||
node --version
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
${{ env.APPDATA }}/npm-cache
|
||||
~/.npm
|
||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v2
|
||||
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'
|
||||
run: |
|
||||
cd node
|
||||
npm install --verbose
|
||||
|
||||
- name: Build Prebuild
|
||||
run: |
|
||||
cd node
|
||||
npm run prebuildify
|
||||
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
|
||||
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: node/${{ matrix.os }}.tar.gz
|
||||
|
||||
pack-module:
|
||||
needs: prebuild
|
||||
name: 'Package deltachat-node and upload to download.delta.chat'
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- name: install tree
|
||||
run: sudo apt install tree
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: System info
|
||||
run: |
|
||||
rustc -vV
|
||||
rustup -vV
|
||||
cargo -vV
|
||||
npm --version
|
||||
node --version
|
||||
- name: Download ubuntu prebuild
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: ubuntu-18.04
|
||||
- name: Download macos prebuild
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: macos-latest
|
||||
- name: Download windows prebuild
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: windows-latest
|
||||
- shell: bash
|
||||
run: |
|
||||
mkdir node/prebuilds
|
||||
tar -xvzf ubuntu-18.04/ubuntu-18.04.tar.gz -C node/prebuilds
|
||||
tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
|
||||
tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
|
||||
tree node/prebuilds
|
||||
- name: install dependencies without running scripts
|
||||
run: |
|
||||
npm install --ignore-scripts
|
||||
- name: build typescript part
|
||||
run: |
|
||||
npm run build:bindings:ts
|
||||
- name: Set DELTACHAT_NODE_TAR_GZ env variable
|
||||
run: |
|
||||
echo "DELTACHAT_NODE_TAR_GZ=deltachat-node-${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
- name: package
|
||||
shell: bash
|
||||
run: |
|
||||
npm pack .
|
||||
ls -lah
|
||||
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: deltachat-node.tgz
|
||||
path: $DELTACHAT_NODE_TAR_GZ
|
||||
# Upload to download.delta.chat/node/preview/
|
||||
- name: Upload deltachat-node preview to download.delta.chat/node/preview/
|
||||
id: upload-preview
|
||||
shell: bash
|
||||
run: |
|
||||
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
|
||||
chmod 600 __TEMP_INPUT_KEY_FILE
|
||||
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r $DELTACHAT_NODE_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
|
||||
continue-on-error: true
|
||||
# Upload to download.delta.chat/node/
|
||||
- name: Upload deltachat-node build to download.delta.chat/node/
|
||||
if: ${{ steps.tag.outputs.tag }}
|
||||
id: upload
|
||||
shell: bash
|
||||
run: |
|
||||
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
|
||||
chmod 600 __TEMP_INPUT_KEY_FILE
|
||||
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r $DELTACHAT_NODE_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/"
|
||||
28
.github/workflows/upload-docs.yml
vendored
Normal file
28
.github/workflows/upload-docs.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Build & Deploy Documentation on rs.delta.chat
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- docs-gh-action
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- name: Build the documentation with cargo
|
||||
run: |
|
||||
cargo doc --package deltachat --no-deps
|
||||
- name: Upload to rs.delta.chat
|
||||
uses: up9cloud/action-rsync@v1.3
|
||||
env:
|
||||
USER: ${{ secrets.USERNAME }}
|
||||
KEY: ${{ secrets.KEY }}
|
||||
HOST: "delta.chat"
|
||||
SOURCE: "target/doc"
|
||||
TARGET: "/var/www/html/rs/"
|
||||
|
||||
28
.github/workflows/upload-ffi-docs.yml
vendored
Normal file
28
.github/workflows/upload-ffi-docs.yml
vendored
Normal file
@@ -0,0 +1,28 @@
|
||||
name: Build & Deploy Documentation on cffi.delta.chat
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
- docs-gh-action
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- name: Build the documentation with cargo
|
||||
run: |
|
||||
cargo doc --package deltachat_ffi --no-deps
|
||||
- name: Upload to cffi.delta.chat
|
||||
uses: up9cloud/action-rsync@v1.3
|
||||
env:
|
||||
USER: ${{ secrets.USERNAME }}
|
||||
KEY: ${{ secrets.KEY }}
|
||||
HOST: "delta.chat"
|
||||
SOURCE: "target/doc"
|
||||
TARGET: "/var/www/html/cffi/"
|
||||
|
||||
11
.gitignore
vendored
11
.gitignore
vendored
@@ -29,3 +29,14 @@ deltachat-ffi/xml
|
||||
|
||||
coverage/
|
||||
.DS_Store
|
||||
.vscode/launch.json
|
||||
python/accounts.txt
|
||||
python/all-testaccounts.txt
|
||||
tmp/
|
||||
|
||||
# from deltachat-node
|
||||
node_modules/
|
||||
node/build/
|
||||
node/dist/
|
||||
node/prebuilds/
|
||||
node/.nyc_output/
|
||||
|
||||
42
.npmignore
Normal file
42
.npmignore
Normal file
@@ -0,0 +1,42 @@
|
||||
.circleci/
|
||||
.gitmodules
|
||||
node/.nyc_output/
|
||||
.travis.yml
|
||||
appveyor.yml
|
||||
node/build/
|
||||
node/README.md
|
||||
rustfmt.toml
|
||||
spec.md
|
||||
test-data/
|
||||
build2/
|
||||
node_modules
|
||||
.git
|
||||
.idea/
|
||||
.pytest_cache
|
||||
CMakeLists.txt
|
||||
README.md
|
||||
contrib/
|
||||
node/ci_scripts/
|
||||
coverage/
|
||||
node/.circleci
|
||||
node/appveyor.yml
|
||||
ci/
|
||||
ci_scripts/
|
||||
python/
|
||||
target
|
||||
proptest-regressions
|
||||
deltachat-ffi/Doxyfile
|
||||
scripts
|
||||
webxdc.md
|
||||
standards.md
|
||||
draft/
|
||||
node/examples/
|
||||
# deltachat-core-rust/assets # don't exclude assets, otherwise it won't build
|
||||
node/images/
|
||||
node/test/
|
||||
node/windows.md
|
||||
node/*.tar.gz
|
||||
node/old_docs.md
|
||||
.vscode/
|
||||
.github/
|
||||
node/.prettierrc.yml
|
||||
240
CHANGELOG.md
240
CHANGELOG.md
@@ -1,11 +1,245 @@
|
||||
# Changelog
|
||||
|
||||
## Unreleased
|
||||
## 1.81.0
|
||||
|
||||
### API-Changes
|
||||
- deprecate unused `marker1before` argument of `dc_get_chat_msgs`
|
||||
and remove `DC_MSG_ID_MARKER1` constant #3274
|
||||
|
||||
### Changes
|
||||
- now the node-bindings are also part of this repository 🎉 #3283
|
||||
- support `source_code_url` from Webxdc manifests #3314
|
||||
- support Webxdc document names and add `document` to `dc_msg_get_webxdc_info()` #3317 #3324
|
||||
- improve chat encryption info, make it easier to find contacts without keys #3318
|
||||
- improve error reporting when creating a folder fails #3325
|
||||
- node: remove unmaintained coverage scripts
|
||||
- send normal messages with higher priority than MDNs #3243
|
||||
- make Scheduler stateless #3302
|
||||
- abort instead of unwinding on panic #3259
|
||||
- improve python bindings #3297 #3298
|
||||
- improve documentation #3307 #3306 #3309 #3319 #3321
|
||||
- refactorings #3304 #3303 #3323
|
||||
|
||||
### Fixes
|
||||
- node: throw error when getting context with an invalid account id
|
||||
- node: throw error when instanciating a wrapper class on `null` (Context, Message, Chat, ChatList and so on)
|
||||
- use same contact-color if email address differ only in upper-/lowercase #3327
|
||||
- fix race condition in ongoing process (import/export, configuration) allocation
|
||||
- repair encrypted mails "mixed up" by Google Workspace "Append footer" function #3315
|
||||
|
||||
### Removed
|
||||
- node: remove unmaintained coverage scripts
|
||||
|
||||
|
||||
## 1.80.0
|
||||
|
||||
### Changes
|
||||
- update provider database #3284
|
||||
- improve python bindings, tests and ci #3287 #3286 #3287 #3289 #3290 #3292
|
||||
|
||||
### Fixes
|
||||
- fix escaping in generated QR-code-SVG #3295
|
||||
|
||||
|
||||
## 1.79.0
|
||||
|
||||
### Changes
|
||||
- Send locations in the background regardless of SMTP loop activity #3247
|
||||
- refactorings #3268
|
||||
- improve tests and ci #3266 #3271
|
||||
|
||||
### Fixes
|
||||
- simplify `dc_stop_io()` and remove potential panics and race conditions #3273
|
||||
- fix correct message escaping consisting of a dot in SMTP protocol #3265
|
||||
|
||||
|
||||
## 1.78.0
|
||||
|
||||
### API-Changes
|
||||
- replaced stock string `DC_STR_ONE_MOMENT` by `DC_STR_NOT_CONNECTED` #3222
|
||||
- add `dc_resend_msgs()` #3238
|
||||
- `dc_provider_new_from_email()` does no longer do an DNS lookup for checking custom domains,
|
||||
this is done by `dc_provider_new_from_email_with_dns()` now #3256
|
||||
|
||||
### Changes
|
||||
- introduce multiple self addresses with the "configured" address always being the primary one #2896
|
||||
- Further improve finding the correct server after logging in #3208
|
||||
- `get_connectivity_html()` returns HTML as non-scalable #3213
|
||||
- add update-serial to `DC_EVENT_WEBXDC_STATUS_UPDATE` #3215
|
||||
- Speed up message receiving via IMAP a bit #3225
|
||||
- mark messages as seen on IMAP in batches #3223
|
||||
- remove Received: based draft detection heuristic #3230
|
||||
- Use pkgconfig for building Python package #2590
|
||||
- don't start io on unconfigured context #2664
|
||||
- do not assign group IDs to ad-hoc groups #2798
|
||||
- dynamic libraries use dylib extension on Darwin #3226
|
||||
- refactorings #3217 #3219 #3224 #3235 #3239 #3244 #3254
|
||||
- improve documentation #3214 #3220 #3237
|
||||
- improve tests and ci #3212 #3233 #3241 #3242 #3252 #3250 #3255 #3260
|
||||
|
||||
### Fixes
|
||||
- Take `delete_device_after` into account when calculating ephemeral loop timeout #3211 #3221
|
||||
- Fix a bug where a blocked contact could send a contact request #3218
|
||||
- Make sure, videochat-room-names are always URL-safe #3231
|
||||
- Try removing account folder multiple times in case of failure #3229
|
||||
- Ignore messages from all spam folders if there are many #3246
|
||||
- Hide location-only messages instead of displaying empty bubbles #3248
|
||||
|
||||
|
||||
## 1.77.0
|
||||
|
||||
### API changes
|
||||
- change semantics of `dc_get_webxdc_status_updates()` second parameter
|
||||
and remove update-id from `DC_EVENT_WEBXDC_STATUS_UPDATE` #3081
|
||||
|
||||
### Changes
|
||||
- add more SMTP logging #3093
|
||||
- place common headers like `From:` before the large `Autocrypt:` header #3079
|
||||
- keep track of securejoin joiner status in database to survive restarts #2920
|
||||
- remove never used `SentboxMove` option #3111
|
||||
- improve speed by caching config values #3131 #3145
|
||||
- optimize `markseen_msgs` #3141
|
||||
- automatically accept chats with outgoing messages #3143
|
||||
- `dc_receive_imf` refactorings #3154 #3156 #3159
|
||||
- add index to speedup deletion of expired ephemeral messages #3155
|
||||
- muted chats stay archived on new messages #3184
|
||||
- support `min_api` from Webxdc manifests #3206
|
||||
- do not read whole webxdc file into memory #3109
|
||||
- improve tests, refactorings #3073 #3096 #3102 #3108 #3139 #3128 #3133 #3142 #3153 #3151 #3174 #3170 #3148 #3179 #3185
|
||||
- improve documentation #2983 #3112 #3103 #3118 #3120
|
||||
|
||||
### Fixes
|
||||
- speed up loading of chat messages by a factor of 20 #3171 #3194 #3173
|
||||
- fix an issue where the app crashes when trying to export a backup #3195
|
||||
- hopefully fix a bug where outgoing messages appear twice with Amazon SES #3077
|
||||
- do not delete messages without Message-IDs as duplicates #3095
|
||||
- assign replies from a different email address to the correct chat #3119
|
||||
- assing outgoing private replies to the correct chat #3177
|
||||
- start ephemeral timer when seen status is synchronized via IMAP #3122
|
||||
- do not create empty contact requests with "setup changed" messages;
|
||||
instead, send a "setup changed" message into all chats we share with the peer #3187
|
||||
- do not delete duplicate messages on IMAP immediately to accidentally deleting
|
||||
the last copy #3138
|
||||
- clear more columns when message expires due to `delete_device_after` setting #3181
|
||||
- do not try to use stale SMTP connections #3180
|
||||
- slightly improve finding the correct server after logging in #3207
|
||||
- retry message sending automatically if loop is not interrupted #3183
|
||||
- fix a bug where sometimes the file extension of a long filename containing a dot was cropped #3098
|
||||
|
||||
|
||||
## 1.76.0
|
||||
|
||||
### Changes
|
||||
- move messages in batches #3058
|
||||
- delete messages in batches #3060
|
||||
- python: remove arbitrary timeouts from tests #3059
|
||||
- refactorings #3026
|
||||
|
||||
### Fixes
|
||||
- avoid archived, fresh chats #3053
|
||||
- Also resync UIDs in folders that are not configured #2289
|
||||
- treat "NO" IMAP response to MOVE and COPY commands as an error #3058
|
||||
- Fix a bug where messages in the Spam folder created contact requests #3015
|
||||
- Fix a bug where drafts disappeared after some days #3067
|
||||
- Parse MS Exchange read receipts and mark the original message as read #3075
|
||||
- do not retry message sending infinitely in case of permanent SMTP failure #3070
|
||||
- set message state to failed when retry limit is exceeded #3072
|
||||
|
||||
|
||||
## 1.75.0
|
||||
|
||||
### Changes
|
||||
- optimize `delete_expired_imap_messages()` #3047
|
||||
|
||||
|
||||
## 1.74.0
|
||||
|
||||
### Fixes
|
||||
- avoid reconnection loop when message without Message-ID is marked as seen #3044
|
||||
|
||||
|
||||
## 1.73.0
|
||||
|
||||
### API changes
|
||||
- added `only_fetch_mvbox` config #3028
|
||||
|
||||
### Changes
|
||||
- don't watch Sent folder by default #3025
|
||||
- use webxdc app name in chatlist/quotes/replies etc. #3027
|
||||
- make it possible to cancel message sending by removing the message #3034,
|
||||
this was previosuly removed in 1.71.0 #2939
|
||||
- synchronize Seen flags only on watched folders to speed up
|
||||
folder scanning #3041
|
||||
- remove direct dependency on `byteorder` crate #3031
|
||||
- refactorings #3023 #3013
|
||||
- update provider database #3043
|
||||
- improve documentation #3017 #3018 #3021
|
||||
|
||||
### Fixes
|
||||
- fix splitting off text from webxdc messages #3032
|
||||
- call slow `delete_expired_imap_messages()` less often #3037
|
||||
- make synchronization of Seen status more robust in case unsolicited FETCH
|
||||
result without UID is returned #3022
|
||||
- fetch Inbox before scanning folders to ensure iOS does
|
||||
not kill the app before it gets to fetch the Inbox in background #3040
|
||||
|
||||
|
||||
## 1.72.0
|
||||
|
||||
### Fixes
|
||||
- run migrations on backup import #3006
|
||||
|
||||
|
||||
## 1.71.0
|
||||
|
||||
### API Changes
|
||||
- added APIs to handle database passwords: `dc_context_new_closed()`, `dc_context_open()`,
|
||||
`dc_context_is_open()` and `dc_accounts_add_closed_account()` #2956 #2972
|
||||
- use second parameter of `dc_imex` to provide backup passphrase #2980
|
||||
- added `DC_MSG_WEBXDC`, `dc_send_webxdc_status_update()`,
|
||||
`dc_get_webxdc_status_updates()`, `dc_msg_get_webxdc_blob()`, `dc_msg_get_webxdc_info()`
|
||||
and `DC_EVENT_WEBXDC_STATUS_UPDATE` #2826 #2971 #2975 #2977 #2979 #2993 #2994 #2998 #3001 #3003
|
||||
- added `dc_msg_get_parent()` #2984
|
||||
- added `dc_msg_force_plaintext()` API for bots #2847
|
||||
- allow removing quotes on drafts `dc_msg_set_quote(msg, NULL)` #2950
|
||||
- removed `mvbox_watch` option; watching is enabled when `mvbox_move` is enabled #2906
|
||||
- removed `inbox_watch` option #2922
|
||||
- deprecated `os_name` in `dc_context_new()`, pass `NULL` or an empty string #2956
|
||||
|
||||
### Changes
|
||||
- start making it possible to write to mailing lists #2736
|
||||
- add `hop_info` to `dc_get_info()` #2751 #2914 #2923
|
||||
- add information about whether the database is encrypted or not to `dc_get_info()` #3000
|
||||
- selfstatus now defaults to empty #2951 #2960
|
||||
- validate detached cryptographic signatures as used eg. by Thunderbird #2865
|
||||
- do not change the draft's `msg_id` on updates and sending #2887
|
||||
- add `imap` table to keep track of message UIDs #2909 #2938
|
||||
- replace `SendMsgToSmtp` jobs which stored outgoing messages in blobdir with `smtp` SQL table #2939 #2996
|
||||
- sql: enable `auto_vacuum=INCREMENTAL` #2931
|
||||
- sql: build rusqlite with sqlcipher #2934
|
||||
- synchronize Seen status across devices #2942
|
||||
- `dc_preconfigure_keypair` now takes ascii armored keys instead of base64 #2862
|
||||
- put removed member in Bcc instead of To in the message about removal #2864
|
||||
- improve group updates #2889
|
||||
- re-write the blob filename creation loop #2981
|
||||
- update provider database (11 Jan 2022) #2959
|
||||
- python: allow timeout for internal configure tracker API #2967
|
||||
- python: remove API deprecated in Python 3.10 #2907
|
||||
- refactorings #2932 #2957 #2947
|
||||
- improve tests #2863 #2866 #2881 #2908 #2918 #2901 #2973
|
||||
- improve documentation #2880 #2886 #2895
|
||||
- improve ci #2919 #2926 #2969 #2999
|
||||
|
||||
### Fixes
|
||||
- fix leaving groups #2929
|
||||
- fix unread count #2861
|
||||
- make `add_parts()` not early-exit #2879
|
||||
- recognize MS Exchange read receipts as read receipts #2890
|
||||
- create parent directory if creating a new file fails #2978
|
||||
- save "configured" flag later #2974
|
||||
- improve log #2928
|
||||
- `dc_receive_imf`: don't fail on invalid address in the To field #2940
|
||||
|
||||
- Removed `mvbox_watch` option. #2906
|
||||
It is automatically enabled whenever `mvbox_move` is enabled.
|
||||
|
||||
## 1.70.0
|
||||
|
||||
|
||||
@@ -4,10 +4,18 @@ include(GNUInstallDirs)
|
||||
|
||||
find_program(CARGO cargo)
|
||||
|
||||
if(APPLE)
|
||||
set(DYNAMIC_EXT "dylib")
|
||||
elseif(UNIX)
|
||||
set(DYNAMIC_EXT "so")
|
||||
else()
|
||||
set(DYNAMIC_EXT "dll")
|
||||
endif()
|
||||
|
||||
add_custom_command(
|
||||
OUTPUT
|
||||
"target/release/libdeltachat.a"
|
||||
"target/release/libdeltachat.so"
|
||||
"target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"target/release/pkgconfig/deltachat.pc"
|
||||
COMMAND
|
||||
PREFIX=${CMAKE_INSTALL_PREFIX}
|
||||
@@ -32,11 +40,11 @@ add_custom_target(
|
||||
ALL
|
||||
DEPENDS
|
||||
"target/release/libdeltachat.a"
|
||||
"target/release/libdeltachat.so"
|
||||
"target/release/libdeltachat.${DYNAMIC_EXT}"
|
||||
"target/release/pkgconfig/deltachat.pc"
|
||||
)
|
||||
|
||||
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "target/release/libdeltachat.so" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||
install(FILES "target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||
|
||||
921
Cargo.lock
generated
921
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
49
Cargo.toml
49
Cargo.toml
@@ -1,16 +1,18 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.70.0"
|
||||
version = "1.81.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
resolver = "2"
|
||||
rust-version = "1.56"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
panic = 'abort'
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = 'abort'
|
||||
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
@@ -19,15 +21,14 @@ ansi_term = { version = "0.12.1", optional = true }
|
||||
anyhow = "1"
|
||||
async-imap = { git = "https://github.com/async-email/async-imap" }
|
||||
async-native-tls = { version = "0.3" }
|
||||
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", features = ["socks5"] }
|
||||
async-std-resolver = "0.20"
|
||||
async-std = { version = "1", features = ["unstable"] }
|
||||
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", default-features=false, features = ["smtp-transport", "socks5"] }
|
||||
async-std-resolver = "0.21"
|
||||
async-std = { version = "1" }
|
||||
async-tar = { version = "0.4", default-features=false }
|
||||
async-trait = "0.1"
|
||||
backtrace = "0.3"
|
||||
base64 = "0.13"
|
||||
bitflags = "1.3"
|
||||
byteorder = "1.3"
|
||||
chrono = "0.4"
|
||||
dirs = { version = "4", optional=true }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
@@ -35,26 +36,26 @@ encoded-words = { git = "https://github.com/async-email/encoded-words", branch="
|
||||
escaper = "0.1"
|
||||
futures = "0.3"
|
||||
hex = "0.4.0"
|
||||
image = { version = "0.23.5", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
image = { version = "0.24.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
kamadak-exif = "0.5"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2"
|
||||
log = {version = "0.4.8", optional = true }
|
||||
log = {version = "0.4.16", optional = true }
|
||||
mailparse = "0.13"
|
||||
native-tls = "0.2"
|
||||
num_cpus = "1.13"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.9.0"
|
||||
once_cell = "1.10.0"
|
||||
percent-encoding = "2.0"
|
||||
pgp = { version = "0.7", default-features = false }
|
||||
pretty_env_logger = { version = "0.4", optional = true }
|
||||
quick-xml = "0.22"
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.19"
|
||||
r2d2_sqlite = "0.20"
|
||||
rand = "0.7"
|
||||
regex = "1.5"
|
||||
rusqlite = { version = "0.26", features = ["sqlcipher"] }
|
||||
rusqlite = { version = "0.27", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustyline = { version = "9", optional = true }
|
||||
sanitize-filename = "0.3"
|
||||
@@ -63,9 +64,8 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
sha-1 = "0.10"
|
||||
sha2 = "0.10"
|
||||
smallvec = "1"
|
||||
stop-token = "0.6"
|
||||
strum = "0.23"
|
||||
strum_macros = "0.23"
|
||||
strum = "0.24"
|
||||
strum_macros = "0.24"
|
||||
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
|
||||
thiserror = "1"
|
||||
toml = "0.5"
|
||||
@@ -74,13 +74,14 @@ uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
fast-socks5 = "0.4"
|
||||
humansize = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
tagger = "3.3.0"
|
||||
textwrap = "0.14.2"
|
||||
tagger = "4.3.3"
|
||||
textwrap = "0.15.0"
|
||||
zip = { version = "0.6.2", default-features = false, features = ["deflate"] }
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
async-std = { version = "1", features = ["unstable", "attributes"] }
|
||||
criterion = "0.3"
|
||||
criterion = { version = "0.3.4", features = ["async_std"] }
|
||||
futures-lite = "1.12"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
@@ -116,6 +117,18 @@ harness = false
|
||||
name = "search_msgs"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "receive_emails"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "get_chat_msgs"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "get_chatlist"
|
||||
harness = false
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
|
||||
12
README.md
12
README.md
@@ -12,6 +12,8 @@ To download and install the official compiler for the Rust programming language,
|
||||
$ curl https://sh.rustup.rs -sSf | sh
|
||||
```
|
||||
|
||||
> On Windows, you may need to also install **Perl** to be able to compile deltachat-core.
|
||||
|
||||
## Using the CLI client
|
||||
|
||||
Compile and run Delta Chat Core command line utility, using `cargo`:
|
||||
@@ -125,11 +127,11 @@ $ cargo test -- --ignored
|
||||
|
||||
Language bindings are available for:
|
||||
|
||||
- [C](https://c.delta.chat)
|
||||
- [Node.js](https://www.npmjs.com/package/deltachat-node)
|
||||
- [Python](https://py.delta.chat)
|
||||
- [Go](https://github.com/deltachat/go-deltachat/)
|
||||
- [Free Pascal](https://github.com/deltachat/deltachat-fp/)
|
||||
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
||||
- **Node.js** \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
|
||||
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
|
||||
- **Go** \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
|
||||
- **Free Pascal** \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
|
||||
- **Java** and **Swift** (contained in the Android/iOS repos)
|
||||
|
||||
The following "frontend" projects make use of the Rust-library
|
||||
|
||||
BIN
assets/icon-webxdc.png
Normal file
BIN
assets/icon-webxdc.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.9 KiB |
181
assets/icon-webxdc.svg
Normal file
181
assets/icon-webxdc.svg
Normal file
@@ -0,0 +1,181 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
||||
xmlns:cc="http://creativecommons.org/ns#"
|
||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:svg="http://www.w3.org/2000/svg"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
width="80mm"
|
||||
height="297mm"
|
||||
viewBox="0 0 80 297"
|
||||
version="1.1"
|
||||
id="svg71"
|
||||
inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
|
||||
sodipodi:docname="icon-webxdc.svg"
|
||||
inkscape:export-filename="C:\Users\user\OneDrive - BFW-Leipzig\Documents\LogoDC\finalohnerand.png"
|
||||
inkscape:export-xdpi="96"
|
||||
inkscape:export-ydpi="96">
|
||||
<metadata
|
||||
id="metadata856">
|
||||
<rdf:RDF>
|
||||
<cc:Work
|
||||
rdf:about="">
|
||||
<dc:format>image/svg+xml</dc:format>
|
||||
<dc:type
|
||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
||||
<dc:title></dc:title>
|
||||
</cc:Work>
|
||||
</rdf:RDF>
|
||||
</metadata>
|
||||
<sodipodi:namedview
|
||||
id="namedview73"
|
||||
pagecolor="#767676"
|
||||
bordercolor="#666666"
|
||||
borderopacity="1.0"
|
||||
inkscape:pageshadow="2"
|
||||
inkscape:pageopacity="0"
|
||||
inkscape:pagecheckerboard="true"
|
||||
inkscape:document-units="mm"
|
||||
showgrid="false"
|
||||
showborder="false"
|
||||
inkscape:snap-bbox="true"
|
||||
inkscape:bbox-paths="true"
|
||||
inkscape:bbox-nodes="true"
|
||||
inkscape:snap-bbox-edge-midpoints="true"
|
||||
inkscape:snap-bbox-midpoints="true"
|
||||
inkscape:object-paths="true"
|
||||
inkscape:snap-intersection-paths="true"
|
||||
inkscape:zoom="1.4142136"
|
||||
inkscape:cx="-90.271136"
|
||||
inkscape:cy="-1233.1209"
|
||||
inkscape:window-width="1864"
|
||||
inkscape:window-height="1027"
|
||||
inkscape:window-x="56"
|
||||
inkscape:window-y="25"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="layer1"
|
||||
inkscape:snap-global="false"
|
||||
showguides="false"
|
||||
inkscape:guide-bbox="true"
|
||||
inkscape:document-rotation="0"
|
||||
units="px">
|
||||
<sodipodi:guide
|
||||
position="-154.76097,641.11689"
|
||||
orientation="0,-1"
|
||||
id="guide21118" />
|
||||
<sodipodi:guide
|
||||
position="-60.286487,633.36619"
|
||||
orientation="0,-1"
|
||||
id="guide21120" />
|
||||
</sodipodi:namedview>
|
||||
<defs
|
||||
id="defs68">
|
||||
<linearGradient
|
||||
id="linearGradient4375">
|
||||
<stop
|
||||
style="stop-color:#364e59;stop-opacity:1;"
|
||||
offset="0"
|
||||
id="stop4377" />
|
||||
<stop
|
||||
style="stop-color:#364e59;stop-opacity:0;"
|
||||
offset="1"
|
||||
id="stop4379" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<g
|
||||
inkscape:label="Ebene 1"
|
||||
inkscape:groupmode="layer"
|
||||
id="layer1">
|
||||
<rect
|
||||
style="fill:#1a1a1a;stroke:#000000;stroke-width:0.167903"
|
||||
id="rect880"
|
||||
width="79.8321"
|
||||
height="79.8321"
|
||||
x="-64.03286"
|
||||
y="-375.9097"
|
||||
ry="0" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path3799-2"
|
||||
d="m -24.089585,-372.59579 c -19.986026,0.24336 -36.196903,16.666 -36.196903,36.67011 0,20.00409 16.210877,36.03233 36.196903,35.78912 19.0024236,-0.076 14.5340713,-10.6146 35.538854,-0.85693 -11.50627538,-17.97454 0.390097,-20.36737 0.658079,-35.81316 0,-20.00411 -16.2108788,-36.03235 -36.196911,-35.78914 z"
|
||||
style="fill:#364e59;fill-opacity:1;stroke:none;stroke-width:1.93355"
|
||||
sodipodi:nodetypes="sscccs" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M -54.193871,-325.26419 Z"
|
||||
id="path3846" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M -49.397951,-326.67773 Z"
|
||||
id="path3848" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="m -49.397951,-326.67773 v 0 0"
|
||||
id="path3850" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.01;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
||||
d="m -51.35133,-325.0334 -7.964067,5.98895 z"
|
||||
id="path3965" />
|
||||
<path
|
||||
inkscape:connector-curvature="0"
|
||||
id="path11037"
|
||||
d="m -24.089585,-372.19891 c -19.986026,0.24156 -36.196903,16.54352 -36.196903,36.40062 0,7.86524 2.543315,15.1113 6.857155,20.97971 6.577146,8.94734 11.123515,9.77363 11.123515,9.77363 1.343237,1.78324 10.270932,4.3223 10.270932,4.3223 l 16.791727,-70.86654 -0.468369,-0.33457 c 0.458597,0.26445 0.428277,-0.27515 -8.378035,-0.27515 z"
|
||||
style="fill:#7cc5cc;fill-opacity:1;stroke:none;stroke-width:1.92643"
|
||||
sodipodi:nodetypes="sssccccss" />
|
||||
<path
|
||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
||||
d="M -49.944239,-310.69957 Z"
|
||||
id="path13674" />
|
||||
<g
|
||||
id="g15178"
|
||||
transform="matrix(0.79975737,0,0,0.79975737,53.088959,-63.716396)">
|
||||
<rect
|
||||
style="fill:#364e59;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect15072"
|
||||
width="29.897917"
|
||||
height="6.8791666"
|
||||
x="-334.4964"
|
||||
y="-154.51025"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
style="fill:#364e59;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect15072-5"
|
||||
width="29.897917"
|
||||
height="6.8791666"
|
||||
x="147.63107"
|
||||
y="-334.4964"
|
||||
transform="rotate(-45)"
|
||||
inkscape:transform-center-x="-0.74835017"
|
||||
inkscape:transform-center-y="0.37417525" />
|
||||
</g>
|
||||
<g
|
||||
id="g22468"
|
||||
transform="translate(3.3033974)">
|
||||
<g
|
||||
id="g15178-0"
|
||||
transform="matrix(-0.79975737,0,0,0.79975737,-103.11028,-63.716404)"
|
||||
style="fill:#7cc5cc;fill-opacity:1">
|
||||
<rect
|
||||
style="fill:#7cc5cc;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect15072-2"
|
||||
width="29.897917"
|
||||
height="6.8791666"
|
||||
x="-334.4964"
|
||||
y="-154.51025"
|
||||
transform="rotate(45)" />
|
||||
<rect
|
||||
style="fill:#7cc5cc;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
|
||||
id="rect15072-5-5"
|
||||
width="29.897917"
|
||||
height="6.8791666"
|
||||
x="147.63107"
|
||||
y="-334.4964"
|
||||
transform="rotate(-45)"
|
||||
inkscape:transform-center-x="-0.74835017"
|
||||
inkscape:transform-center-y="0.37417525" />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.5 KiB |
40
benches/get_chat_msgs.rs
Normal file
40
benches/get_chat_msgs.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use async_std::path::Path;
|
||||
|
||||
use criterion::async_executor::AsyncStdExecutor;
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
use deltachat::chat::{self, ChatId};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::context::Context;
|
||||
|
||||
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
|
||||
let id = 100;
|
||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
||||
|
||||
for c in chats.iter().take(10) {
|
||||
black_box(chat::get_chat_msgs(&context, *c, 0).await.ok());
|
||||
}
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
|
||||
// messages, such as your primary account.
|
||||
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
|
||||
let chats: Vec<_> = async_std::task::block_on(async {
|
||||
let context = Context::new((&path).into(), 100).await.unwrap();
|
||||
let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap();
|
||||
let len = chatlist.len();
|
||||
(0..len).map(|i| chatlist.get_chat_id(i).unwrap()).collect()
|
||||
});
|
||||
|
||||
c.bench_function("chat::get_chat_msgs (load messages from 10 chats)", |b| {
|
||||
b.to_async(AsyncStdExecutor)
|
||||
.iter(|| get_chat_msgs_benchmark(black_box(path.as_ref()), black_box(&chats)))
|
||||
});
|
||||
} else {
|
||||
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
27
benches/get_chatlist.rs
Normal file
27
benches/get_chatlist.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use criterion::async_executor::AsyncStdExecutor;
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::context::Context;
|
||||
|
||||
async fn get_chat_list_benchmark(context: &Context) {
|
||||
Chatlist::try_load(context, 0, None, None).await.unwrap();
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
|
||||
// messages, such as your primary account.
|
||||
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
|
||||
let context =
|
||||
async_std::task::block_on(async { Context::new(path.into(), 100).await.unwrap() });
|
||||
c.bench_function("chatlist:try_load (Get Chatlist)", |b| {
|
||||
b.to_async(AsyncStdExecutor)
|
||||
.iter(|| get_chat_list_benchmark(black_box(&context)))
|
||||
});
|
||||
} else {
|
||||
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
|
||||
}
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
84
benches/receive_emails.rs
Normal file
84
benches/receive_emails.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use async_std::{path::PathBuf, task::block_on};
|
||||
use criterion::{
|
||||
async_executor::AsyncStdExecutor, black_box, criterion_group, criterion_main, BatchSize,
|
||||
Criterion,
|
||||
};
|
||||
use deltachat::{
|
||||
config::Config,
|
||||
context::Context,
|
||||
dc_receive_imf::dc_receive_imf,
|
||||
imex::{imex, ImexMode},
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
|
||||
async fn recv_all_emails(context: Context) -> Context {
|
||||
for i in 0..100 {
|
||||
let imf_raw = format!(
|
||||
"Subject: Benchmark
|
||||
Message-ID: Mr.OssSYnOFkhR.{i}@testrun.org
|
||||
Date: Sat, 07 Dec 2019 19:00:27 +0000
|
||||
To: alice@example.com
|
||||
From: sender@testrun.org
|
||||
Chat-Version: 1.0
|
||||
Chat-Disposition-Notification-To: sender@testrun.org
|
||||
Chat-User-Avatar: 0
|
||||
In-Reply-To: Mr.OssSYnOFkhR.{i_dec}@testrun.org
|
||||
MIME-Version: 1.0
|
||||
|
||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||
|
||||
Hello {i}",
|
||||
i = i,
|
||||
i_dec = i - 1,
|
||||
);
|
||||
dc_receive_imf(&context, black_box(imf_raw.as_bytes()), false)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
context
|
||||
}
|
||||
|
||||
async fn create_context() -> Context {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let id = 100;
|
||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
||||
|
||||
let backup: PathBuf = std::env::current_dir()
|
||||
.unwrap()
|
||||
.join("delta-chat-backup.tar")
|
||||
.into();
|
||||
if backup.exists().await {
|
||||
println!("Importing backup");
|
||||
imex(&context, ImexMode::ImportBackup, &backup, None)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let addr = "alice@example.com";
|
||||
context.set_config(Config::Addr, Some(addr)).await.unwrap();
|
||||
context
|
||||
.set_config(Config::ConfiguredAddr, Some(addr))
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
.set_config(Config::Configured, Some("1"))
|
||||
.await
|
||||
.unwrap();
|
||||
context
|
||||
}
|
||||
|
||||
fn criterion_benchmark(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("Receive messages");
|
||||
group.bench_function("Receive 100 simple text msgs", |b| {
|
||||
b.to_async(AsyncStdExecutor).iter_batched(
|
||||
|| block_on(create_context()),
|
||||
|context| recv_all_emails(black_box(context)),
|
||||
BatchSize::LargeInput,
|
||||
);
|
||||
});
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, criterion_benchmark);
|
||||
criterion_main!(benches);
|
||||
@@ -20,6 +20,8 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
c.bench_function("search hello", |b| {
|
||||
b.iter(|| block_on(async { search_benchmark(black_box(&path)).await }))
|
||||
});
|
||||
} else {
|
||||
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.70.0"
|
||||
version = "1.81.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
# Delta Chat C Interface
|
||||
|
||||
## Installation
|
||||
|
||||
see `Installing libdeltachat system wide` in [../README.md](../README.md)
|
||||
|
||||
## Documentation
|
||||
|
||||
To generate the C Interface documentation,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,5 @@
|
||||
use crate::chat::ChatItem;
|
||||
use crate::constants::{DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1};
|
||||
use crate::constants::DC_MSG_ID_DAYMARKER;
|
||||
use crate::location::Location;
|
||||
use crate::message::MsgId;
|
||||
|
||||
@@ -18,7 +18,6 @@ impl dc_array_t {
|
||||
Self::MsgIds(array) => array[index].to_u32(),
|
||||
Self::Chat(array) => match array[index] {
|
||||
ChatItem::Message { msg_id } => msg_id.to_u32(),
|
||||
ChatItem::Marker1 => DC_MSG_ID_MARKER1,
|
||||
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
|
||||
},
|
||||
Self::Locations(array) => array[index].location_id,
|
||||
@@ -31,7 +30,6 @@ impl dc_array_t {
|
||||
Self::MsgIds(_) => None,
|
||||
Self::Chat(array) => array.get(index).and_then(|item| match item {
|
||||
ChatItem::Message { .. } => None,
|
||||
ChatItem::Marker1 { .. } => None,
|
||||
ChatItem::DayMarker { timestamp } => Some(*timestamp),
|
||||
}),
|
||||
Self::Locations(array) => array.get(index).map(|location| location.timestamp),
|
||||
|
||||
@@ -27,15 +27,17 @@ use async_std::sync::RwLock;
|
||||
use async_std::task::{block_on, spawn};
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
use rand::Rng;
|
||||
|
||||
use deltachat::chat::{ChatId, ChatVisibility, MuteDuration, ProtectionStatus};
|
||||
use deltachat::constants::DC_MSG_ID_LAST_SPECIAL;
|
||||
use deltachat::contact::{Contact, Origin};
|
||||
use deltachat::contact::{Contact, ContactId, Origin};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::key::DcKey;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
use deltachat::{accounts::Accounts, log::LogExt};
|
||||
|
||||
@@ -75,7 +77,6 @@ pub unsafe extern "C" fn dc_context_new(
|
||||
}
|
||||
|
||||
let ctx = if blobdir.is_null() || *blobdir == 0 {
|
||||
use rand::Rng;
|
||||
// generate random ID as this functionality is not yet available on the C-api.
|
||||
let id = rand::thread_rng().gen();
|
||||
block_on(Context::new(as_path(dbfile).to_path_buf().into(), id))
|
||||
@@ -86,12 +87,63 @@ pub unsafe extern "C" fn dc_context_new(
|
||||
match ctx {
|
||||
Ok(ctx) => Box::into_raw(Box::new(ctx)),
|
||||
Err(err) => {
|
||||
eprintln!("failed to create context: {}", err);
|
||||
eprintln!("failed to create context: {:#}", err);
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *mut dc_context_t {
|
||||
setup_panic!();
|
||||
|
||||
if dbfile.is_null() {
|
||||
eprintln!("ignoring careless call to dc_context_new_closed()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let id = rand::thread_rng().gen();
|
||||
match block_on(Context::new_closed(
|
||||
as_path(dbfile).to_path_buf().into(),
|
||||
id,
|
||||
)) {
|
||||
Ok(context) => Box::into_raw(Box::new(context)),
|
||||
Err(err) => {
|
||||
eprintln!("failed to create context: {:#}", err);
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_context_open(
|
||||
context: *mut dc_context_t,
|
||||
passphrase: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_context_open()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let ctx = &*context;
|
||||
let passphrase = to_string_lossy(passphrase);
|
||||
block_on(ctx.open(passphrase))
|
||||
.log_err(ctx, "dc_context_open() failed")
|
||||
.map(|b| b as libc::c_int)
|
||||
.unwrap_or(0)
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_context_is_open()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let ctx = &*context;
|
||||
block_on(ctx.is_open()) as libc::c_int
|
||||
}
|
||||
|
||||
/// Release the context structure.
|
||||
///
|
||||
/// This function releases the memory of the `dc_context_t` structure.
|
||||
@@ -441,14 +493,17 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
|
||||
EventType::ContactsChanged(id) | EventType::LocationChanged(id) => {
|
||||
let id = id.unwrap_or_default();
|
||||
id as libc::c_int
|
||||
id.to_u32() as libc::c_int
|
||||
}
|
||||
EventType::ConfigureProgress { progress, .. } | EventType::ImexProgress(progress) => {
|
||||
*progress as libc::c_int
|
||||
}
|
||||
EventType::ImexFileWritten(_) => 0,
|
||||
EventType::SecurejoinInviterProgress { contact_id, .. }
|
||||
| EventType::SecurejoinJoinerProgress { contact_id, .. } => *contact_id as libc::c_int,
|
||||
| EventType::SecurejoinJoinerProgress { contact_id, .. } => {
|
||||
contact_id.to_u32() as libc::c_int
|
||||
}
|
||||
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,8 +535,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ImexFileWritten(_)
|
||||
| EventType::MsgsNoticed(_)
|
||||
| EventType::ConnectivityChanged
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::ChatModified(_) => 0,
|
||||
| EventType::SelfavatarChanged => 0,
|
||||
EventType::ChatModified(_) => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::IncomingMsg { msg_id, .. }
|
||||
| EventType::MsgDelivered { msg_id, .. }
|
||||
@@ -490,6 +545,10 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
EventType::SecurejoinInviterProgress { progress, .. }
|
||||
| EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
|
||||
EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
|
||||
EventType::WebxdcStatusUpdate {
|
||||
status_update_serial,
|
||||
..
|
||||
} => status_update_serial.to_u32() as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -531,6 +590,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::SecurejoinJoinerProgress { .. }
|
||||
| EventType::ConnectivityChanged
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::WebxdcStatusUpdate { .. }
|
||||
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
@@ -597,7 +657,7 @@ pub unsafe extern "C" fn dc_get_next_event(events: *mut dc_event_emitter_t) -> *
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_stop_io(context: *mut dc_context_t) {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_shutdown()");
|
||||
eprintln!("ignoring careless call to dc_stop_io()");
|
||||
return;
|
||||
}
|
||||
let ctx = &*context;
|
||||
@@ -660,7 +720,11 @@ pub unsafe extern "C" fn dc_get_chatlist(
|
||||
let ctx = &*context;
|
||||
let qs = to_opt_string_lossy(query_str);
|
||||
|
||||
let qi = if query_id == 0 { None } else { Some(query_id) };
|
||||
let qi = if query_id == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(ContactId::new(query_id))
|
||||
};
|
||||
|
||||
block_on(async move {
|
||||
match chatlist::Chatlist::try_load(ctx, flags as usize, qs.as_deref(), qi)
|
||||
@@ -688,7 +752,7 @@ pub unsafe extern "C" fn dc_create_chat_by_contact_id(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
ChatId::create_for_contact(ctx, contact_id)
|
||||
ChatId::create_for_contact(ctx, ContactId::new(contact_id))
|
||||
.await
|
||||
.log_err(ctx, "Failed to create chat from contact_id")
|
||||
.map(|id| id.to_u32())
|
||||
@@ -708,7 +772,7 @@ pub unsafe extern "C" fn dc_get_chat_id_by_contact_id(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
ChatId::lookup_by_contact(ctx, contact_id)
|
||||
ChatId::lookup_by_contact(ctx, ContactId::new(contact_id))
|
||||
.await
|
||||
.log_err(ctx, "Failed to get chat for contact_id")
|
||||
.unwrap_or_default() // unwraps the Result
|
||||
@@ -820,6 +884,48 @@ pub unsafe extern "C" fn dc_send_videochat_invitation(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
json: *const libc::c_char,
|
||||
descr: *const libc::c_char,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_send_webxdc_status_update()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(ctx.send_webxdc_status_update(
|
||||
MsgId::new(msg_id),
|
||||
&to_string_lossy(json),
|
||||
&to_string_lossy(descr),
|
||||
))
|
||||
.log_err(ctx, "Failed to send webxdc update")
|
||||
.is_ok() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_webxdc_status_updates(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
last_known_serial: u32,
|
||||
) -> *mut libc::c_char {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_webxdc_status_updates()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(ctx.get_webxdc_status_updates(
|
||||
MsgId::new(msg_id),
|
||||
StatusUpdateSerial::new(last_known_serial),
|
||||
))
|
||||
.unwrap_or_else(|_| "".to_string())
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_draft(
|
||||
context: *mut dc_context_t,
|
||||
@@ -921,22 +1027,17 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
flags: u32,
|
||||
marker1before: u32,
|
||||
_marker1before: u32,
|
||||
) -> *mut dc_array::dc_array_t {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_chat_msgs()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
let marker_flag = if marker1before <= DC_MSG_ID_LAST_SPECIAL {
|
||||
None
|
||||
} else {
|
||||
Some(MsgId::new(marker1before))
|
||||
};
|
||||
|
||||
block_on(async move {
|
||||
Box::into_raw(Box::new(
|
||||
chat::get_chat_msgs(ctx, ChatId::new(chat_id), flags, marker_flag)
|
||||
chat::get_chat_msgs(ctx, ChatId::new(chat_id), flags)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to get chat msgs")
|
||||
.into(),
|
||||
@@ -1243,7 +1344,10 @@ pub unsafe extern "C" fn dc_get_chat_contacts(
|
||||
let arr = dc_array_t::from(
|
||||
chat::get_chat_contacts(ctx, ChatId::new(chat_id))
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "Failed get_chat_contacts"),
|
||||
.unwrap_or_log_default(ctx, "Failed get_chat_contacts")
|
||||
.iter()
|
||||
.map(|id| id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
);
|
||||
Box::into_raw(Box::new(arr))
|
||||
})
|
||||
@@ -1353,7 +1457,7 @@ pub unsafe extern "C" fn dc_is_contact_in_chat(
|
||||
block_on(chat::is_contact_in_chat(
|
||||
ctx,
|
||||
ChatId::new(chat_id),
|
||||
contact_id,
|
||||
ContactId::new(contact_id),
|
||||
))
|
||||
.log_err(ctx, "is_contact_in_chat failed")
|
||||
.unwrap_or_default() as libc::c_int
|
||||
@@ -1374,7 +1478,7 @@ pub unsafe extern "C" fn dc_add_contact_to_chat(
|
||||
block_on(chat::add_contact_to_chat(
|
||||
ctx,
|
||||
ChatId::new(chat_id),
|
||||
contact_id,
|
||||
ContactId::new(contact_id),
|
||||
))
|
||||
.log_err(ctx, "Failed to add contact")
|
||||
.is_ok() as libc::c_int
|
||||
@@ -1395,7 +1499,7 @@ pub unsafe extern "C" fn dc_remove_contact_from_chat(
|
||||
block_on(chat::remove_contact_from_chat(
|
||||
ctx,
|
||||
ChatId::new(chat_id),
|
||||
contact_id,
|
||||
ContactId::new(contact_id),
|
||||
))
|
||||
.log_err(ctx, "Failed to remove contact")
|
||||
.is_ok() as libc::c_int
|
||||
@@ -1642,6 +1746,27 @@ pub unsafe extern "C" fn dc_forward_msgs(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_resend_msgs(
|
||||
context: *mut dc_context_t,
|
||||
msg_ids: *const u32,
|
||||
msg_cnt: libc::c_int,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() || msg_ids.is_null() || msg_cnt <= 0 {
|
||||
eprintln!("ignoring careless call to dc_resend_msgs()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
let msg_ids = convert_and_prune_message_ids(msg_ids, msg_cnt);
|
||||
|
||||
if let Err(err) = block_on(chat::resend_msgs(ctx, &msg_ids)) {
|
||||
error!(ctx, "Resending failed: {}", err);
|
||||
0
|
||||
} else {
|
||||
1
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_markseen_msgs(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1730,7 +1855,8 @@ pub unsafe extern "C" fn dc_lookup_contact_id_by_addr(
|
||||
Contact::lookup_id_by_addr(ctx, &to_string_lossy(addr), Origin::IncomingReplyTo)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to lookup id")
|
||||
.unwrap_or(0)
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1750,6 +1876,7 @@ pub unsafe extern "C" fn dc_create_contact(
|
||||
block_on(async move {
|
||||
Contact::create(ctx, &name, &to_string_lossy(addr))
|
||||
.await
|
||||
.map(|id| id.to_u32())
|
||||
.unwrap_or(0)
|
||||
})
|
||||
}
|
||||
@@ -1787,8 +1914,10 @@ pub unsafe extern "C" fn dc_get_contacts(
|
||||
let query = to_opt_string_lossy(query);
|
||||
|
||||
block_on(async move {
|
||||
match Contact::get_all(ctx, flags, query).await {
|
||||
Ok(contacts) => Box::into_raw(Box::new(dc_array_t::from(contacts))),
|
||||
match Contact::get_all(ctx, flags, query.as_deref()).await {
|
||||
Ok(contacts) => Box::into_raw(Box::new(dc_array_t::from(
|
||||
contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>(),
|
||||
))),
|
||||
Err(_) => ptr::null_mut(),
|
||||
}
|
||||
})
|
||||
@@ -1825,7 +1954,10 @@ pub unsafe extern "C" fn dc_get_blocked_contacts(
|
||||
Contact::get_all_blocked(ctx)
|
||||
.await
|
||||
.log_err(ctx, "Can't get blocked contacts")
|
||||
.unwrap_or_default(),
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|id| id.to_u32())
|
||||
.collect::<Vec<u32>>(),
|
||||
)))
|
||||
})
|
||||
}
|
||||
@@ -1836,7 +1968,8 @@ pub unsafe extern "C" fn dc_block_contact(
|
||||
contact_id: u32,
|
||||
block: libc::c_int,
|
||||
) {
|
||||
if context.is_null() || contact_id <= constants::DC_CONTACT_ID_LAST_SPECIAL as u32 {
|
||||
let contact_id = ContactId::new(contact_id);
|
||||
if context.is_null() || contact_id.is_special() {
|
||||
eprintln!("ignoring careless call to dc_block_contact()");
|
||||
return;
|
||||
}
|
||||
@@ -1866,7 +1999,7 @@ pub unsafe extern "C" fn dc_get_contact_encrinfo(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
Contact::get_encrinfo(ctx, contact_id)
|
||||
Contact::get_encrinfo(ctx, ContactId::new(contact_id))
|
||||
.await
|
||||
.map(|s| s.strdup())
|
||||
.unwrap_or_else(|e| {
|
||||
@@ -1881,7 +2014,8 @@ pub unsafe extern "C" fn dc_delete_contact(
|
||||
context: *mut dc_context_t,
|
||||
contact_id: u32,
|
||||
) -> libc::c_int {
|
||||
if context.is_null() || contact_id <= constants::DC_CONTACT_ID_LAST_SPECIAL as u32 {
|
||||
let contact_id = ContactId::new(contact_id);
|
||||
if context.is_null() || contact_id.is_special() {
|
||||
eprintln!("ignoring careless call to dc_delete_contact()");
|
||||
return 0;
|
||||
}
|
||||
@@ -1907,7 +2041,7 @@ pub unsafe extern "C" fn dc_get_contact(
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
Contact::get_by_id(ctx, contact_id)
|
||||
Contact::get_by_id(ctx, ContactId::new(contact_id))
|
||||
.await
|
||||
.map(|contact| Box::into_raw(Box::new(ContactWrapper { context, contact })))
|
||||
.unwrap_or_else(|_| ptr::null_mut())
|
||||
@@ -1919,7 +2053,7 @@ pub unsafe extern "C" fn dc_imex(
|
||||
context: *mut dc_context_t,
|
||||
what_raw: libc::c_int,
|
||||
param1: *const libc::c_char,
|
||||
_param2: *const libc::c_char,
|
||||
param2: *const libc::c_char,
|
||||
) {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_imex()");
|
||||
@@ -1932,12 +2066,13 @@ pub unsafe extern "C" fn dc_imex(
|
||||
return;
|
||||
}
|
||||
};
|
||||
let passphrase = to_opt_string_lossy(param2);
|
||||
|
||||
let ctx = &*context;
|
||||
|
||||
if let Some(param1) = to_opt_string_lossy(param1) {
|
||||
spawn(async move {
|
||||
imex::imex(ctx, what, param1.as_ref())
|
||||
imex::imex(ctx, what, param1.as_ref(), passphrase)
|
||||
.await
|
||||
.log_err(ctx, "IMEX failed")
|
||||
});
|
||||
@@ -2329,7 +2464,7 @@ pub unsafe extern "C" fn dc_array_get_contact_id(
|
||||
return 0;
|
||||
}
|
||||
|
||||
(*array).get_location(index).contact_id
|
||||
(*array).get_location(index).contact_id.to_u32()
|
||||
}
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_array_get_msg_id(
|
||||
@@ -2443,7 +2578,14 @@ pub unsafe extern "C" fn dc_chatlist_get_chat_id(
|
||||
return 0;
|
||||
}
|
||||
let ffi_list = &*chatlist;
|
||||
ffi_list.list.get_chat_id(index as usize).to_u32()
|
||||
let ctx = &*ffi_list.context;
|
||||
match ffi_list.list.get_chat_id(index as usize) {
|
||||
Ok(chat_id) => chat_id.to_u32(),
|
||||
Err(err) => {
|
||||
warn!(ctx, "get_chat_id failed: {}", err);
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2838,7 +2980,7 @@ pub unsafe extern "C" fn dc_msg_get_from_id(msg: *mut dc_msg_t) -> u32 {
|
||||
return 0;
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
ffi_msg.message.get_from_id()
|
||||
ffi_msg.message.get_from_id().to_u32()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2960,6 +3102,61 @@ pub unsafe extern "C" fn dc_msg_get_filename(msg: *mut dc_msg_t) -> *mut libc::c
|
||||
ffi_msg.message.get_filename().unwrap_or_default().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_webxdc_blob(
|
||||
msg: *mut dc_msg_t,
|
||||
filename: *const libc::c_char,
|
||||
ret_bytes: *mut libc::size_t,
|
||||
) -> *mut libc::c_char {
|
||||
if msg.is_null() || filename.is_null() || ret_bytes.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_webxdc_blob()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
let ctx = &*ffi_msg.context;
|
||||
let blob = block_on(async move {
|
||||
ffi_msg
|
||||
.message
|
||||
.get_webxdc_blob(ctx, &to_string_lossy(filename))
|
||||
.await
|
||||
});
|
||||
match blob {
|
||||
Ok(blob) => {
|
||||
*ret_bytes = blob.len();
|
||||
let ptr = libc::malloc(*ret_bytes);
|
||||
libc::memcpy(ptr, blob.as_ptr() as *mut libc::c_void, *ret_bytes);
|
||||
ptr as *mut libc::c_char
|
||||
}
|
||||
Err(err) => {
|
||||
eprintln!("failed read blob from archive: {}", err);
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_webxdc_info(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_webxdc_info()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
let ctx = &*ffi_msg.context;
|
||||
|
||||
block_on(async move {
|
||||
let info = match ffi_msg.message.get_webxdc_info(ctx).await {
|
||||
Ok(info) => info,
|
||||
Err(err) => {
|
||||
error!(ctx, "dc_msg_get_webxdc_info() failed to get info: {}", err);
|
||||
return "".strdup();
|
||||
}
|
||||
};
|
||||
serde_json::to_string(&info)
|
||||
.unwrap_or_log_default(ctx, "dc_msg_get_webxdc_info() failed to serialise to json")
|
||||
.strdup()
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_filemime(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
@@ -3372,17 +3569,21 @@ pub unsafe extern "C" fn dc_msg_set_quote(msg: *mut dc_msg_t, quote: *const dc_m
|
||||
return;
|
||||
}
|
||||
let ffi_msg = &mut *msg;
|
||||
let ffi_quote = &*quote;
|
||||
|
||||
if ffi_msg.context != ffi_quote.context {
|
||||
eprintln!("ignoring attempt to quote message from a different context");
|
||||
return;
|
||||
}
|
||||
let quote_msg = if quote.is_null() {
|
||||
None
|
||||
} else {
|
||||
let ffi_quote = &*quote;
|
||||
if ffi_msg.context != ffi_quote.context {
|
||||
eprintln!("ignoring attempt to quote message from a different context");
|
||||
return;
|
||||
}
|
||||
Some(&ffi_quote.message)
|
||||
};
|
||||
|
||||
block_on(async move {
|
||||
ffi_msg
|
||||
.message
|
||||
.set_quote(&*ffi_msg.context, &ffi_quote.message)
|
||||
.set_quote(&*ffi_msg.context, quote_msg)
|
||||
.await
|
||||
.log_err(&*ffi_msg.context, "failed to set quote")
|
||||
.ok();
|
||||
@@ -3425,6 +3626,29 @@ pub unsafe extern "C" fn dc_msg_get_quoted_msg(msg: *const dc_msg_t) -> *mut dc_
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_parent(msg: *const dc_msg_t) -> *mut dc_msg_t {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_parent()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ffi_msg: &MessageWrapper = &*msg;
|
||||
let context = &*ffi_msg.context;
|
||||
let res = block_on(async move {
|
||||
ffi_msg
|
||||
.message
|
||||
.parent(context)
|
||||
.await
|
||||
.log_err(context, "failed to get parent message")
|
||||
.unwrap_or(None)
|
||||
});
|
||||
|
||||
match res {
|
||||
Some(message) => Box::into_raw(Box::new(MessageWrapper { context, message })),
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_force_plaintext(msg: *mut dc_msg_t) {
|
||||
if msg.is_null() {
|
||||
@@ -3467,7 +3691,7 @@ pub unsafe extern "C" fn dc_contact_get_id(contact: *mut dc_contact_t) -> u32 {
|
||||
return 0;
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.get_id()
|
||||
ffi_contact.contact.get_id().to_u32()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3744,6 +3968,25 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
|
||||
}
|
||||
let addr = to_string_lossy(addr);
|
||||
|
||||
let ctx = &*context;
|
||||
|
||||
match block_on(provider::get_provider_info(ctx, addr.as_str(), true)) {
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
|
||||
context: *const dc_context_t,
|
||||
addr: *const libc::c_char,
|
||||
) -> *const dc_provider_t {
|
||||
if context.is_null() || addr.is_null() {
|
||||
eprintln!("ignoring careless call to dc_provider_new_from_email_with_dns()");
|
||||
return ptr::null();
|
||||
}
|
||||
let addr = to_string_lossy(addr);
|
||||
|
||||
let ctx = &*context;
|
||||
let socks5_enabled = block_on(async move {
|
||||
ctx.get_config_bool(config::Config::Socks5Enabled)
|
||||
@@ -3954,6 +4197,30 @@ pub unsafe extern "C" fn dc_accounts_add_account(accounts: *mut dc_accounts_t) -
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_add_closed_account(accounts: *mut dc_accounts_t) -> u32 {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_add_closed_account()");
|
||||
return 0;
|
||||
}
|
||||
|
||||
let accounts = &mut *accounts;
|
||||
|
||||
block_on(async move {
|
||||
let mut accounts = accounts.write().await;
|
||||
match accounts.add_closed_account().await {
|
||||
Ok(id) => id,
|
||||
Err(err) => {
|
||||
accounts.emit_event(EventType::Error(format!(
|
||||
"Failed to add account: {:#}",
|
||||
err
|
||||
)));
|
||||
0
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_remove_account(
|
||||
accounts: *mut dc_accounts_t,
|
||||
|
||||
@@ -111,19 +111,19 @@ impl Lot {
|
||||
match self {
|
||||
Self::Summary(_) => Default::default(),
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::AskVerifyContact { contact_id, .. } => *contact_id,
|
||||
Qr::AskVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::AskVerifyGroup { .. } => Default::default(),
|
||||
Qr::FprOk { contact_id } => *contact_id,
|
||||
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default(),
|
||||
Qr::FprOk { contact_id } => contact_id.to_u32(),
|
||||
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
|
||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Addr { contact_id } => *contact_id,
|
||||
Qr::Addr { contact_id } => contact_id.to_u32(),
|
||||
Qr::Url { .. } => Default::default(),
|
||||
Qr::Text { .. } => Default::default(),
|
||||
Qr::WithdrawVerifyContact { contact_id, .. } => *contact_id,
|
||||
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::WithdrawVerifyGroup { .. } => Default::default(),
|
||||
Qr::ReviveVerifyContact { contact_id, .. } => *contact_id,
|
||||
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::ReviveVerifyGroup { .. } => Default::default(),
|
||||
},
|
||||
Self::Error(_) => Default::default(),
|
||||
|
||||
198
draft/webxdc-dev-reference.md
Normal file
198
draft/webxdc-dev-reference.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# Webxdc Developer Reference
|
||||
|
||||
(This document may eventually be merged with the [webxdc guidebook](https://deltachat.github.io/webxdc_docs/), where you may currently find other useful information.)
|
||||
|
||||
## Webxdc File Format
|
||||
|
||||
- a **Webxdc** is a **ZIP-file** with the extension `.xdc`
|
||||
- the ZIP-file must use the default compression methods as of RFC 1950,
|
||||
this is "Deflate" or "Store"
|
||||
- the ZIP-file must contain at least the file `index.html`
|
||||
- if the Webxdc is started, `index.html` is opened in a restricted webview
|
||||
that allow accessing resources only from the ZIP-file
|
||||
|
||||
|
||||
## Webxdc API
|
||||
|
||||
There are some additional APIs available once `webxdc.js` is included
|
||||
(the file will be provided by the concrete implementations,
|
||||
no need to add `webxdc.js` to your ZIP-file):
|
||||
|
||||
```html
|
||||
<script src="webxdc.js"></script>
|
||||
```
|
||||
|
||||
### sendUpdate()
|
||||
|
||||
```js
|
||||
window.webxdc.sendUpdate(update, descr);
|
||||
```
|
||||
|
||||
A Webxdc is usually shared in a chat and run independently on each peer.
|
||||
To get a shared state, the peers use `sendUpdate()` to send updates to each other.
|
||||
|
||||
- `update`: an object with the following properties:
|
||||
- `update.payload`: any javascript primitive, array or object.
|
||||
- `update.info`: optional, short, informational message that will be added to the chat,
|
||||
eg. "Alice voted" or "Bob scored 123 in MyGame";
|
||||
usually only one line of text is shown,
|
||||
use this option sparingly to not spam the chat.
|
||||
- `update.document`: optional, name of the document in edit,
|
||||
must not be used eg. in games where the Webxdc does not create documents
|
||||
- `update.summary`: optional, short text, shown beside Webxdc icon;
|
||||
it is recommended to use some aggregated value, eg. "8 votes", "Highscore: 123"
|
||||
|
||||
- `descr`: short, human-readable description what this update is about.
|
||||
this is shown eg. as a fallback text in an email program.
|
||||
|
||||
All peers, including the sending one,
|
||||
will receive the update by the callback given to `setUpdateListener()`.
|
||||
|
||||
There are situations where the user cannot send messages to a chat,
|
||||
eg. if the webxdc instance comes as a contact request or if the user has left a group.
|
||||
In these cases, you can still call `sendUpdate()`,
|
||||
however, the update won't be sent to other peers
|
||||
and you won't get the update by `setUpdateListener()`.
|
||||
|
||||
|
||||
### setUpdateListener()
|
||||
|
||||
```js
|
||||
let promise = window.webxdc.setUpdateListener((update) => {}, serial);
|
||||
```
|
||||
|
||||
With `setUpdateListener()` you define a callback that receives the updates
|
||||
sent by `sendUpdate()`. The callback is called for updates sent by you or other peers.
|
||||
The `serial` specifies the last serial that you know about (defaults to 0).
|
||||
The returned promise resolves when the listener has processed all the update messages known at the time when `setUpdateListener` was called.
|
||||
|
||||
Each `update` which is passed to the callback comes with the following properties:
|
||||
|
||||
- `update.payload`: equals the payload given to `sendUpdate()`
|
||||
|
||||
- `update.serial`: the serial number of this update.
|
||||
Serials are larger `0` and newer serials have higher numbers.
|
||||
There may be gaps in the serials
|
||||
and it is not guaranteed that the next serial is exactly incremented by one.
|
||||
|
||||
- `update.max_serial`: the maximum serial currently known.
|
||||
If `max_serial` equals `serial` this update is the last update (until new network messages arrive).
|
||||
|
||||
- `update.info`: optional, short, informational message (see `sendUpdate()`)
|
||||
|
||||
- `update.document`: optional, document name as set by the sender, (see `sendUpdate()`),
|
||||
implementations show the document name eg. beside the app icon or in the title bar
|
||||
|
||||
- `update.summary`: optional, short text, shown beside icon (see `sendUpdate()`)
|
||||
|
||||
|
||||
### selfAddr
|
||||
|
||||
```js
|
||||
window.webxdc.selfAddr
|
||||
```
|
||||
|
||||
Property with the peer's own address.
|
||||
This is esp. useful if you want to differ between different peers -
|
||||
just send the address along with the payload,
|
||||
and, if needed, compare the payload addresses against selfAddr() later on.
|
||||
|
||||
|
||||
### selfName
|
||||
|
||||
```js
|
||||
window.webxdc.selfName
|
||||
```
|
||||
|
||||
Property with the peer's own name.
|
||||
This is name chosen by the user in their settings,
|
||||
if there is nothing set, that defaults to the peer's address.
|
||||
|
||||
|
||||
## manifest.toml
|
||||
|
||||
If the ZIP-file contains a `manifest.toml` in its root directory,
|
||||
some basic information are read and used from there.
|
||||
|
||||
the `manifest.toml` has the following format
|
||||
|
||||
```toml
|
||||
name = "My Name"
|
||||
source_code_url = "https://example.org/orga/repo"
|
||||
```
|
||||
|
||||
- `name` - The name of the Webxdc.
|
||||
If no name is set or if there is no manifest, the filename is used as the Webxdc name.
|
||||
|
||||
- `source_code_url` - Optional URL where the source code of the Webxdc and maybe other information can be found.
|
||||
UI may make the url accessible via a "Help" menu in the Webxdc window.
|
||||
|
||||
|
||||
## Webxdc Icon
|
||||
|
||||
If the ZIP-root contains an `icon.png` or `icon.jpg`,
|
||||
these files are used as the icon for the Webxdc.
|
||||
The icon should be a square at reasonable width/height;
|
||||
round corners etc. will be added by the implementations as needed.
|
||||
If no icon is set, a default icon will be used.
|
||||
|
||||
|
||||
## Webxdc Examples
|
||||
|
||||
The following example shows an input field and every input is show on all peers.
|
||||
|
||||
```html
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8"/>
|
||||
<script src="webxdc.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<input id="input" type="text"/>
|
||||
<a href="" onclick="sendMsg(); return false;">Send</a>
|
||||
<p id="output"></p>
|
||||
<script>
|
||||
|
||||
function sendMsg() {
|
||||
msg = document.getElementById("input").value;
|
||||
window.webxdc.sendUpdate({payload: msg}, 'Someone typed "'+msg+'".');
|
||||
}
|
||||
|
||||
function receiveUpdate(update) {
|
||||
document.getElementById('output').innerHTML += update.payload + "<br>";
|
||||
}
|
||||
|
||||
window.webxdc.setUpdateListener(receiveUpdate, 0);
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
[Webxdc Development Tool](https://github.com/deltachat/webxdc-dev)
|
||||
offers an **Webxdc Simulator** that can be used in many browsers without any installation needed.
|
||||
You can also use that repository as a template for your own Webxdc -
|
||||
just clone and start adapting things to your need.
|
||||
|
||||
|
||||
### Advanced Examples
|
||||
|
||||
- [2048](https://github.com/adbenitez/2048.xdc)
|
||||
- [Draw](https://github.com/adbenitez/draw.xdc)
|
||||
- [Poll](https://github.com/r10s/webxdc-poll/)
|
||||
- [Tic Tac Toe](https://github.com/Simon-Laux/tictactoe.xdc)
|
||||
- Even more with [Topic #webxdc on Github](https://github.com/topics/webxdc) or in the [webxdc GitHub organization](https://github.com/webxdc)
|
||||
|
||||
|
||||
## Closing Remarks
|
||||
|
||||
- older devices might not have the newest js features in their webview,
|
||||
you may want to transpile your code down to an older js version eg. with https://babeljs.io
|
||||
- viewport and scaling features are implementation specific,
|
||||
if you want to have an explicit behavior, you can add eg.
|
||||
`<meta name="viewport" content="initial-scale=1; user-scalable=no">` to your Webxdc
|
||||
- the `<title>` tag should not be used and its content is usually not displayed;
|
||||
instead, use the `name` property from `manifest.toml`
|
||||
- there are tons of ideas for enhancements of the API and the file format,
|
||||
eg. in the future, we will may define icon- and manifest-files,
|
||||
allow to aggregate the state or add metadata.
|
||||
@@ -17,11 +17,10 @@ use deltachat::download::DownloadState;
|
||||
use deltachat::imex::*;
|
||||
use deltachat::location;
|
||||
use deltachat::log::LogExt;
|
||||
use deltachat::message::{self, Message, MessageState, MsgId};
|
||||
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
use deltachat::sql;
|
||||
use deltachat::EventType;
|
||||
use deltachat::{config, provider};
|
||||
use std::fs;
|
||||
use std::time::{Duration, SystemTime};
|
||||
@@ -84,6 +83,7 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
context.sql().config_cache().write().await.clear();
|
||||
context
|
||||
.sql()
|
||||
.execute("DELETE FROM leftgrps;", paramsv![])
|
||||
@@ -92,16 +92,13 @@ async fn reset_tables(context: &Context, bits: i32) {
|
||||
println!("(8) Rest but server config reset.");
|
||||
}
|
||||
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
context.emit_msgs_changed_without_ids();
|
||||
}
|
||||
|
||||
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> {
|
||||
let data = dc_read_file(context, filename).await?;
|
||||
|
||||
if let Err(err) = dc_receive_imf(context, &data, "import", false).await {
|
||||
if let Err(err) = dc_receive_imf(context, &data, false).await {
|
||||
println!("dc_receive_imf errored: {:?}", err);
|
||||
}
|
||||
Ok(())
|
||||
@@ -163,10 +160,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
}
|
||||
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
|
||||
if read_cnt > 0 {
|
||||
context.emit_event(EventType::MsgsChanged {
|
||||
chat_id: ChatId::new(0),
|
||||
msg_id: MsgId::new(0),
|
||||
});
|
||||
context.emit_msgs_changed_without_ids();
|
||||
}
|
||||
true
|
||||
}
|
||||
@@ -209,7 +203,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
||||
contact_id,
|
||||
msgtext.unwrap_or_default(),
|
||||
if msg.has_html() { "[HAS-HTML]️" } else { "" },
|
||||
if msg.get_from_id() == 1 {
|
||||
if msg.get_from_id() == ContactId::SELF {
|
||||
""
|
||||
} else if msg.get_state() == MessageState::InSeen {
|
||||
"[SEEN]"
|
||||
@@ -267,9 +261,8 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn log_contactlist(context: &Context, contacts: &[u32]) -> Result<()> {
|
||||
async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()> {
|
||||
for contact_id in contacts {
|
||||
let line;
|
||||
let mut line2 = "".to_string();
|
||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||
let name = contact.get_display_name();
|
||||
@@ -284,24 +277,20 @@ async fn log_contactlist(context: &Context, contacts: &[u32]) -> Result<()> {
|
||||
} else {
|
||||
""
|
||||
};
|
||||
line = format!(
|
||||
let line = format!(
|
||||
"{}{} <{}>",
|
||||
if !name.is_empty() {
|
||||
&name
|
||||
name
|
||||
} else {
|
||||
"<name unset>"
|
||||
},
|
||||
verified_str,
|
||||
if !addr.is_empty() {
|
||||
&addr
|
||||
} else {
|
||||
"addr unset"
|
||||
}
|
||||
if !addr.is_empty() { addr } else { "addr unset" }
|
||||
);
|
||||
let peerstate = Peerstate::from_addr(context, &addr)
|
||||
let peerstate = Peerstate::from_addr(context, addr)
|
||||
.await
|
||||
.expect("peerstate error");
|
||||
if peerstate.is_some() && *contact_id != 1 {
|
||||
if peerstate.is_some() && *contact_id != ContactId::SELF {
|
||||
line2 = format!(
|
||||
", prefer-encrypt={}",
|
||||
peerstate.as_ref().unwrap().prefer_encrypt
|
||||
@@ -387,6 +376,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
sendfile <file> [<text>]\n\
|
||||
sendhtml <file for html-part> [<text for plain-part>]\n\
|
||||
sendsyncmsg\n\
|
||||
sendupdate <msg-id> <json status update>\n\
|
||||
videochat\n\
|
||||
draft [<text>]\n\
|
||||
devicemsg <text>\n\
|
||||
@@ -409,6 +399,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
html <msg-id>\n\
|
||||
listfresh\n\
|
||||
forward <msg-id> <chat-id>\n\
|
||||
resend <msg-id>\n\
|
||||
markseen <msg-id>\n\
|
||||
delmsg <msg-id>\n\
|
||||
===========================Contact commands==\n\
|
||||
@@ -429,7 +420,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
joinqr <qr-content>\n\
|
||||
setqr <qr-content>\n\
|
||||
providerinfo <addr>\n\
|
||||
event <event-id to test>\n\
|
||||
fileinfo <file>\n\
|
||||
estimatedeletion <seconds>\n\
|
||||
clear -- clear screen\n\
|
||||
@@ -471,20 +461,32 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"export-backup" => {
|
||||
let dir = dirs::home_dir().unwrap_or_default();
|
||||
imex(&context, ImexMode::ExportBackup, dir.as_ref()).await?;
|
||||
imex(
|
||||
&context,
|
||||
ImexMode::ExportBackup,
|
||||
dir.as_ref(),
|
||||
Some(arg2.to_string()),
|
||||
)
|
||||
.await?;
|
||||
println!("Exported to {}.", dir.to_string_lossy());
|
||||
}
|
||||
"import-backup" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <backup-file> missing.");
|
||||
imex(&context, ImexMode::ImportBackup, arg1.as_ref()).await?;
|
||||
imex(
|
||||
&context,
|
||||
ImexMode::ImportBackup,
|
||||
arg1.as_ref(),
|
||||
Some(arg2.to_string()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
"export-keys" => {
|
||||
let dir = dirs::home_dir().unwrap_or_default();
|
||||
imex(&context, ImexMode::ExportSelfKeys, dir.as_ref()).await?;
|
||||
imex(&context, ImexMode::ExportSelfKeys, dir.as_ref(), None).await?;
|
||||
println!("Exported to {}.", dir.to_string_lossy());
|
||||
}
|
||||
"import-keys" => {
|
||||
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref()).await?;
|
||||
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?;
|
||||
}
|
||||
"export-setup" => {
|
||||
let setup_code = create_setup_code(&context);
|
||||
@@ -563,7 +565,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
);
|
||||
|
||||
for i in (0..cnt).rev() {
|
||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
|
||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
|
||||
println!(
|
||||
"{}#{}: {} [{} fresh] {}{}{}{}",
|
||||
chat_prefix(&chat),
|
||||
@@ -637,14 +639,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
let time_start = std::time::SystemTime::now();
|
||||
let msglist =
|
||||
chat::get_chat_msgs(&context, sel_chat.get_id(), DC_GCM_ADDDAYMARKER, None).await?;
|
||||
chat::get_chat_msgs(&context, sel_chat.get_id(), DC_GCM_ADDDAYMARKER).await?;
|
||||
let time_needed = time_start.elapsed().unwrap_or_default();
|
||||
|
||||
let msglist: Vec<MsgId> = msglist
|
||||
.into_iter()
|
||||
.map(|x| match x {
|
||||
ChatItem::Message { msg_id } => msg_id,
|
||||
ChatItem::Marker1 => MsgId::new(DC_MSG_ID_MARKER1),
|
||||
ChatItem::DayMarker { .. } => MsgId::new(DC_MSG_ID_DAYMARKER),
|
||||
})
|
||||
.collect();
|
||||
@@ -706,7 +707,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"createchat" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id: u32 = arg1.parse()?;
|
||||
let contact_id = ContactId::new(arg1.parse()?);
|
||||
let chat_id = ChatId::create_for_contact(&context, contact_id).await?;
|
||||
|
||||
println!("Single#{} created successfully.", chat_id,);
|
||||
@@ -734,7 +735,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ensure!(sel_chat.is_some(), "No chat selected");
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
|
||||
let contact_id_0: u32 = arg1.parse()?;
|
||||
let contact_id_0 = ContactId::new(arg1.parse()?);
|
||||
chat::add_contact_to_chat(&context, sel_chat.as_ref().unwrap().get_id(), contact_id_0)
|
||||
.await?;
|
||||
println!("Contact added to chat.");
|
||||
@@ -742,7 +743,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"removemember" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id_1: u32 = arg1.parse()?;
|
||||
let contact_id_1 = ContactId::new(arg1.parse()?);
|
||||
chat::remove_contact_from_chat(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
@@ -758,7 +759,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
chat::set_chat_name(
|
||||
&context,
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
&format!("{} {}", arg1, arg2).trim(),
|
||||
format!("{} {}", arg1, arg2).trim(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -907,6 +908,16 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
Some(msg_id) => println!("sync message sent as {}.", msg_id),
|
||||
None => println!("sync message not needed."),
|
||||
},
|
||||
"sendupdate" => {
|
||||
ensure!(
|
||||
!arg1.is_empty() && !arg2.is_empty(),
|
||||
"Arguments <msg-id> <json status update> expected"
|
||||
);
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
context
|
||||
.send_webxdc_status_update(msg_id, arg2, "this is a webxdc status update")
|
||||
.await?;
|
||||
}
|
||||
"videochat" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||
@@ -914,13 +925,24 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"listmsgs" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <query> missing.");
|
||||
|
||||
let query = format!("{} {}", arg1, arg2).trim().to_string();
|
||||
let chat = sel_chat.as_ref().map(|sel_chat| sel_chat.get_id());
|
||||
let time_start = std::time::SystemTime::now();
|
||||
let msglist = context.search_msgs(chat, arg1).await?;
|
||||
let msglist = context.search_msgs(chat, &query).await?;
|
||||
let time_needed = time_start.elapsed().unwrap_or_default();
|
||||
|
||||
log_msglist(&context, &msglist).await?;
|
||||
println!("{} messages.", msglist.len());
|
||||
println!(
|
||||
"{}{} messages for {}search of \"{}\"",
|
||||
msglist.len(),
|
||||
if msglist.len() == 1000 { "+" } else { "" },
|
||||
if chat.is_none() {
|
||||
"global "
|
||||
} else {
|
||||
"in-chat-"
|
||||
},
|
||||
query,
|
||||
);
|
||||
println!("{:?} to create this list", time_needed);
|
||||
}
|
||||
"draft" => {
|
||||
@@ -1077,6 +1099,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
msg_ids[0] = MsgId::new(arg1.parse()?);
|
||||
chat::forward_msgs(&context, &msg_ids, chat_id).await?;
|
||||
}
|
||||
"resend" => {
|
||||
ensure!(!arg1.is_empty(), "Arguments <msg-id> expected");
|
||||
|
||||
let mut msg_ids = [MsgId::new(0); 1];
|
||||
msg_ids[0] = MsgId::new(arg1.parse()?);
|
||||
chat::resend_msgs(&context, &msg_ids).await?;
|
||||
}
|
||||
"markseen" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let mut msg_ids = vec![MsgId::new(0)];
|
||||
@@ -1116,7 +1145,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
"contactinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
|
||||
let contact_id: u32 = arg1.parse()?;
|
||||
let contact_id = ContactId::new(arg1.parse()?);
|
||||
let contact = Contact::get_by_id(&context, contact_id).await?;
|
||||
let name_n_addr = contact.get_name_n_addr();
|
||||
|
||||
@@ -1142,7 +1171,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
if 0 != i {
|
||||
res += ", ";
|
||||
}
|
||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)).await?;
|
||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
|
||||
res += &format!("{}#{}", chat_prefix(&chat), chat.get_id());
|
||||
}
|
||||
}
|
||||
@@ -1151,16 +1180,16 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
"delcontact" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
Contact::delete(&context, arg1.parse()?).await?;
|
||||
Contact::delete(&context, ContactId::new(arg1.parse()?)).await?;
|
||||
}
|
||||
"block" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id = arg1.parse()?;
|
||||
let contact_id = ContactId::new(arg1.parse()?);
|
||||
Contact::block(&context, contact_id).await?;
|
||||
}
|
||||
"unblock" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||
let contact_id = arg1.parse()?;
|
||||
let contact_id = ContactId::new(arg1.parse()?);
|
||||
Contact::unblock(&context, contact_id).await?;
|
||||
}
|
||||
"listblocked" => {
|
||||
@@ -1201,17 +1230,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: implement this again, unclear how to match this through though, without writing a parser.
|
||||
// "event" => {
|
||||
// ensure!(!arg1.is_empty(), "Argument <id> missing.");
|
||||
// let event = arg1.parse()?;
|
||||
// let event = EventType::from_u32(event).ok_or(format_err!("EventType::from_u32({})", event))?;
|
||||
// let r = context.emit_event(event, 0 as libc::uintptr_t, 0 as libc::uintptr_t);
|
||||
// println!(
|
||||
// "Sending event {:?}({}), received value {}.",
|
||||
// event, event as usize, r,
|
||||
// );
|
||||
// }
|
||||
"fileinfo" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||
|
||||
|
||||
@@ -169,7 +169,7 @@ const DB_COMMANDS: [&str; 10] = [
|
||||
"housekeeping",
|
||||
];
|
||||
|
||||
const CHAT_COMMANDS: [&str; 35] = [
|
||||
const CHAT_COMMANDS: [&str; 36] = [
|
||||
"listchats",
|
||||
"listarchived",
|
||||
"chat",
|
||||
@@ -191,6 +191,7 @@ const CHAT_COMMANDS: [&str; 35] = [
|
||||
"sendfile",
|
||||
"sendhtml",
|
||||
"sendsyncmsg",
|
||||
"sendupdate",
|
||||
"videochat",
|
||||
"draft",
|
||||
"listmedia",
|
||||
@@ -206,11 +207,12 @@ const CHAT_COMMANDS: [&str; 35] = [
|
||||
"accept",
|
||||
"blockchat",
|
||||
];
|
||||
const MESSAGE_COMMANDS: [&str; 7] = [
|
||||
const MESSAGE_COMMANDS: [&str; 8] = [
|
||||
"listmsgs",
|
||||
"msginfo",
|
||||
"listfresh",
|
||||
"forward",
|
||||
"resend",
|
||||
"markseen",
|
||||
"delmsg",
|
||||
"download",
|
||||
@@ -226,13 +228,12 @@ const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"unblock",
|
||||
"listblocked",
|
||||
];
|
||||
const MISC_COMMANDS: [&str; 12] = [
|
||||
const MISC_COMMANDS: [&str; 11] = [
|
||||
"getqr",
|
||||
"getqrsvg",
|
||||
"getbadqr",
|
||||
"checkqr",
|
||||
"joinqr",
|
||||
"event",
|
||||
"fileinfo",
|
||||
"clear",
|
||||
"exit",
|
||||
@@ -415,7 +416,7 @@ async fn handle_cmd(
|
||||
}
|
||||
"getqr" | "getbadqr" => {
|
||||
ctx.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
|
||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
||||
let mut qr = dc_get_securejoin_qr(&ctx, group).await?;
|
||||
if !qr.is_empty() {
|
||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||
@@ -432,7 +433,7 @@ async fn handle_cmd(
|
||||
}
|
||||
"getqrsvg" => {
|
||||
ctx.start_io().await;
|
||||
let group = arg1.parse::<u32>().ok().map(|id| ChatId::new(id));
|
||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
||||
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
||||
match get_securejoin_qr_svg(&ctx, group).await {
|
||||
Ok(svg) => {
|
||||
|
||||
6
node/.prettierrc.yml
Normal file
6
node/.prettierrc.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
# .prettierrc
|
||||
trailingComma: es5
|
||||
tabWidth: 2
|
||||
semi: false
|
||||
singleQuote: true
|
||||
jsxSingleQuote: true
|
||||
21
node/CONTRIBUTORS.md
Normal file
21
node/CONTRIBUTORS.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Contributors
|
||||
|
||||
| Name | GitHub |
|
||||
| :-------------------- | :----------------------------------------------- |
|
||||
| **Lars-Magnus Skog** | |
|
||||
| **jikstra** | |
|
||||
| **Simon Laux** | [**@Simon-Laux**](https://github.com/Simon-Laux) |
|
||||
| **Jikstra** | [**@Jikstra**](https://github.com/Jikstra) |
|
||||
| **Nico de Haen** | |
|
||||
| **B. Petersen** | |
|
||||
| **Karissa McKelvey** | [**@karissa**](https://github.com/karissa) |
|
||||
| **developer** | |
|
||||
| **Alexander Krotov** | |
|
||||
| **Floris Bruynooghe** | |
|
||||
| **lefherz** | |
|
||||
| **Pablo** | [**@pabzm**](https://github.com/pabzm) |
|
||||
| **pabzm** | |
|
||||
| **holger krekel** | |
|
||||
| **Robert Schütz** | |
|
||||
| **bb** | |
|
||||
| **Charles Paul** | |
|
||||
674
node/LICENSE
Normal file
674
node/LICENSE
Normal file
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
247
node/README.md
Normal file
247
node/README.md
Normal file
@@ -0,0 +1,247 @@
|
||||
# deltachat-node
|
||||
|
||||
> node.js bindings for [`deltachat-core-rust`](..)
|
||||
|
||||
[](https://www.npmjs.com/package/deltachat-node)
|
||||

|
||||
[](https://prettier.io)
|
||||
|
||||
`deltachat-node` primarily aims to offer two things:
|
||||
|
||||
- A high level JavaScript api with syntactic sugar
|
||||
- A low level c binding api around [`deltachat-core-rust`](..)
|
||||
|
||||
This code used to live at [`deltachat-node`](https://github.com/deltachat/deltachat-node)
|
||||
|
||||
## Table of Contents
|
||||
|
||||
<details><summary>Click to expand</summary>
|
||||
|
||||
- [Install](#install)
|
||||
- [Dependencies](#dependencies)
|
||||
- [Build from source](#build-from-source)
|
||||
- [Usage](#usage)
|
||||
- [Developing](#developing)
|
||||
- [License](#license)
|
||||
|
||||
</details>
|
||||
|
||||
## Install
|
||||
|
||||
By default the installation will build try to use the bundled prebuilds in the
|
||||
npm package. If this fails it falls back to compile `../deltachat-core-rust` from
|
||||
this repository, using `scripts/rebuild-core.js`.
|
||||
|
||||
To install from npm use:
|
||||
|
||||
```
|
||||
npm install deltachat-node
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Nodejs >= `v16.0.0`
|
||||
- rustup (optional if you can't use the prebuilds)
|
||||
|
||||
> On Windows, you may need to also install **Perl** to be able to compile deltachat-core.
|
||||
|
||||
## Build from source
|
||||
|
||||
If you want to build from source, make sure that you have `rustup` installed.
|
||||
You can either use `npm install deltachat-node --build-from-source` to force
|
||||
building from source or clone this repository and follow this steps:
|
||||
|
||||
1. `git clone https://github.com/deltachat/deltachat-core-rust.git`
|
||||
2. `cd deltachat-core-rust`
|
||||
3. `npm i`
|
||||
4. `npm run build`
|
||||
|
||||
> Our `package.json` file is located in the root directory of this repository,
|
||||
> not inside this folder. (We need this in order to include the rust source
|
||||
> code in the npm package.)
|
||||
|
||||
### Use build-from-source in deltachat-desktop
|
||||
|
||||
If you want to use the manually built node bindings in the desktop client (for
|
||||
example), you can follow these instructions:
|
||||
|
||||
First clone the
|
||||
[deltachat-desktop](https://github.com/deltachat/deltachat-desktop) repository,
|
||||
e.g. with `git clone https://github.com/deltachat/deltachat-desktop`.
|
||||
|
||||
Then you need to make sure that this directory is referenced correctly in
|
||||
deltachat-desktop's package.json. You need to change
|
||||
`deltachat-desktop/package.json` like this:
|
||||
|
||||
```
|
||||
diff --git i/package.json w/package.json
|
||||
index 45893894..5154512c 100644
|
||||
--- i/package.json
|
||||
+++ w/package.json
|
||||
@@ -83,7 +83,7 @@
|
||||
"application-config": "^1.0.1",
|
||||
"classnames": "^2.3.1",
|
||||
"debounce": "^1.2.0",
|
||||
- "deltachat-node": "1.79.3",
|
||||
+ "deltachat-node": "file:../deltachat-core-rust/",
|
||||
"emoji-js-clean": "^4.0.0",
|
||||
"emoji-mart": "^3.0.1",
|
||||
"emoji-regex": "^9.2.2",
|
||||
```
|
||||
|
||||
Then, in the `deltachat-desktop` repository, run:
|
||||
|
||||
1. `npm i`
|
||||
2. `npm run build`
|
||||
3. And `npm run start` to start the newly built client.
|
||||
|
||||
### Workaround to build for x86_64 on Apple's M1
|
||||
|
||||
deltachat doesn't support universal (fat) binaries (that contain builds for both cpu architectures) yet, until it does you can use the following workaround to get x86_64 builds:
|
||||
|
||||
```
|
||||
$ fnm install 17 --arch x64
|
||||
$ fnm use 17
|
||||
$ node -p process.arch
|
||||
# result should be x64
|
||||
$ cd deltachat-core-rust && rustup target add x86_64-apple-darwin && cd -
|
||||
$ git apply patches/m1_build_use_x86_64.patch
|
||||
$ CARGO_BUILD_TARGET=x86_64-apple-darwin npm run build
|
||||
$ npm run test
|
||||
```
|
||||
|
||||
(when using [fnm](https://github.com/Schniz/fnm) instead of nvm, you can select the architecture)
|
||||
If your node and electron are already build for arm64 you can also try bulding for arm:
|
||||
|
||||
```
|
||||
$ fnm install 16 --arch arm64
|
||||
$ fnm use 16
|
||||
$ node -p process.arch
|
||||
# result should be arm64
|
||||
$ npm_config_arch=arm64 npm run build
|
||||
$ npm run test
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```js
|
||||
const { Context } = require('deltachat-node')
|
||||
|
||||
const opts = {
|
||||
addr: '[email]',
|
||||
mail_pw: '[password]',
|
||||
}
|
||||
|
||||
const contact = '[email]'
|
||||
|
||||
async function main() {
|
||||
const dc = Context.open('./')
|
||||
dc.on('ALL', console.log.bind(null, 'core |'))
|
||||
|
||||
try {
|
||||
await dc.configure(opts)
|
||||
} catch (err) {
|
||||
console.error('Failed to configure because of: ', err)
|
||||
dc.unref()
|
||||
return
|
||||
}
|
||||
|
||||
dc.startIO()
|
||||
console.log('fully configured')
|
||||
|
||||
const contactId = dc.createContact('Test', contact)
|
||||
const chatId = dc.createChatByContactId(contactId)
|
||||
dc.sendMessage(chatId, 'Hi!')
|
||||
|
||||
console.log('sent message')
|
||||
|
||||
dc.once('DC_EVENT_SMTP_MESSAGE_SENT', async () => {
|
||||
console.log('Message sent, shutting down...')
|
||||
dc.stopIO()
|
||||
console.log('stopped io')
|
||||
dc.unref()
|
||||
})
|
||||
}
|
||||
|
||||
main()
|
||||
```
|
||||
this example can also be found in the examples folder [examples/send_message.js](./examples/send_message.js)
|
||||
|
||||
### Generating Docs
|
||||
|
||||
We are curently migrating to automaticaly generated documentation.
|
||||
You can find the old documentation at [old_docs](./old_docs).
|
||||
|
||||
to generate the documentation, run:
|
||||
|
||||
```
|
||||
npx typedoc
|
||||
```
|
||||
|
||||
The resulting documentation can be found in the `docs/` folder.
|
||||
An online version can be found under [js.delta.chat](https://js.delta.chat).
|
||||
|
||||
## Developing
|
||||
|
||||
### Tests and Coverage
|
||||
|
||||
Running `npm test` ends with showing a code coverage report, which is produced by [`nyc`](https://github.com/istanbuljs/nyc#readme).
|
||||
|
||||

|
||||
|
||||
The coverage report from `nyc` in the console is rather limited. To get a more detailed coverage report you can run `npm run coverage-html-report`. This will produce a html report from the `nyc` data and display it in a browser on your local machine.
|
||||
|
||||
To run the integration tests you need to set the `DCC_NEW_TMP_EMAIL` environment variables. E.g.:
|
||||
|
||||
```
|
||||
$ export DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=[token]
|
||||
$ npm run test
|
||||
```
|
||||
|
||||
### Scripts
|
||||
|
||||
We have the following scripts for building, testing and coverage:
|
||||
|
||||
- `npm run coverage` Creates a coverage report and passes it to `coveralls`. Only done by `Travis`.
|
||||
- `npm run coverage-html-report` Generates a html report from the coverage data and opens it in a browser on the local machine.
|
||||
- `npm run generate-constants` Generates `constants.js` and `events.js` based on the `deltachat-core-rust/deltachat-ffi/deltachat.h` header file.
|
||||
- `npm install` After dependencies are installed, runs `node-gyp-build` to see if the native code needs to be rebuilt.
|
||||
- `npm run build` Rebuilds all code.
|
||||
- `npm run build:core` Rebuilds code in `deltachat-core-rust`.
|
||||
- `npm run build:bindings` Rebuilds the bindings and links with `deltachat-core-rust`.
|
||||
- `ǹpm run clean` Removes all built code
|
||||
- `npm run prebuildify` Builds prebuilt binary to `prebuilds/$PLATFORM-$ARCH`. Copies `deltachat.dll` from `deltachat-core-rust` for windows.
|
||||
- `npm run download-prebuilds` Downloads all prebuilt binaries from github before `npm publish`.
|
||||
- `npm test` Runs `standard` and then the tests in `test/index.js`.
|
||||
- `npm run test-integration` Runs the integration tests.
|
||||
- `npm run hallmark` Runs `hallmark` on all markdown files.
|
||||
|
||||
### Releases
|
||||
|
||||
The following steps are needed to make a release:
|
||||
|
||||
1. Wait until `pack-module` github action is completed
|
||||
2. Run `npm publish https://download.delta.chat/node/deltachat-node-v1.x.x.tar.gz` to publish it to npm. You probably need write rights to npm.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under `GPL-3.0-or-later`, see [LICENSE](./LICENSE) file for details.
|
||||
|
||||
> Copyright © 2018 `DeltaChat` contributors.
|
||||
>
|
||||
> This program is free software: you can redistribute it and/or modify
|
||||
> it under the terms of the GNU General Public License as published by
|
||||
> the Free Software Foundation, either version 3 of the License, or
|
||||
> (at your option) any later version.
|
||||
>
|
||||
> This program is distributed in the hope that it will be useful,
|
||||
> but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
> MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
> GNU General Public License for more details.
|
||||
>
|
||||
> You should have received a copy of the GNU General Public License
|
||||
> along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
[appveyor-shield]: https://ci.appveyor.com/api/projects/status/t0narp672wpbl6pd?svg=true
|
||||
|
||||
[appveyor]: https://ci.appveyor.com/project/ralphtheninja/deltachat-node-d4bf8
|
||||
78
node/binding.gyp
Normal file
78
node/binding.gyp
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
# documentation about the format of this file can be found under https://gyp.gsrc.io/docs/InputFormatReference.md
|
||||
# Variables can be specified when calling node-gyp as so:
|
||||
# node-gyp configure -- -Dvarname=value
|
||||
"variables": {
|
||||
# Whether to use a system-wide installation of deltachat-core
|
||||
# using pkg-config. Set to either "true" or "false".
|
||||
"USE_SYSTEM_LIBDELTACHAT%": "<!(echo $USE_SYSTEM_LIBDELTACHAT)",
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "deltachat",
|
||||
"sources": ["./src/module.c"],
|
||||
"include_dirs": ["<!(node -e \"require('napi-macros')\")"],
|
||||
"conditions": [
|
||||
[
|
||||
"OS == 'win'",
|
||||
{
|
||||
"include_dirs": ["../deltachat-ffi"],
|
||||
"libraries": [
|
||||
"../../target/release/deltachat.dll.lib",
|
||||
],
|
||||
"conditions": [
|
||||
[
|
||||
"USE_SYSTEM_LIBDELTACHAT == 'true'",
|
||||
{
|
||||
"cflags": ["<!(pkg-config --cflags deltachat)"],
|
||||
"libraries": ["<!(pkg-config --libs deltachat)"],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
"OS == 'linux' or OS == 'mac'",
|
||||
{
|
||||
"libraries": ["-lpthread"],
|
||||
"cflags": ["-std=gnu99"],
|
||||
"conditions": [
|
||||
[
|
||||
"USE_SYSTEM_LIBDELTACHAT != 'true'",
|
||||
{
|
||||
"include_dirs": ["../deltachat-ffi"],
|
||||
"ldflags": ["-Wl,-Bsymbolic"], # Prevent sqlite3 from electron from overriding sqlcipher
|
||||
"libraries": [
|
||||
"../../target/release/libdeltachat.a",
|
||||
"-ldl",
|
||||
],
|
||||
"conditions": [],
|
||||
},
|
||||
{
|
||||
# USE_SYSTEM_LIBDELTACHAT == 'true'
|
||||
"cflags": ["<!(pkg-config --cflags deltachat)"],
|
||||
"libraries": ["<!(pkg-config --libs deltachat)"],
|
||||
},
|
||||
],
|
||||
[
|
||||
"OS == 'mac'",
|
||||
{
|
||||
"libraries": [
|
||||
"-framework CoreFoundation",
|
||||
"-framework CoreServices",
|
||||
"-framework Security",
|
||||
"-lresolv",
|
||||
],
|
||||
},
|
||||
{
|
||||
# OS == 'linux'
|
||||
"libraries": ["-lm", "-lrt"],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
1
node/binding.js
Normal file
1
node/binding.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('node-gyp-build')(__dirname)
|
||||
226
node/constants.js
Normal file
226
node/constants.js
Normal file
@@ -0,0 +1,226 @@
|
||||
// Generated!
|
||||
|
||||
module.exports = {
|
||||
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES: 3,
|
||||
DC_CERTCK_AUTO: 0,
|
||||
DC_CERTCK_STRICT: 1,
|
||||
DC_CHAT_ID_ALLDONE_HINT: 7,
|
||||
DC_CHAT_ID_ARCHIVED_LINK: 6,
|
||||
DC_CHAT_ID_LAST_SPECIAL: 9,
|
||||
DC_CHAT_ID_TRASH: 3,
|
||||
DC_CHAT_TYPE_BROADCAST: 160,
|
||||
DC_CHAT_TYPE_GROUP: 120,
|
||||
DC_CHAT_TYPE_MAILINGLIST: 140,
|
||||
DC_CHAT_TYPE_SINGLE: 100,
|
||||
DC_CHAT_TYPE_UNDEFINED: 0,
|
||||
DC_CHAT_VISIBILITY_ARCHIVED: 1,
|
||||
DC_CHAT_VISIBILITY_NORMAL: 0,
|
||||
DC_CHAT_VISIBILITY_PINNED: 2,
|
||||
DC_CONNECTIVITY_CONNECTED: 4000,
|
||||
DC_CONNECTIVITY_CONNECTING: 2000,
|
||||
DC_CONNECTIVITY_NOT_CONNECTED: 1000,
|
||||
DC_CONNECTIVITY_WORKING: 3000,
|
||||
DC_CONTACT_ID_DEVICE: 5,
|
||||
DC_CONTACT_ID_INFO: 2,
|
||||
DC_CONTACT_ID_LAST_SPECIAL: 9,
|
||||
DC_CONTACT_ID_SELF: 1,
|
||||
DC_DOWNLOAD_AVAILABLE: 10,
|
||||
DC_DOWNLOAD_DONE: 0,
|
||||
DC_DOWNLOAD_FAILURE: 20,
|
||||
DC_DOWNLOAD_IN_PROGRESS: 1000,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED: 2021,
|
||||
DC_EVENT_CHAT_MODIFIED: 2020,
|
||||
DC_EVENT_CONFIGURE_PROGRESS: 2041,
|
||||
DC_EVENT_CONNECTIVITY_CHANGED: 2100,
|
||||
DC_EVENT_CONTACTS_CHANGED: 2030,
|
||||
DC_EVENT_DELETED_BLOB_FILE: 151,
|
||||
DC_EVENT_ERROR: 400,
|
||||
DC_EVENT_ERROR_SELF_NOT_IN_GROUP: 410,
|
||||
DC_EVENT_IMAP_CONNECTED: 102,
|
||||
DC_EVENT_IMAP_MESSAGE_DELETED: 104,
|
||||
DC_EVENT_IMAP_MESSAGE_MOVED: 105,
|
||||
DC_EVENT_IMEX_FILE_WRITTEN: 2052,
|
||||
DC_EVENT_IMEX_PROGRESS: 2051,
|
||||
DC_EVENT_INCOMING_MSG: 2005,
|
||||
DC_EVENT_INFO: 100,
|
||||
DC_EVENT_LOCATION_CHANGED: 2035,
|
||||
DC_EVENT_MSGS_CHANGED: 2000,
|
||||
DC_EVENT_MSGS_NOTICED: 2008,
|
||||
DC_EVENT_MSG_DELIVERED: 2010,
|
||||
DC_EVENT_MSG_FAILED: 2012,
|
||||
DC_EVENT_MSG_READ: 2015,
|
||||
DC_EVENT_NEW_BLOB_FILE: 150,
|
||||
DC_EVENT_SECUREJOIN_INVITER_PROGRESS: 2060,
|
||||
DC_EVENT_SECUREJOIN_JOINER_PROGRESS: 2061,
|
||||
DC_EVENT_SELFAVATAR_CHANGED: 2110,
|
||||
DC_EVENT_SMTP_CONNECTED: 101,
|
||||
DC_EVENT_SMTP_MESSAGE_SENT: 103,
|
||||
DC_EVENT_WARNING: 300,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE: 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT: 4,
|
||||
DC_GCL_ADD_SELF: 2,
|
||||
DC_GCL_ARCHIVED_ONLY: 1,
|
||||
DC_GCL_FOR_FORWARDING: 8,
|
||||
DC_GCL_NO_SPECIALS: 2,
|
||||
DC_GCL_VERIFIED_ONLY: 1,
|
||||
DC_GCM_ADDDAYMARKER: 1,
|
||||
DC_GCM_INFO_ONLY: 2,
|
||||
DC_IMEX_EXPORT_BACKUP: 11,
|
||||
DC_IMEX_EXPORT_SELF_KEYS: 1,
|
||||
DC_IMEX_IMPORT_BACKUP: 12,
|
||||
DC_IMEX_IMPORT_SELF_KEYS: 2,
|
||||
DC_INFO_PROTECTION_DISABLED: 12,
|
||||
DC_INFO_PROTECTION_ENABLED: 11,
|
||||
DC_KEY_GEN_DEFAULT: 0,
|
||||
DC_KEY_GEN_ED25519: 2,
|
||||
DC_KEY_GEN_RSA2048: 1,
|
||||
DC_LP_AUTH_NORMAL: 4,
|
||||
DC_LP_AUTH_OAUTH2: 2,
|
||||
DC_MEDIA_QUALITY_BALANCED: 0,
|
||||
DC_MEDIA_QUALITY_WORSE: 1,
|
||||
DC_MSG_AUDIO: 40,
|
||||
DC_MSG_FILE: 60,
|
||||
DC_MSG_GIF: 21,
|
||||
DC_MSG_ID_DAYMARKER: 9,
|
||||
DC_MSG_ID_LAST_SPECIAL: 9,
|
||||
DC_MSG_ID_MARKER1: 1,
|
||||
DC_MSG_IMAGE: 20,
|
||||
DC_MSG_STICKER: 23,
|
||||
DC_MSG_TEXT: 10,
|
||||
DC_MSG_VIDEO: 50,
|
||||
DC_MSG_VIDEOCHAT_INVITATION: 70,
|
||||
DC_MSG_VOICE: 41,
|
||||
DC_MSG_WEBXDC: 80,
|
||||
DC_PROVIDER_STATUS_BROKEN: 3,
|
||||
DC_PROVIDER_STATUS_OK: 1,
|
||||
DC_PROVIDER_STATUS_PREPARATION: 2,
|
||||
DC_QR_ACCOUNT: 250,
|
||||
DC_QR_ADDR: 320,
|
||||
DC_QR_ASK_VERIFYCONTACT: 200,
|
||||
DC_QR_ASK_VERIFYGROUP: 202,
|
||||
DC_QR_ERROR: 400,
|
||||
DC_QR_FPR_MISMATCH: 220,
|
||||
DC_QR_FPR_OK: 210,
|
||||
DC_QR_FPR_WITHOUT_ADDR: 230,
|
||||
DC_QR_REVIVE_VERIFYCONTACT: 510,
|
||||
DC_QR_REVIVE_VERIFYGROUP: 512,
|
||||
DC_QR_TEXT: 330,
|
||||
DC_QR_URL: 332,
|
||||
DC_QR_WEBRTC_INSTANCE: 260,
|
||||
DC_QR_WITHDRAW_VERIFYCONTACT: 500,
|
||||
DC_QR_WITHDRAW_VERIFYGROUP: 502,
|
||||
DC_SHOW_EMAILS_ACCEPTED_CONTACTS: 1,
|
||||
DC_SHOW_EMAILS_ALL: 2,
|
||||
DC_SHOW_EMAILS_OFF: 0,
|
||||
DC_SOCKET_AUTO: 0,
|
||||
DC_SOCKET_PLAIN: 3,
|
||||
DC_SOCKET_SSL: 1,
|
||||
DC_SOCKET_STARTTLS: 2,
|
||||
DC_STATE_IN_FRESH: 10,
|
||||
DC_STATE_IN_NOTICED: 13,
|
||||
DC_STATE_IN_SEEN: 16,
|
||||
DC_STATE_OUT_DELIVERED: 26,
|
||||
DC_STATE_OUT_DRAFT: 19,
|
||||
DC_STATE_OUT_FAILED: 24,
|
||||
DC_STATE_OUT_MDN_RCVD: 28,
|
||||
DC_STATE_OUT_PENDING: 20,
|
||||
DC_STATE_OUT_PREPARING: 18,
|
||||
DC_STATE_UNDEFINED: 0,
|
||||
DC_STR_AC_SETUP_MSG_BODY: 43,
|
||||
DC_STR_AC_SETUP_MSG_SUBJECT: 42,
|
||||
DC_STR_ARCHIVEDCHATS: 40,
|
||||
DC_STR_AUDIO: 11,
|
||||
DC_STR_BAD_TIME_MSG_BODY: 85,
|
||||
DC_STR_BROADCAST_LIST: 115,
|
||||
DC_STR_CANNOT_LOGIN: 60,
|
||||
DC_STR_CANTDECRYPT_MSG_BODY: 29,
|
||||
DC_STR_CONFIGURATION_FAILED: 84,
|
||||
DC_STR_CONNECTED: 107,
|
||||
DC_STR_CONNTECTING: 108,
|
||||
DC_STR_CONTACT_NOT_VERIFIED: 36,
|
||||
DC_STR_CONTACT_SETUP_CHANGED: 37,
|
||||
DC_STR_CONTACT_VERIFIED: 35,
|
||||
DC_STR_DEVICE_MESSAGES: 68,
|
||||
DC_STR_DEVICE_MESSAGES_HINT: 70,
|
||||
DC_STR_DOWNLOAD_AVAILABILITY: 100,
|
||||
DC_STR_DRAFT: 3,
|
||||
DC_STR_E2E_AVAILABLE: 25,
|
||||
DC_STR_E2E_PREFERRED: 34,
|
||||
DC_STR_ENCRYPTEDMSG: 24,
|
||||
DC_STR_ENCR_NONE: 28,
|
||||
DC_STR_ENCR_TRANSP: 27,
|
||||
DC_STR_EPHEMERAL_DAY: 79,
|
||||
DC_STR_EPHEMERAL_DAYS: 95,
|
||||
DC_STR_EPHEMERAL_DISABLED: 75,
|
||||
DC_STR_EPHEMERAL_FOUR_WEEKS: 81,
|
||||
DC_STR_EPHEMERAL_HOUR: 78,
|
||||
DC_STR_EPHEMERAL_HOURS: 94,
|
||||
DC_STR_EPHEMERAL_MINUTE: 77,
|
||||
DC_STR_EPHEMERAL_MINUTES: 93,
|
||||
DC_STR_EPHEMERAL_SECONDS: 76,
|
||||
DC_STR_EPHEMERAL_WEEK: 80,
|
||||
DC_STR_EPHEMERAL_WEEKS: 96,
|
||||
DC_STR_ERROR: 112,
|
||||
DC_STR_ERROR_NO_NETWORK: 87,
|
||||
DC_STR_FAILED_SENDING_TO: 74,
|
||||
DC_STR_FILE: 12,
|
||||
DC_STR_FINGERPRINTS: 30,
|
||||
DC_STR_FORWARDED: 97,
|
||||
DC_STR_GIF: 23,
|
||||
DC_STR_IMAGE: 9,
|
||||
DC_STR_INCOMING_MESSAGES: 103,
|
||||
DC_STR_LAST_MSG_SENT_SUCCESSFULLY: 111,
|
||||
DC_STR_LOCATION: 66,
|
||||
DC_STR_MESSAGES: 114,
|
||||
DC_STR_MSGACTIONBYME: 63,
|
||||
DC_STR_MSGACTIONBYUSER: 62,
|
||||
DC_STR_MSGADDMEMBER: 17,
|
||||
DC_STR_MSGDELMEMBER: 18,
|
||||
DC_STR_MSGGROUPLEFT: 19,
|
||||
DC_STR_MSGGRPIMGCHANGED: 16,
|
||||
DC_STR_MSGGRPIMGDELETED: 33,
|
||||
DC_STR_MSGGRPNAME: 15,
|
||||
DC_STR_MSGLOCATIONDISABLED: 65,
|
||||
DC_STR_MSGLOCATIONENABLED: 64,
|
||||
DC_STR_NOMESSAGES: 1,
|
||||
DC_STR_NOT_CONNECTED: 121,
|
||||
DC_STR_NOT_SUPPORTED_BY_PROVIDER: 113,
|
||||
DC_STR_ONE_MOMENT: 106,
|
||||
DC_STR_OUTGOING_MESSAGES: 104,
|
||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
|
||||
DC_STR_PART_OF_TOTAL_USED: 116,
|
||||
DC_STR_PROTECTION_DISABLED: 89,
|
||||
DC_STR_PROTECTION_ENABLED: 88,
|
||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
|
||||
DC_STR_READRCPT: 31,
|
||||
DC_STR_READRCPT_MAILBODY: 32,
|
||||
DC_STR_REPLY_NOUN: 90,
|
||||
DC_STR_SAVED_MESSAGES: 69,
|
||||
DC_STR_SECURE_JOIN_GROUP_QR_DESC: 120,
|
||||
DC_STR_SECURE_JOIN_REPLIES: 118,
|
||||
DC_STR_SECURE_JOIN_STARTED: 117,
|
||||
DC_STR_SELF: 2,
|
||||
DC_STR_SELF_DELETED_MSG_BODY: 91,
|
||||
DC_STR_SENDING: 110,
|
||||
DC_STR_SERVER_TURNED_OFF: 92,
|
||||
DC_STR_SETUP_CONTACT_QR_DESC: 119,
|
||||
DC_STR_STICKER: 67,
|
||||
DC_STR_STORAGE_ON_DOMAIN: 105,
|
||||
DC_STR_SUBJECT_FOR_NEW_CONTACT: 73,
|
||||
DC_STR_SYNC_MSG_BODY: 102,
|
||||
DC_STR_SYNC_MSG_SUBJECT: 101,
|
||||
DC_STR_UNKNOWN_SENDER_FOR_CHAT: 72,
|
||||
DC_STR_UPDATE_REMINDER_MSG_BODY: 86,
|
||||
DC_STR_UPDATING: 109,
|
||||
DC_STR_VIDEO: 10,
|
||||
DC_STR_VIDEOCHAT_INVITATION: 82,
|
||||
DC_STR_VIDEOCHAT_INVITE_MSG_BODY: 83,
|
||||
DC_STR_VOICEMESSAGE: 7,
|
||||
DC_STR_WELCOME_MESSAGE: 71,
|
||||
DC_TEXT1_DRAFT: 1,
|
||||
DC_TEXT1_SELF: 3,
|
||||
DC_TEXT1_USERNAME: 2,
|
||||
DC_VIDEOCHATTYPE_BASICWEBRTC: 1,
|
||||
DC_VIDEOCHATTYPE_JITSI: 2,
|
||||
DC_VIDEOCHATTYPE_UNKNOWN: 0
|
||||
}
|
||||
34
node/events.js
Normal file
34
node/events.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/* eslint-disable quotes */
|
||||
// Generated!
|
||||
|
||||
module.exports = {
|
||||
100: 'DC_EVENT_INFO',
|
||||
101: 'DC_EVENT_SMTP_CONNECTED',
|
||||
102: 'DC_EVENT_IMAP_CONNECTED',
|
||||
103: 'DC_EVENT_SMTP_MESSAGE_SENT',
|
||||
104: 'DC_EVENT_IMAP_MESSAGE_DELETED',
|
||||
105: 'DC_EVENT_IMAP_MESSAGE_MOVED',
|
||||
150: 'DC_EVENT_NEW_BLOB_FILE',
|
||||
151: 'DC_EVENT_DELETED_BLOB_FILE',
|
||||
300: 'DC_EVENT_WARNING',
|
||||
400: 'DC_EVENT_ERROR',
|
||||
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||
2005: 'DC_EVENT_INCOMING_MSG',
|
||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||
2010: 'DC_EVENT_MSG_DELIVERED',
|
||||
2012: 'DC_EVENT_MSG_FAILED',
|
||||
2015: 'DC_EVENT_MSG_READ',
|
||||
2020: 'DC_EVENT_CHAT_MODIFIED',
|
||||
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
|
||||
2030: 'DC_EVENT_CONTACTS_CHANGED',
|
||||
2035: 'DC_EVENT_LOCATION_CHANGED',
|
||||
2041: 'DC_EVENT_CONFIGURE_PROGRESS',
|
||||
2051: 'DC_EVENT_IMEX_PROGRESS',
|
||||
2052: 'DC_EVENT_IMEX_FILE_WRITTEN',
|
||||
2060: 'DC_EVENT_SECUREJOIN_INVITER_PROGRESS',
|
||||
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
|
||||
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
|
||||
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE'
|
||||
}
|
||||
40
node/examples/send_message.js
Normal file
40
node/examples/send_message.js
Normal file
@@ -0,0 +1,40 @@
|
||||
//@ts-check
|
||||
const { Context } = require('../dist')
|
||||
|
||||
const opts = {
|
||||
addr: '[email]',
|
||||
mail_pw: '[password]',
|
||||
}
|
||||
|
||||
const contact = '[email]'
|
||||
|
||||
async function main() {
|
||||
const dc = Context.open('./')
|
||||
dc.on('ALL', console.log.bind(null, 'core |'))
|
||||
|
||||
try {
|
||||
await dc.configure(opts)
|
||||
} catch (err) {
|
||||
console.error('Failed to configure because of: ', err)
|
||||
dc.unref()
|
||||
return
|
||||
}
|
||||
|
||||
dc.startIO()
|
||||
console.log('fully configured')
|
||||
|
||||
const contactId = dc.createContact('Test', contact)
|
||||
const chatId = dc.createChatByContactId(contactId)
|
||||
dc.sendMessage(chatId, 'Hi!')
|
||||
|
||||
console.log('sent message')
|
||||
|
||||
dc.once('DC_EVENT_SMTP_MESSAGE_SENT', async () => {
|
||||
console.log('Message sent, shutting down...')
|
||||
dc.stopIO()
|
||||
console.log('stopped io')
|
||||
dc.unref()
|
||||
})
|
||||
}
|
||||
|
||||
main()
|
||||
BIN
node/images/tests.png
Normal file
BIN
node/images/tests.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
9
node/lib/binding.ts
Normal file
9
node/lib/binding.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { join } from 'path'
|
||||
|
||||
/**
|
||||
* bindings are not typed yet.
|
||||
* if the availible function names are required they can be found inside of `../src/module.c`
|
||||
*/
|
||||
export const bindings: any = require('node-gyp-build')(join(__dirname, '../'))
|
||||
|
||||
export default bindings
|
||||
106
node/lib/chat.ts
Normal file
106
node/lib/chat.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import binding from './binding'
|
||||
import rawDebug from 'debug'
|
||||
const debug = rawDebug('deltachat:node:chat')
|
||||
import { C } from './constants'
|
||||
import { integerToHexColor } from './util'
|
||||
import { ChatJSON } from './types'
|
||||
|
||||
interface NativeChat {}
|
||||
/**
|
||||
* Wrapper around dc_chat_t*
|
||||
*/
|
||||
|
||||
export class Chat {
|
||||
constructor(public dc_chat: NativeChat) {
|
||||
debug('Chat constructor')
|
||||
if (dc_chat === null) {
|
||||
throw new Error('native chat can not be null')
|
||||
}
|
||||
}
|
||||
|
||||
getVisibility():
|
||||
| C.DC_CHAT_VISIBILITY_NORMAL
|
||||
| C.DC_CHAT_VISIBILITY_ARCHIVED
|
||||
| C.DC_CHAT_VISIBILITY_PINNED {
|
||||
return binding.dcn_chat_get_visibility(this.dc_chat)
|
||||
}
|
||||
|
||||
get color(): string {
|
||||
return integerToHexColor(binding.dcn_chat_get_color(this.dc_chat))
|
||||
}
|
||||
|
||||
getId(): number {
|
||||
return binding.dcn_chat_get_id(this.dc_chat)
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return binding.dcn_chat_get_name(this.dc_chat)
|
||||
}
|
||||
|
||||
getProfileImage(): string {
|
||||
return binding.dcn_chat_get_profile_image(this.dc_chat)
|
||||
}
|
||||
|
||||
getType(): number {
|
||||
return binding.dcn_chat_get_type(this.dc_chat)
|
||||
}
|
||||
|
||||
isSelfTalk(): boolean {
|
||||
return Boolean(binding.dcn_chat_is_self_talk(this.dc_chat))
|
||||
}
|
||||
|
||||
isContactRequest(): boolean {
|
||||
return Boolean(binding.dcn_chat_is_contact_request(this.dc_chat))
|
||||
}
|
||||
|
||||
isUnpromoted(): boolean {
|
||||
return Boolean(binding.dcn_chat_is_unpromoted(this.dc_chat))
|
||||
}
|
||||
|
||||
isProtected(): boolean {
|
||||
return Boolean(binding.dcn_chat_is_protected(this.dc_chat))
|
||||
}
|
||||
|
||||
get canSend(): boolean {
|
||||
return Boolean(binding.dcn_chat_can_send(this.dc_chat))
|
||||
}
|
||||
|
||||
isDeviceTalk(): boolean {
|
||||
return Boolean(binding.dcn_chat_is_device_talk(this.dc_chat))
|
||||
}
|
||||
|
||||
isSingle(): boolean {
|
||||
return this.getType() === C.DC_CHAT_TYPE_SINGLE
|
||||
}
|
||||
|
||||
isGroup(): boolean {
|
||||
return this.getType() === C.DC_CHAT_TYPE_GROUP
|
||||
}
|
||||
|
||||
isMuted(): boolean {
|
||||
return Boolean(binding.dcn_chat_is_muted(this.dc_chat))
|
||||
}
|
||||
|
||||
toJson(): ChatJSON {
|
||||
debug('toJson')
|
||||
const visibility = this.getVisibility()
|
||||
return {
|
||||
archived: visibility === C.DC_CHAT_VISIBILITY_ARCHIVED,
|
||||
pinned: visibility === C.DC_CHAT_VISIBILITY_PINNED,
|
||||
color: this.color,
|
||||
id: this.getId(),
|
||||
name: this.getName(),
|
||||
profileImage: this.getProfileImage(),
|
||||
type: this.getType(),
|
||||
isSelfTalk: this.isSelfTalk(),
|
||||
isUnpromoted: this.isUnpromoted(),
|
||||
isProtected: this.isProtected(),
|
||||
canSend: this.canSend,
|
||||
isDeviceTalk: this.isDeviceTalk(),
|
||||
isContactRequest: this.isContactRequest(),
|
||||
muted: this.isMuted(),
|
||||
}
|
||||
}
|
||||
}
|
||||
42
node/lib/chatlist.ts
Normal file
42
node/lib/chatlist.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import binding from './binding'
|
||||
import { Lot } from './lot'
|
||||
import { Chat } from './chat'
|
||||
const debug = require('debug')('deltachat:node:chatlist')
|
||||
|
||||
interface NativeChatList {}
|
||||
/**
|
||||
* Wrapper around dc_chatlist_t*
|
||||
*/
|
||||
export class ChatList {
|
||||
constructor(private dc_chatlist: NativeChatList) {
|
||||
debug('ChatList constructor')
|
||||
if (dc_chatlist === null) {
|
||||
throw new Error('native chat list can not be null')
|
||||
}
|
||||
}
|
||||
|
||||
getChatId(index: number): number {
|
||||
debug(`getChatId ${index}`)
|
||||
return binding.dcn_chatlist_get_chat_id(this.dc_chatlist, index)
|
||||
}
|
||||
|
||||
getCount(): number {
|
||||
debug('getCount')
|
||||
return binding.dcn_chatlist_get_cnt(this.dc_chatlist)
|
||||
}
|
||||
|
||||
getMessageId(index: number): number {
|
||||
debug(`getMessageId ${index}`)
|
||||
return binding.dcn_chatlist_get_msg_id(this.dc_chatlist, index)
|
||||
}
|
||||
|
||||
getSummary(index: number, chat?: Chat): Lot {
|
||||
debug(`getSummary ${index}`)
|
||||
const dc_chat = (chat && chat.dc_chat) || null
|
||||
return new Lot(
|
||||
binding.dcn_chatlist_get_summary(this.dc_chatlist, index, dc_chat)
|
||||
)
|
||||
}
|
||||
}
|
||||
260
node/lib/constants.ts
Normal file
260
node/lib/constants.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
// Generated!
|
||||
|
||||
export enum C {
|
||||
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
|
||||
DC_CERTCK_AUTO = 0,
|
||||
DC_CERTCK_STRICT = 1,
|
||||
DC_CHAT_ID_ALLDONE_HINT = 7,
|
||||
DC_CHAT_ID_ARCHIVED_LINK = 6,
|
||||
DC_CHAT_ID_LAST_SPECIAL = 9,
|
||||
DC_CHAT_ID_TRASH = 3,
|
||||
DC_CHAT_TYPE_BROADCAST = 160,
|
||||
DC_CHAT_TYPE_GROUP = 120,
|
||||
DC_CHAT_TYPE_MAILINGLIST = 140,
|
||||
DC_CHAT_TYPE_SINGLE = 100,
|
||||
DC_CHAT_TYPE_UNDEFINED = 0,
|
||||
DC_CHAT_VISIBILITY_ARCHIVED = 1,
|
||||
DC_CHAT_VISIBILITY_NORMAL = 0,
|
||||
DC_CHAT_VISIBILITY_PINNED = 2,
|
||||
DC_CONNECTIVITY_CONNECTED = 4000,
|
||||
DC_CONNECTIVITY_CONNECTING = 2000,
|
||||
DC_CONNECTIVITY_NOT_CONNECTED = 1000,
|
||||
DC_CONNECTIVITY_WORKING = 3000,
|
||||
DC_CONTACT_ID_DEVICE = 5,
|
||||
DC_CONTACT_ID_INFO = 2,
|
||||
DC_CONTACT_ID_LAST_SPECIAL = 9,
|
||||
DC_CONTACT_ID_SELF = 1,
|
||||
DC_DOWNLOAD_AVAILABLE = 10,
|
||||
DC_DOWNLOAD_DONE = 0,
|
||||
DC_DOWNLOAD_FAILURE = 20,
|
||||
DC_DOWNLOAD_IN_PROGRESS = 1000,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021,
|
||||
DC_EVENT_CHAT_MODIFIED = 2020,
|
||||
DC_EVENT_CONFIGURE_PROGRESS = 2041,
|
||||
DC_EVENT_CONNECTIVITY_CHANGED = 2100,
|
||||
DC_EVENT_CONTACTS_CHANGED = 2030,
|
||||
DC_EVENT_DELETED_BLOB_FILE = 151,
|
||||
DC_EVENT_ERROR = 400,
|
||||
DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410,
|
||||
DC_EVENT_IMAP_CONNECTED = 102,
|
||||
DC_EVENT_IMAP_MESSAGE_DELETED = 104,
|
||||
DC_EVENT_IMAP_MESSAGE_MOVED = 105,
|
||||
DC_EVENT_IMEX_FILE_WRITTEN = 2052,
|
||||
DC_EVENT_IMEX_PROGRESS = 2051,
|
||||
DC_EVENT_INCOMING_MSG = 2005,
|
||||
DC_EVENT_INFO = 100,
|
||||
DC_EVENT_LOCATION_CHANGED = 2035,
|
||||
DC_EVENT_MSGS_CHANGED = 2000,
|
||||
DC_EVENT_MSGS_NOTICED = 2008,
|
||||
DC_EVENT_MSG_DELIVERED = 2010,
|
||||
DC_EVENT_MSG_FAILED = 2012,
|
||||
DC_EVENT_MSG_READ = 2015,
|
||||
DC_EVENT_NEW_BLOB_FILE = 150,
|
||||
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060,
|
||||
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061,
|
||||
DC_EVENT_SELFAVATAR_CHANGED = 2110,
|
||||
DC_EVENT_SMTP_CONNECTED = 101,
|
||||
DC_EVENT_SMTP_MESSAGE_SENT = 103,
|
||||
DC_EVENT_WARNING = 300,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT = 4,
|
||||
DC_GCL_ADD_SELF = 2,
|
||||
DC_GCL_ARCHIVED_ONLY = 1,
|
||||
DC_GCL_FOR_FORWARDING = 8,
|
||||
DC_GCL_NO_SPECIALS = 2,
|
||||
DC_GCL_VERIFIED_ONLY = 1,
|
||||
DC_GCM_ADDDAYMARKER = 1,
|
||||
DC_GCM_INFO_ONLY = 2,
|
||||
DC_IMEX_EXPORT_BACKUP = 11,
|
||||
DC_IMEX_EXPORT_SELF_KEYS = 1,
|
||||
DC_IMEX_IMPORT_BACKUP = 12,
|
||||
DC_IMEX_IMPORT_SELF_KEYS = 2,
|
||||
DC_INFO_PROTECTION_DISABLED = 12,
|
||||
DC_INFO_PROTECTION_ENABLED = 11,
|
||||
DC_KEY_GEN_DEFAULT = 0,
|
||||
DC_KEY_GEN_ED25519 = 2,
|
||||
DC_KEY_GEN_RSA2048 = 1,
|
||||
DC_LP_AUTH_NORMAL = 4,
|
||||
DC_LP_AUTH_OAUTH2 = 2,
|
||||
DC_MEDIA_QUALITY_BALANCED = 0,
|
||||
DC_MEDIA_QUALITY_WORSE = 1,
|
||||
DC_MSG_AUDIO = 40,
|
||||
DC_MSG_FILE = 60,
|
||||
DC_MSG_GIF = 21,
|
||||
DC_MSG_ID_DAYMARKER = 9,
|
||||
DC_MSG_ID_LAST_SPECIAL = 9,
|
||||
DC_MSG_ID_MARKER1 = 1,
|
||||
DC_MSG_IMAGE = 20,
|
||||
DC_MSG_STICKER = 23,
|
||||
DC_MSG_TEXT = 10,
|
||||
DC_MSG_VIDEO = 50,
|
||||
DC_MSG_VIDEOCHAT_INVITATION = 70,
|
||||
DC_MSG_VOICE = 41,
|
||||
DC_MSG_WEBXDC = 80,
|
||||
DC_PROVIDER_STATUS_BROKEN = 3,
|
||||
DC_PROVIDER_STATUS_OK = 1,
|
||||
DC_PROVIDER_STATUS_PREPARATION = 2,
|
||||
DC_QR_ACCOUNT = 250,
|
||||
DC_QR_ADDR = 320,
|
||||
DC_QR_ASK_VERIFYCONTACT = 200,
|
||||
DC_QR_ASK_VERIFYGROUP = 202,
|
||||
DC_QR_ERROR = 400,
|
||||
DC_QR_FPR_MISMATCH = 220,
|
||||
DC_QR_FPR_OK = 210,
|
||||
DC_QR_FPR_WITHOUT_ADDR = 230,
|
||||
DC_QR_REVIVE_VERIFYCONTACT = 510,
|
||||
DC_QR_REVIVE_VERIFYGROUP = 512,
|
||||
DC_QR_TEXT = 330,
|
||||
DC_QR_URL = 332,
|
||||
DC_QR_WEBRTC_INSTANCE = 260,
|
||||
DC_QR_WITHDRAW_VERIFYCONTACT = 500,
|
||||
DC_QR_WITHDRAW_VERIFYGROUP = 502,
|
||||
DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1,
|
||||
DC_SHOW_EMAILS_ALL = 2,
|
||||
DC_SHOW_EMAILS_OFF = 0,
|
||||
DC_SOCKET_AUTO = 0,
|
||||
DC_SOCKET_PLAIN = 3,
|
||||
DC_SOCKET_SSL = 1,
|
||||
DC_SOCKET_STARTTLS = 2,
|
||||
DC_STATE_IN_FRESH = 10,
|
||||
DC_STATE_IN_NOTICED = 13,
|
||||
DC_STATE_IN_SEEN = 16,
|
||||
DC_STATE_OUT_DELIVERED = 26,
|
||||
DC_STATE_OUT_DRAFT = 19,
|
||||
DC_STATE_OUT_FAILED = 24,
|
||||
DC_STATE_OUT_MDN_RCVD = 28,
|
||||
DC_STATE_OUT_PENDING = 20,
|
||||
DC_STATE_OUT_PREPARING = 18,
|
||||
DC_STATE_UNDEFINED = 0,
|
||||
DC_STR_AC_SETUP_MSG_BODY = 43,
|
||||
DC_STR_AC_SETUP_MSG_SUBJECT = 42,
|
||||
DC_STR_ARCHIVEDCHATS = 40,
|
||||
DC_STR_AUDIO = 11,
|
||||
DC_STR_BAD_TIME_MSG_BODY = 85,
|
||||
DC_STR_BROADCAST_LIST = 115,
|
||||
DC_STR_CANNOT_LOGIN = 60,
|
||||
DC_STR_CANTDECRYPT_MSG_BODY = 29,
|
||||
DC_STR_CONFIGURATION_FAILED = 84,
|
||||
DC_STR_CONNECTED = 107,
|
||||
DC_STR_CONNTECTING = 108,
|
||||
DC_STR_CONTACT_NOT_VERIFIED = 36,
|
||||
DC_STR_CONTACT_SETUP_CHANGED = 37,
|
||||
DC_STR_CONTACT_VERIFIED = 35,
|
||||
DC_STR_DEVICE_MESSAGES = 68,
|
||||
DC_STR_DEVICE_MESSAGES_HINT = 70,
|
||||
DC_STR_DOWNLOAD_AVAILABILITY = 100,
|
||||
DC_STR_DRAFT = 3,
|
||||
DC_STR_E2E_AVAILABLE = 25,
|
||||
DC_STR_E2E_PREFERRED = 34,
|
||||
DC_STR_ENCRYPTEDMSG = 24,
|
||||
DC_STR_ENCR_NONE = 28,
|
||||
DC_STR_ENCR_TRANSP = 27,
|
||||
DC_STR_EPHEMERAL_DAY = 79,
|
||||
DC_STR_EPHEMERAL_DAYS = 95,
|
||||
DC_STR_EPHEMERAL_DISABLED = 75,
|
||||
DC_STR_EPHEMERAL_FOUR_WEEKS = 81,
|
||||
DC_STR_EPHEMERAL_HOUR = 78,
|
||||
DC_STR_EPHEMERAL_HOURS = 94,
|
||||
DC_STR_EPHEMERAL_MINUTE = 77,
|
||||
DC_STR_EPHEMERAL_MINUTES = 93,
|
||||
DC_STR_EPHEMERAL_SECONDS = 76,
|
||||
DC_STR_EPHEMERAL_WEEK = 80,
|
||||
DC_STR_EPHEMERAL_WEEKS = 96,
|
||||
DC_STR_ERROR = 112,
|
||||
DC_STR_ERROR_NO_NETWORK = 87,
|
||||
DC_STR_FAILED_SENDING_TO = 74,
|
||||
DC_STR_FILE = 12,
|
||||
DC_STR_FINGERPRINTS = 30,
|
||||
DC_STR_FORWARDED = 97,
|
||||
DC_STR_GIF = 23,
|
||||
DC_STR_IMAGE = 9,
|
||||
DC_STR_INCOMING_MESSAGES = 103,
|
||||
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
|
||||
DC_STR_LOCATION = 66,
|
||||
DC_STR_MESSAGES = 114,
|
||||
DC_STR_MSGACTIONBYME = 63,
|
||||
DC_STR_MSGACTIONBYUSER = 62,
|
||||
DC_STR_MSGADDMEMBER = 17,
|
||||
DC_STR_MSGDELMEMBER = 18,
|
||||
DC_STR_MSGGROUPLEFT = 19,
|
||||
DC_STR_MSGGRPIMGCHANGED = 16,
|
||||
DC_STR_MSGGRPIMGDELETED = 33,
|
||||
DC_STR_MSGGRPNAME = 15,
|
||||
DC_STR_MSGLOCATIONDISABLED = 65,
|
||||
DC_STR_MSGLOCATIONENABLED = 64,
|
||||
DC_STR_NOMESSAGES = 1,
|
||||
DC_STR_NOT_CONNECTED = 121,
|
||||
DC_STR_NOT_SUPPORTED_BY_PROVIDER = 113,
|
||||
DC_STR_ONE_MOMENT = 106,
|
||||
DC_STR_OUTGOING_MESSAGES = 104,
|
||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
|
||||
DC_STR_PART_OF_TOTAL_USED = 116,
|
||||
DC_STR_PROTECTION_DISABLED = 89,
|
||||
DC_STR_PROTECTION_ENABLED = 88,
|
||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
|
||||
DC_STR_READRCPT = 31,
|
||||
DC_STR_READRCPT_MAILBODY = 32,
|
||||
DC_STR_REPLY_NOUN = 90,
|
||||
DC_STR_SAVED_MESSAGES = 69,
|
||||
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
|
||||
DC_STR_SECURE_JOIN_REPLIES = 118,
|
||||
DC_STR_SECURE_JOIN_STARTED = 117,
|
||||
DC_STR_SELF = 2,
|
||||
DC_STR_SELF_DELETED_MSG_BODY = 91,
|
||||
DC_STR_SENDING = 110,
|
||||
DC_STR_SERVER_TURNED_OFF = 92,
|
||||
DC_STR_SETUP_CONTACT_QR_DESC = 119,
|
||||
DC_STR_STICKER = 67,
|
||||
DC_STR_STORAGE_ON_DOMAIN = 105,
|
||||
DC_STR_SUBJECT_FOR_NEW_CONTACT = 73,
|
||||
DC_STR_SYNC_MSG_BODY = 102,
|
||||
DC_STR_SYNC_MSG_SUBJECT = 101,
|
||||
DC_STR_UNKNOWN_SENDER_FOR_CHAT = 72,
|
||||
DC_STR_UPDATE_REMINDER_MSG_BODY = 86,
|
||||
DC_STR_UPDATING = 109,
|
||||
DC_STR_VIDEO = 10,
|
||||
DC_STR_VIDEOCHAT_INVITATION = 82,
|
||||
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
|
||||
DC_STR_VOICEMESSAGE = 7,
|
||||
DC_STR_WELCOME_MESSAGE = 71,
|
||||
DC_TEXT1_DRAFT = 1,
|
||||
DC_TEXT1_SELF = 3,
|
||||
DC_TEXT1_USERNAME = 2,
|
||||
DC_VIDEOCHATTYPE_BASICWEBRTC = 1,
|
||||
DC_VIDEOCHATTYPE_JITSI = 2,
|
||||
DC_VIDEOCHATTYPE_UNKNOWN = 0,
|
||||
}
|
||||
|
||||
// Generated!
|
||||
|
||||
export const EventId2EventName: { [key: number]: string } = {
|
||||
100: 'DC_EVENT_INFO',
|
||||
101: 'DC_EVENT_SMTP_CONNECTED',
|
||||
102: 'DC_EVENT_IMAP_CONNECTED',
|
||||
103: 'DC_EVENT_SMTP_MESSAGE_SENT',
|
||||
104: 'DC_EVENT_IMAP_MESSAGE_DELETED',
|
||||
105: 'DC_EVENT_IMAP_MESSAGE_MOVED',
|
||||
150: 'DC_EVENT_NEW_BLOB_FILE',
|
||||
151: 'DC_EVENT_DELETED_BLOB_FILE',
|
||||
300: 'DC_EVENT_WARNING',
|
||||
400: 'DC_EVENT_ERROR',
|
||||
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||
2005: 'DC_EVENT_INCOMING_MSG',
|
||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||
2010: 'DC_EVENT_MSG_DELIVERED',
|
||||
2012: 'DC_EVENT_MSG_FAILED',
|
||||
2015: 'DC_EVENT_MSG_READ',
|
||||
2020: 'DC_EVENT_CHAT_MODIFIED',
|
||||
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
|
||||
2030: 'DC_EVENT_CONTACTS_CHANGED',
|
||||
2035: 'DC_EVENT_LOCATION_CHANGED',
|
||||
2041: 'DC_EVENT_CONFIGURE_PROGRESS',
|
||||
2051: 'DC_EVENT_IMEX_PROGRESS',
|
||||
2052: 'DC_EVENT_IMEX_FILE_WRITTEN',
|
||||
2060: 'DC_EVENT_SECUREJOIN_INVITER_PROGRESS',
|
||||
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
|
||||
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
|
||||
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
}
|
||||
94
node/lib/contact.ts
Normal file
94
node/lib/contact.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { integerToHexColor } from './util'
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import binding from './binding'
|
||||
const debug = require('debug')('deltachat:node:contact')
|
||||
|
||||
interface NativeContact {}
|
||||
/**
|
||||
* Wrapper around dc_contact_t*
|
||||
*/
|
||||
export class Contact {
|
||||
constructor(public dc_contact: NativeContact) {
|
||||
debug('Contact constructor')
|
||||
if (dc_contact === null) {
|
||||
throw new Error('native contact can not be null')
|
||||
}
|
||||
}
|
||||
|
||||
toJson() {
|
||||
debug('toJson')
|
||||
return {
|
||||
address: this.getAddress(),
|
||||
color: this.color,
|
||||
authName: this.authName,
|
||||
status: this.status,
|
||||
displayName: this.getDisplayName(),
|
||||
id: this.getId(),
|
||||
lastSeen: this.lastSeen,
|
||||
name: this.getName(),
|
||||
profileImage: this.getProfileImage(),
|
||||
nameAndAddr: this.getNameAndAddress(),
|
||||
isBlocked: this.isBlocked(),
|
||||
isVerified: this.isVerified(),
|
||||
}
|
||||
}
|
||||
|
||||
getAddress(): string {
|
||||
return binding.dcn_contact_get_addr(this.dc_contact)
|
||||
}
|
||||
|
||||
/** Get original contact name.
|
||||
* This is the name of the contact as defined by the contact themself.
|
||||
* If the contact themself does not define such a name,
|
||||
* an empty string is returned. */
|
||||
get authName(): string {
|
||||
return binding.dcn_contact_get_auth_name(this.dc_contact)
|
||||
}
|
||||
|
||||
get color(): string {
|
||||
return integerToHexColor(binding.dcn_contact_get_color(this.dc_contact))
|
||||
}
|
||||
|
||||
/**
|
||||
* contact's status
|
||||
*
|
||||
* Status is the last signature received in a message from this contact.
|
||||
*/
|
||||
get status(): string {
|
||||
return binding.dcn_contact_get_status(this.dc_contact)
|
||||
}
|
||||
|
||||
getDisplayName(): string {
|
||||
return binding.dcn_contact_get_display_name(this.dc_contact)
|
||||
}
|
||||
|
||||
getId(): number {
|
||||
return binding.dcn_contact_get_id(this.dc_contact)
|
||||
}
|
||||
|
||||
get lastSeen(): number {
|
||||
return binding.dcn_contact_get_last_seen(this.dc_contact)
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return binding.dcn_contact_get_name(this.dc_contact)
|
||||
}
|
||||
|
||||
getNameAndAddress(): string {
|
||||
return binding.dcn_contact_get_name_n_addr(this.dc_contact)
|
||||
}
|
||||
|
||||
getProfileImage(): string {
|
||||
return binding.dcn_contact_get_profile_image(this.dc_contact)
|
||||
}
|
||||
|
||||
isBlocked() {
|
||||
return Boolean(binding.dcn_contact_is_blocked(this.dc_contact))
|
||||
}
|
||||
|
||||
isVerified() {
|
||||
return Boolean(binding.dcn_contact_is_verified(this.dc_contact))
|
||||
}
|
||||
}
|
||||
955
node/lib/context.ts
Normal file
955
node/lib/context.ts
Normal file
@@ -0,0 +1,955 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import binding from './binding'
|
||||
import { C, EventId2EventName } from './constants'
|
||||
import { Chat } from './chat'
|
||||
import { ChatList } from './chatlist'
|
||||
import { Contact } from './contact'
|
||||
import { Message } from './message'
|
||||
import { Lot } from './lot'
|
||||
import { Locations } from './locations'
|
||||
import rawDebug from 'debug'
|
||||
import { AccountManager } from './deltachat'
|
||||
import { join } from 'path'
|
||||
import { EventEmitter } from 'stream'
|
||||
const debug = rawDebug('deltachat:node:index')
|
||||
|
||||
const noop = function () {}
|
||||
interface NativeContext {}
|
||||
|
||||
/**
|
||||
* Wrapper around dcn_context_t*
|
||||
*
|
||||
* only acts as event emitter when created in standalone mode (without account manager)
|
||||
* with `Context.open`
|
||||
*/
|
||||
export class Context extends EventEmitter {
|
||||
constructor(
|
||||
readonly manager: AccountManager | null,
|
||||
private inner_dcn_context: NativeContext,
|
||||
readonly account_id: number | null
|
||||
) {
|
||||
super()
|
||||
debug('DeltaChat constructor')
|
||||
if (inner_dcn_context === null) {
|
||||
throw new Error('inner_dcn_context can not be null')
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens a stanalone context (without an account manager)
|
||||
* automatically starts the event handler */
|
||||
static open(cwd: string): Context {
|
||||
const dbFile = join(cwd, 'db.sqlite')
|
||||
const context = new Context(null, binding.dcn_context_new(dbFile), null)
|
||||
debug('Opened context')
|
||||
function handleCoreEvent(
|
||||
eventId: number,
|
||||
data1: number,
|
||||
data2: number | string
|
||||
) {
|
||||
const eventString = EventId2EventName[eventId]
|
||||
debug(eventString, data1, data2)
|
||||
if (!context.emit) {
|
||||
console.log('Received an event but EventEmitter is already destroyed.')
|
||||
console.log(eventString, data1, data2)
|
||||
return
|
||||
}
|
||||
context.emit(eventString, data1, data2)
|
||||
context.emit('ALL', eventString, data1, data2)
|
||||
}
|
||||
binding.dcn_start_event_handler(
|
||||
context.dcn_context,
|
||||
handleCoreEvent.bind(this)
|
||||
)
|
||||
debug('Started event handler')
|
||||
return context
|
||||
}
|
||||
|
||||
get dcn_context() {
|
||||
return this.inner_dcn_context
|
||||
}
|
||||
|
||||
get is_open() {
|
||||
return Boolean(binding.dcn_context_is_open())
|
||||
}
|
||||
|
||||
open(passphrase?: string) {
|
||||
return Boolean(
|
||||
binding.dcn_context_open(this.dcn_context, passphrase ? passphrase : '')
|
||||
)
|
||||
}
|
||||
|
||||
unref() {
|
||||
binding.dcn_context_unref(this.dcn_context)
|
||||
;(this.inner_dcn_context as any) = null
|
||||
}
|
||||
|
||||
acceptChat(chatId: number) {
|
||||
binding.dcn_accept_chat(this.dcn_context, chatId)
|
||||
}
|
||||
|
||||
blockChat(chatId: number) {
|
||||
binding.dcn_block_chat(this.dcn_context, chatId)
|
||||
}
|
||||
|
||||
addAddressBook(addressBook: string) {
|
||||
debug(`addAddressBook ${addressBook}`)
|
||||
return binding.dcn_add_address_book(this.dcn_context, addressBook)
|
||||
}
|
||||
|
||||
addContactToChat(chatId: number, contactId: number) {
|
||||
debug(`addContactToChat ${chatId} ${contactId}`)
|
||||
return Boolean(
|
||||
binding.dcn_add_contact_to_chat(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
Number(contactId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
addDeviceMessage(label: string, msg: Message | string) {
|
||||
debug(`addDeviceMessage ${label} ${msg}`)
|
||||
if (!msg) {
|
||||
throw new Error('invalid msg parameter')
|
||||
}
|
||||
if (typeof label !== 'string') {
|
||||
throw new Error('invalid label parameter, must be a string')
|
||||
}
|
||||
if (typeof msg === 'string') {
|
||||
const msgObj = this.messageNew()
|
||||
msgObj.setText(msg)
|
||||
msg = msgObj
|
||||
}
|
||||
if (!msg.dc_msg) {
|
||||
throw new Error('invalid msg object')
|
||||
}
|
||||
return binding.dcn_add_device_msg(this.dcn_context, label, msg.dc_msg)
|
||||
}
|
||||
|
||||
setChatVisibility(
|
||||
chatId: number,
|
||||
visibility:
|
||||
| C.DC_CHAT_VISIBILITY_NORMAL
|
||||
| C.DC_CHAT_VISIBILITY_ARCHIVED
|
||||
| C.DC_CHAT_VISIBILITY_PINNED
|
||||
) {
|
||||
debug(`setChatVisibility ${chatId} ${visibility}`)
|
||||
binding.dcn_set_chat_visibility(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
visibility
|
||||
)
|
||||
}
|
||||
|
||||
blockContact(contactId: number, block: boolean) {
|
||||
debug(`blockContact ${contactId} ${block}`)
|
||||
binding.dcn_block_contact(
|
||||
this.dcn_context,
|
||||
Number(contactId),
|
||||
block ? 1 : 0
|
||||
)
|
||||
}
|
||||
|
||||
checkQrCode(qrCode: string) {
|
||||
debug(`checkQrCode ${qrCode}`)
|
||||
const dc_lot = binding.dcn_check_qr(this.dcn_context, qrCode)
|
||||
let result = dc_lot ? new Lot(dc_lot) : null
|
||||
if (result) {
|
||||
return { id: result.getId(), ...result.toJson() }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
configure(opts: any): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
debug('configure')
|
||||
|
||||
const onSuccess = () => {
|
||||
removeListeners()
|
||||
resolve()
|
||||
}
|
||||
const onFail = (error: string) => {
|
||||
removeListeners()
|
||||
reject(new Error(error))
|
||||
}
|
||||
|
||||
let onConfigure: (...args: any[]) => void
|
||||
if (this.account_id === null) {
|
||||
onConfigure = (data1: number, data2: string) => {
|
||||
if (data1 === 0) return onFail(data2)
|
||||
else if (data1 === 1000) return onSuccess()
|
||||
}
|
||||
} else {
|
||||
onConfigure = (accountId: number, data1: number, data2: string) => {
|
||||
if (this.account_id !== accountId) {
|
||||
return
|
||||
}
|
||||
if (data1 === 0) return onFail(data2)
|
||||
else if (data1 === 1000) return onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
const removeListeners = () => {
|
||||
;(this.manager || this).removeListener(
|
||||
'DC_EVENT_CONFIGURE_PROGRESS',
|
||||
onConfigure
|
||||
)
|
||||
}
|
||||
|
||||
const registerListeners = () => {
|
||||
;(this.manager || this).on('DC_EVENT_CONFIGURE_PROGRESS', onConfigure)
|
||||
}
|
||||
|
||||
registerListeners()
|
||||
|
||||
if (!opts) opts = {}
|
||||
Object.keys(opts).forEach((key) => {
|
||||
const value = opts[key]
|
||||
this.setConfig(key, value)
|
||||
})
|
||||
|
||||
binding.dcn_configure(this.dcn_context)
|
||||
})
|
||||
}
|
||||
|
||||
continueKeyTransfer(messageId: number, setupCode: string) {
|
||||
debug(`continueKeyTransfer ${messageId}`)
|
||||
return new Promise((resolve, reject) => {
|
||||
binding.dcn_continue_key_transfer(
|
||||
this.dcn_context,
|
||||
Number(messageId),
|
||||
setupCode,
|
||||
(result: number) => resolve(result === 1)
|
||||
)
|
||||
})
|
||||
}
|
||||
/** @returns chatId */
|
||||
createBroadcastList(): number {
|
||||
debug(`createBroadcastList`)
|
||||
return binding.dcn_create_broadcast_list(this.dcn_context)
|
||||
}
|
||||
|
||||
/** @returns chatId */
|
||||
createChatByContactId(contactId: number): number {
|
||||
debug(`createChatByContactId ${contactId}`)
|
||||
return binding.dcn_create_chat_by_contact_id(
|
||||
this.dcn_context,
|
||||
Number(contactId)
|
||||
)
|
||||
}
|
||||
|
||||
/** @returns contactId */
|
||||
createContact(name: string, addr: string): number {
|
||||
debug(`createContact ${name} ${addr}`)
|
||||
return binding.dcn_create_contact(this.dcn_context, name, addr)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param chatName The name of the chat that should be created
|
||||
* @param is_protected Whether the chat should be protected at creation time
|
||||
* @returns chatId
|
||||
*/
|
||||
createGroupChat(chatName: string, is_protected: boolean = false): number {
|
||||
debug(`createGroupChat ${chatName} [protected:${is_protected}]`)
|
||||
return binding.dcn_create_group_chat(
|
||||
this.dcn_context,
|
||||
is_protected ? 1 : 0,
|
||||
chatName
|
||||
)
|
||||
}
|
||||
|
||||
deleteChat(chatId: number) {
|
||||
debug(`deleteChat ${chatId}`)
|
||||
binding.dcn_delete_chat(this.dcn_context, Number(chatId))
|
||||
}
|
||||
|
||||
deleteContact(contactId: number) {
|
||||
debug(`deleteContact ${contactId}`)
|
||||
return Boolean(
|
||||
binding.dcn_delete_contact(this.dcn_context, Number(contactId))
|
||||
)
|
||||
}
|
||||
|
||||
deleteMessages(messageIds: number[]) {
|
||||
if (!Array.isArray(messageIds)) {
|
||||
messageIds = [messageIds]
|
||||
}
|
||||
messageIds = messageIds.map((id) => Number(id))
|
||||
debug('deleteMessages', messageIds)
|
||||
binding.dcn_delete_msgs(this.dcn_context, messageIds)
|
||||
}
|
||||
|
||||
forwardMessages(messageIds: number[], chatId: number) {
|
||||
if (!Array.isArray(messageIds)) {
|
||||
messageIds = [messageIds]
|
||||
}
|
||||
messageIds = messageIds.map((id) => Number(id))
|
||||
debug('forwardMessages', messageIds)
|
||||
binding.dcn_forward_msgs(this.dcn_context, messageIds, chatId)
|
||||
}
|
||||
|
||||
getBlobdir(): string {
|
||||
debug('getBlobdir')
|
||||
return binding.dcn_get_blobdir(this.dcn_context)
|
||||
}
|
||||
|
||||
getBlockedCount(): number {
|
||||
debug('getBlockedCount')
|
||||
return binding.dcn_get_blocked_cnt(this.dcn_context)
|
||||
}
|
||||
|
||||
getBlockedContacts(): number[] {
|
||||
debug('getBlockedContacts')
|
||||
return binding.dcn_get_blocked_contacts(this.dcn_context)
|
||||
}
|
||||
|
||||
getChat(chatId: number) {
|
||||
debug(`getChat ${chatId}`)
|
||||
const dc_chat = binding.dcn_get_chat(this.dcn_context, Number(chatId))
|
||||
return dc_chat ? new Chat(dc_chat) : null
|
||||
}
|
||||
|
||||
getChatContacts(chatId: number): number[] {
|
||||
debug(`getChatContacts ${chatId}`)
|
||||
return binding.dcn_get_chat_contacts(this.dcn_context, Number(chatId))
|
||||
}
|
||||
|
||||
getChatIdByContactId(contactId: number): number {
|
||||
debug(`getChatIdByContactId ${contactId}`)
|
||||
return binding.dcn_get_chat_id_by_contact_id(
|
||||
this.dcn_context,
|
||||
Number(contactId)
|
||||
)
|
||||
}
|
||||
|
||||
getChatMedia(
|
||||
chatId: number,
|
||||
msgType1: number,
|
||||
msgType2: number,
|
||||
msgType3: number
|
||||
): number[] {
|
||||
debug(`getChatMedia ${chatId}`)
|
||||
return binding.dcn_get_chat_media(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
msgType1,
|
||||
msgType2 || 0,
|
||||
msgType3 || 0
|
||||
)
|
||||
}
|
||||
|
||||
getMimeHeaders(messageId: number): string {
|
||||
debug(`getMimeHeaders ${messageId}`)
|
||||
return binding.dcn_get_mime_headers(this.dcn_context, Number(messageId))
|
||||
}
|
||||
|
||||
getChatlistItemSummary(chatId: number, messageId: number) {
|
||||
debug(`getChatlistItemSummary ${chatId} ${messageId}`)
|
||||
return new Lot(
|
||||
binding.dcn_chatlist_get_summary2(this.dcn_context, chatId, messageId)
|
||||
)
|
||||
}
|
||||
|
||||
getChatMessages(chatId: number, flags: number, marker1before: number) {
|
||||
debug(`getChatMessages ${chatId} ${flags} ${marker1before}`)
|
||||
return binding.dcn_get_chat_msgs(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
flags,
|
||||
marker1before
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get encryption info for a chat.
|
||||
* Get a multi-line encryption info, containing encryption preferences of all members.
|
||||
* Can be used to find out why messages sent to group are not encrypted.
|
||||
*
|
||||
* @param chatId ID of the chat to get the encryption info for.
|
||||
* @return Multi-line text, must be released using dc_str_unref() after usage.
|
||||
*/
|
||||
getChatEncrytionInfo(chatId: number): string {
|
||||
return binding.dcn_get_chat_encrinfo(this.dcn_context, chatId)
|
||||
}
|
||||
|
||||
getChats(listFlags: number, queryStr: string, queryContactId: number) {
|
||||
debug('getChats')
|
||||
const result = []
|
||||
const list = this.getChatList(listFlags, queryStr, queryContactId)
|
||||
const count = list.getCount()
|
||||
for (let i = 0; i < count; i++) {
|
||||
result.push(list.getChatId(i))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
getChatList(listFlags: number, queryStr: string, queryContactId: number) {
|
||||
listFlags = listFlags || 0
|
||||
queryStr = queryStr || ''
|
||||
queryContactId = queryContactId || 0
|
||||
debug(`getChatList ${listFlags} ${queryStr} ${queryContactId}`)
|
||||
return new ChatList(
|
||||
binding.dcn_get_chatlist(
|
||||
this.dcn_context,
|
||||
listFlags,
|
||||
queryStr,
|
||||
Number(queryContactId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
getConfig(key: string): string {
|
||||
debug(`getConfig ${key}`)
|
||||
return binding.dcn_get_config(this.dcn_context, key)
|
||||
}
|
||||
|
||||
getContact(contactId: number) {
|
||||
debug(`getContact ${contactId}`)
|
||||
const dc_contact = binding.dcn_get_contact(
|
||||
this.dcn_context,
|
||||
Number(contactId)
|
||||
)
|
||||
return dc_contact ? new Contact(dc_contact) : null
|
||||
}
|
||||
|
||||
getContactEncryptionInfo(contactId: number) {
|
||||
debug(`getContactEncryptionInfo ${contactId}`)
|
||||
return binding.dcn_get_contact_encrinfo(this.dcn_context, Number(contactId))
|
||||
}
|
||||
|
||||
getContacts(listFlags: number, query: string) {
|
||||
listFlags = listFlags || 0
|
||||
query = query || ''
|
||||
debug(`getContacts ${listFlags} ${query}`)
|
||||
return binding.dcn_get_contacts(this.dcn_context, listFlags, query)
|
||||
}
|
||||
|
||||
wasDeviceMessageEverAdded(label: string) {
|
||||
debug(`wasDeviceMessageEverAdded ${label}`)
|
||||
const added = binding.dcn_was_device_msg_ever_added(this.dcn_context, label)
|
||||
return added === 1
|
||||
}
|
||||
|
||||
getDraft(chatId: number) {
|
||||
debug(`getDraft ${chatId}`)
|
||||
const dc_msg = binding.dcn_get_draft(this.dcn_context, Number(chatId))
|
||||
return dc_msg ? new Message(dc_msg) : null
|
||||
}
|
||||
|
||||
getFreshMessageCount(chatId: number): number {
|
||||
debug(`getFreshMessageCount ${chatId}`)
|
||||
return binding.dcn_get_fresh_msg_cnt(this.dcn_context, Number(chatId))
|
||||
}
|
||||
|
||||
getFreshMessages() {
|
||||
debug('getFreshMessages')
|
||||
return binding.dcn_get_fresh_msgs(this.dcn_context)
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
debug('getInfo')
|
||||
const info = binding.dcn_get_info(this.dcn_context)
|
||||
return AccountManager.parseGetInfo(info)
|
||||
}
|
||||
|
||||
getMessage(messageId: number) {
|
||||
debug(`getMessage ${messageId}`)
|
||||
const dc_msg = binding.dcn_get_msg(this.dcn_context, Number(messageId))
|
||||
return dc_msg ? new Message(dc_msg) : null
|
||||
}
|
||||
|
||||
getMessageCount(chatId: number): number {
|
||||
debug(`getMessageCount ${chatId}`)
|
||||
return binding.dcn_get_msg_cnt(this.dcn_context, Number(chatId))
|
||||
}
|
||||
|
||||
getMessageInfo(messageId: number): string {
|
||||
debug(`getMessageInfo ${messageId}`)
|
||||
return binding.dcn_get_msg_info(this.dcn_context, Number(messageId))
|
||||
}
|
||||
|
||||
getMessageHTML(messageId: number): string {
|
||||
debug(`getMessageHTML ${messageId}`)
|
||||
return binding.dcn_get_msg_html(this.dcn_context, Number(messageId))
|
||||
}
|
||||
|
||||
getNextMediaMessage(
|
||||
messageId: number,
|
||||
msgType1: number,
|
||||
msgType2: number,
|
||||
msgType3: number
|
||||
) {
|
||||
debug(
|
||||
`getNextMediaMessage ${messageId} ${msgType1} ${msgType2} ${msgType3}`
|
||||
)
|
||||
return this._getNextMedia(messageId, 1, msgType1, msgType2, msgType3)
|
||||
}
|
||||
|
||||
getPreviousMediaMessage(
|
||||
messageId: number,
|
||||
msgType1: number,
|
||||
msgType2: number,
|
||||
msgType3: number
|
||||
) {
|
||||
debug(
|
||||
`getPreviousMediaMessage ${messageId} ${msgType1} ${msgType2} ${msgType3}`
|
||||
)
|
||||
return this._getNextMedia(messageId, -1, msgType1, msgType2, msgType3)
|
||||
}
|
||||
|
||||
_getNextMedia(
|
||||
messageId: number,
|
||||
dir: number,
|
||||
msgType1: number,
|
||||
msgType2: number,
|
||||
msgType3: number
|
||||
): number {
|
||||
return binding.dcn_get_next_media(
|
||||
this.dcn_context,
|
||||
Number(messageId),
|
||||
dir,
|
||||
msgType1 || 0,
|
||||
msgType2 || 0,
|
||||
msgType3 || 0
|
||||
)
|
||||
}
|
||||
|
||||
getSecurejoinQrCode(chatId: number): string {
|
||||
debug(`getSecurejoinQrCode ${chatId}`)
|
||||
return binding.dcn_get_securejoin_qr(this.dcn_context, Number(chatId))
|
||||
}
|
||||
|
||||
getSecurejoinQrCodeSVG(chatId: number): string {
|
||||
debug(`getSecurejoinQrCodeSVG ${chatId}`)
|
||||
return binding.dcn_get_securejoin_qr_svg(this.dcn_context, chatId)
|
||||
}
|
||||
|
||||
startIO(): void {
|
||||
debug(`startIO`)
|
||||
binding.dcn_start_io(this.dcn_context)
|
||||
}
|
||||
|
||||
stopIO(): void {
|
||||
debug(`stopIO`)
|
||||
binding.dcn_stop_io(this.dcn_context)
|
||||
}
|
||||
|
||||
stopOngoingProcess(): void {
|
||||
debug(`stopOngoingProcess`)
|
||||
binding.dcn_stop_ongoing_process(this.dcn_context)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @deprectated please use `AccountManager.getSystemInfo()` instead
|
||||
*/
|
||||
static getSystemInfo() {
|
||||
return AccountManager.getSystemInfo()
|
||||
}
|
||||
|
||||
getConnectivity(): number {
|
||||
return binding.dcn_get_connectivity(this.dcn_context)
|
||||
}
|
||||
|
||||
getConnectivityHTML(): String {
|
||||
return binding.dcn_get_connectivity_html(this.dcn_context)
|
||||
}
|
||||
|
||||
importExport(what: number, param1: string, param2 = '') {
|
||||
debug(`importExport ${what} ${param1} ${param2}`)
|
||||
binding.dcn_imex(this.dcn_context, what, param1, param2)
|
||||
}
|
||||
|
||||
importExportHasBackup(dir: string) {
|
||||
debug(`importExportHasBackup ${dir}`)
|
||||
return binding.dcn_imex_has_backup(this.dcn_context, dir)
|
||||
}
|
||||
|
||||
initiateKeyTransfer(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
debug('initiateKeyTransfer2')
|
||||
binding.dcn_initiate_key_transfer(this.dcn_context, resolve)
|
||||
})
|
||||
}
|
||||
|
||||
isConfigured() {
|
||||
debug('isConfigured')
|
||||
return Boolean(binding.dcn_is_configured(this.dcn_context))
|
||||
}
|
||||
|
||||
isContactInChat(chatId: number, contactId: number) {
|
||||
debug(`isContactInChat ${chatId} ${contactId}`)
|
||||
return Boolean(
|
||||
binding.dcn_is_contact_in_chat(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
Number(contactId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns resulting chat id or 0 on error
|
||||
*/
|
||||
joinSecurejoin(qrCode: string): number {
|
||||
debug(`joinSecurejoin ${qrCode}`)
|
||||
return binding.dcn_join_securejoin(this.dcn_context, qrCode)
|
||||
}
|
||||
|
||||
lookupContactIdByAddr(addr: string): number {
|
||||
debug(`lookupContactIdByAddr ${addr}`)
|
||||
return binding.dcn_lookup_contact_id_by_addr(this.dcn_context, addr)
|
||||
}
|
||||
|
||||
markNoticedChat(chatId: number) {
|
||||
debug(`markNoticedChat ${chatId}`)
|
||||
binding.dcn_marknoticed_chat(this.dcn_context, Number(chatId))
|
||||
}
|
||||
|
||||
markSeenMessages(messageIds: number[]) {
|
||||
if (!Array.isArray(messageIds)) {
|
||||
messageIds = [messageIds]
|
||||
}
|
||||
messageIds = messageIds.map((id) => Number(id))
|
||||
debug('markSeenMessages', messageIds)
|
||||
binding.dcn_markseen_msgs(this.dcn_context, messageIds)
|
||||
}
|
||||
|
||||
maybeNetwork() {
|
||||
debug('maybeNetwork')
|
||||
binding.dcn_maybe_network(this.dcn_context)
|
||||
}
|
||||
|
||||
messageNew(viewType = C.DC_MSG_TEXT) {
|
||||
debug(`messageNew ${viewType}`)
|
||||
return new Message(binding.dcn_msg_new(this.dcn_context, viewType))
|
||||
}
|
||||
|
||||
removeContactFromChat(chatId: number, contactId: number) {
|
||||
debug(`removeContactFromChat ${chatId} ${contactId}`)
|
||||
return Boolean(
|
||||
binding.dcn_remove_contact_from_chat(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
Number(contactId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param chatId ID of the chat to search messages in. Set this to 0 for a global search.
|
||||
* @param query The query to search for.
|
||||
*/
|
||||
searchMessages(chatId: number, query: string): number[] {
|
||||
debug(`searchMessages ${chatId} ${query}`)
|
||||
return binding.dcn_search_msgs(this.dcn_context, Number(chatId), query)
|
||||
}
|
||||
|
||||
sendMessage(chatId: number, msg: string | Message) {
|
||||
debug(`sendMessage ${chatId}`)
|
||||
if (!msg) {
|
||||
throw new Error('invalid msg parameter')
|
||||
}
|
||||
if (typeof msg === 'string') {
|
||||
const msgObj = this.messageNew()
|
||||
msgObj.setText(msg)
|
||||
msg = msgObj
|
||||
}
|
||||
if (!msg.dc_msg) {
|
||||
throw new Error('invalid msg object')
|
||||
}
|
||||
return binding.dcn_send_msg(this.dcn_context, Number(chatId), msg.dc_msg)
|
||||
}
|
||||
|
||||
downloadFullMessage(messageId: number) {
|
||||
binding.dcn_download_full_msg(this.dcn_context, messageId)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<number>} Promise that resolves into the resulting message id
|
||||
*/
|
||||
sendVideochatInvitation(chatId: number): Promise<number> {
|
||||
debug(`sendVideochatInvitation ${chatId}`)
|
||||
return new Promise((resolve, reject) => {
|
||||
binding.dcn_send_videochat_invitation(
|
||||
this.dcn_context,
|
||||
chatId,
|
||||
(result: number) => {
|
||||
if (result !== 0) {
|
||||
resolve(result)
|
||||
} else {
|
||||
reject(
|
||||
'Videochatinvitation failed to send, see error events for detailed info'
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
setChatName(chatId: number, name: string) {
|
||||
debug(`setChatName ${chatId} ${name}`)
|
||||
return Boolean(
|
||||
binding.dcn_set_chat_name(this.dcn_context, Number(chatId), name)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param chatId
|
||||
* @param protect
|
||||
* @returns success boolean
|
||||
*/
|
||||
setChatProtection(chatId: number, protect: boolean) {
|
||||
debug(`setChatProtection ${chatId} ${protect}`)
|
||||
return Boolean(
|
||||
binding.dcn_set_chat_protection(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
protect ? 1 : 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
getChatEphemeralTimer(chatId: number): number {
|
||||
debug(`getChatEphemeralTimer ${chatId}`)
|
||||
return binding.dcn_get_chat_ephemeral_timer(
|
||||
this.dcn_context,
|
||||
Number(chatId)
|
||||
)
|
||||
}
|
||||
|
||||
setChatEphemeralTimer(chatId: number, timer: number) {
|
||||
debug(`setChatEphemeralTimer ${chatId} ${timer}`)
|
||||
return Boolean(
|
||||
binding.dcn_set_chat_ephemeral_timer(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
Number(timer)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
setChatProfileImage(chatId: number, image: string) {
|
||||
debug(`setChatProfileImage ${chatId} ${image}`)
|
||||
return Boolean(
|
||||
binding.dcn_set_chat_profile_image(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
image || ''
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
setConfig(key: string, value: string | boolean | number): number {
|
||||
debug(`setConfig (string) ${key} ${value}`)
|
||||
if (value === null) {
|
||||
return binding.dcn_set_config_null(this.dcn_context, key)
|
||||
} else {
|
||||
if (typeof value === 'boolean') {
|
||||
value = value === true ? '1' : '0'
|
||||
} else if (typeof value === 'number') {
|
||||
value = String(value)
|
||||
}
|
||||
return binding.dcn_set_config(this.dcn_context, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
setConfigFromQr(qrcodeContent: string): boolean {
|
||||
return Boolean(
|
||||
binding.dcn_set_config_from_qr(this.dcn_context, qrcodeContent)
|
||||
)
|
||||
}
|
||||
|
||||
estimateDeletionCount(fromServer: boolean, seconds: number): number {
|
||||
debug(`estimateDeletionCount fromServer: ${fromServer} seconds: ${seconds}`)
|
||||
return binding.dcn_estimate_deletion_cnt(
|
||||
this.dcn_context,
|
||||
fromServer === true ? 1 : 0,
|
||||
seconds
|
||||
)
|
||||
}
|
||||
|
||||
setStockTranslation(stockId: number, stockMsg: string) {
|
||||
debug(`setStockTranslation ${stockId} ${stockMsg}`)
|
||||
return Boolean(
|
||||
binding.dcn_set_stock_translation(
|
||||
this.dcn_context,
|
||||
Number(stockId),
|
||||
stockMsg
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
setDraft(chatId: number, msg: Message | null) {
|
||||
debug(`setDraft ${chatId}`)
|
||||
binding.dcn_set_draft(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
msg ? msg.dc_msg : null
|
||||
)
|
||||
}
|
||||
|
||||
setLocation(latitude: number, longitude: number, accuracy: number) {
|
||||
debug(`setLocation ${latitude}`)
|
||||
binding.dcn_set_location(
|
||||
this.dcn_context,
|
||||
Number(latitude),
|
||||
Number(longitude),
|
||||
Number(accuracy)
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* @param chatId Chat-id to get location information for.
|
||||
* 0 to get locations independently of the chat.
|
||||
* @param contactId Contact id to get location information for.
|
||||
* If also a chat-id is given, this should be a member of the given chat.
|
||||
* 0 to get locations independently of the contact.
|
||||
* @param timestampFrom Start of timespan to return.
|
||||
* Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC.
|
||||
* 0 for "start from the beginning".
|
||||
* @param timestampTo End of timespan to return.
|
||||
* Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC.
|
||||
* 0 for "all up to now".
|
||||
* @return Array of locations, NULL is never returned.
|
||||
* The array is sorted decending;
|
||||
* the first entry in the array is the location with the newest timestamp.
|
||||
*
|
||||
* Examples:
|
||||
* // get locations from the last hour for a global map
|
||||
* getLocations(0, 0, time(NULL)-60*60, 0);
|
||||
*
|
||||
* // get locations from a contact for a global map
|
||||
* getLocations(0, contact_id, 0, 0);
|
||||
*
|
||||
* // get all locations known for a given chat
|
||||
* getLocations(chat_id, 0, 0, 0);
|
||||
*
|
||||
* // get locations from a single contact for a given chat
|
||||
* getLocations(chat_id, contact_id, 0, 0);
|
||||
*/
|
||||
|
||||
getLocations(
|
||||
chatId: number,
|
||||
contactId: number,
|
||||
timestampFrom = 0,
|
||||
timestampTo = 0
|
||||
) {
|
||||
const locations = new Locations(
|
||||
binding.dcn_get_locations(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
Number(contactId),
|
||||
timestampFrom,
|
||||
timestampTo
|
||||
)
|
||||
)
|
||||
return locations.toJson()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param duration The duration (0 for no mute, -1 for forever mute, everything else is is the relative mute duration from now in seconds)
|
||||
*/
|
||||
setChatMuteDuration(chatId: number, duration: number) {
|
||||
return Boolean(
|
||||
binding.dcn_set_chat_mute_duration(this.dcn_context, chatId, duration)
|
||||
)
|
||||
}
|
||||
|
||||
/** get information about the provider */
|
||||
getProviderFromEmail(email: string) {
|
||||
debug('DeltaChat.getProviderFromEmail')
|
||||
const provider = binding.dcn_provider_new_from_email(
|
||||
this.dcn_context,
|
||||
email
|
||||
)
|
||||
if (!provider) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
before_login_hint: binding.dcn_provider_get_before_login_hint(provider),
|
||||
overview_page: binding.dcn_provider_get_overview_page(provider),
|
||||
status: binding.dcn_provider_get_status(provider),
|
||||
}
|
||||
}
|
||||
|
||||
sendWebxdcStatusUpdate<T>(
|
||||
msgId: number,
|
||||
json: WebxdcSendingStatusUpdate<T>,
|
||||
descr: string
|
||||
) {
|
||||
return Boolean(
|
||||
binding.dcn_send_webxdc_status_update(
|
||||
this.dcn_context,
|
||||
msgId,
|
||||
JSON.stringify(json),
|
||||
descr
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
getWebxdcStatusUpdates<T>(
|
||||
msgId: number,
|
||||
serial = 0
|
||||
): WebxdcReceivedStatusUpdate<T>[] {
|
||||
return JSON.parse(
|
||||
binding.dcn_get_webxdc_status_updates(this.dcn_context, msgId, serial)
|
||||
)
|
||||
}
|
||||
|
||||
/** the string contains the binary data, it is an "u8 string", maybe we will use a more efficient type in the future. */
|
||||
getWebxdcBlob(message: Message, filename: string): Buffer | null {
|
||||
return binding.dcn_msg_get_webxdc_blob(message.dc_msg, filename)
|
||||
}
|
||||
}
|
||||
|
||||
export type WebxdcInfo = {
|
||||
name: string
|
||||
icon: string
|
||||
summary: string
|
||||
/**
|
||||
* if set by the webxdc, name of the document in edit
|
||||
*/
|
||||
document?: string
|
||||
}
|
||||
|
||||
type WebxdcSendingStatusUpdate<T> = {
|
||||
/** the payload, deserialized json:
|
||||
* any javascript primitive, array or object. */
|
||||
payload: T
|
||||
/** optional, short, informational message that will be added to the chat,
|
||||
* eg. "Alice voted" or "Bob scored 123 in MyGame";
|
||||
* usually only one line of text is shown,
|
||||
* use this option sparingly to not spam the chat. */
|
||||
info?: string
|
||||
/** optional, short text, shown beside app icon;
|
||||
* it is recommended to use some aggregated value,
|
||||
* eg. "8 votes", "Highscore: 123" */
|
||||
summary?: string
|
||||
/**
|
||||
* optional, name of the document in edit,
|
||||
* must not be used eg. in games where the Webxdc does not create documents
|
||||
*/
|
||||
document?: string
|
||||
}
|
||||
|
||||
type WebxdcReceivedStatusUpdate<T> = {
|
||||
/** the payload, deserialized json */
|
||||
payload: T
|
||||
/** the serial number of this update. Serials are larger `0` and newer serials have higher numbers. */
|
||||
serial: number
|
||||
/** the maximum serial currently known.
|
||||
* If `max_serial` equals `serial` this update is the last update (until new network messages arrive). */
|
||||
max_serial: number
|
||||
/** optional, short, informational message. */
|
||||
info?: string
|
||||
/** optional, short text, shown beside app icon. If there are no updates, an empty JSON-array is returned. */
|
||||
summary?: string
|
||||
}
|
||||
205
node/lib/deltachat.ts
Normal file
205
node/lib/deltachat.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import binding from './binding'
|
||||
import { EventId2EventName } from './constants'
|
||||
import { EventEmitter } from 'events'
|
||||
import { existsSync } from 'fs'
|
||||
import rawDebug from 'debug'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { Context } from './context'
|
||||
const debug = rawDebug('deltachat:node:index')
|
||||
|
||||
const noop = function () {}
|
||||
interface NativeAccount {}
|
||||
|
||||
/**
|
||||
* Wrapper around dcn_account_t*
|
||||
*/
|
||||
export class AccountManager extends EventEmitter {
|
||||
dcn_accounts: NativeAccount
|
||||
accountDir: string
|
||||
|
||||
constructor(cwd: string, os = 'deltachat-node') {
|
||||
debug('DeltaChat constructor')
|
||||
super()
|
||||
|
||||
this.accountDir = cwd
|
||||
this.dcn_accounts = binding.dcn_accounts_new(os, this.accountDir)
|
||||
}
|
||||
|
||||
getAllAccountIds() {
|
||||
return binding.dcn_accounts_get_all(this.dcn_accounts)
|
||||
}
|
||||
|
||||
selectAccount(account_id: number) {
|
||||
return binding.dcn_accounts_select_account(this.dcn_accounts, account_id)
|
||||
}
|
||||
|
||||
selectedAccount(): number {
|
||||
return binding.dcn_accounts_get_selected_account(this.dcn_accounts)
|
||||
}
|
||||
|
||||
addAccount(): number {
|
||||
return binding.dcn_accounts_add_account(this.dcn_accounts)
|
||||
}
|
||||
|
||||
addClosedAccount(): number {
|
||||
return binding.dcn_accounts_add_closed_account(this.dcn_accounts)
|
||||
}
|
||||
|
||||
removeAccount(account_id: number) {
|
||||
return binding.dcn_accounts_remove_account(this.dcn_accounts, account_id)
|
||||
}
|
||||
|
||||
accountContext(account_id: number) {
|
||||
const native_context = binding.dcn_accounts_get_account(
|
||||
this.dcn_accounts,
|
||||
account_id
|
||||
)
|
||||
if (native_context === null) {
|
||||
throw new Error(
|
||||
`could not get context with id ${account_id}, does it even exist? please check your ids`
|
||||
)
|
||||
}
|
||||
return new Context(this, native_context, account_id)
|
||||
}
|
||||
|
||||
migrateAccount(dbfile: string): number {
|
||||
return binding.dcn_accounts_migrate_account(this.dcn_accounts, dbfile)
|
||||
}
|
||||
|
||||
close() {
|
||||
this.stopIO()
|
||||
debug('unrefing context')
|
||||
binding.dcn_accounts_unref(this.dcn_accounts)
|
||||
debug('Unref end')
|
||||
}
|
||||
|
||||
emit(
|
||||
event: string | symbol,
|
||||
account_id: number,
|
||||
data1: any,
|
||||
data2: any
|
||||
): boolean {
|
||||
super.emit('ALL', event, account_id, data1, data2)
|
||||
return super.emit(event, account_id, data1, data2)
|
||||
}
|
||||
|
||||
handleCoreEvent(
|
||||
eventId: number,
|
||||
accountId: number,
|
||||
data1: number,
|
||||
data2: number | string
|
||||
) {
|
||||
const eventString = EventId2EventName[eventId]
|
||||
debug('event', eventString, accountId, data1, data2)
|
||||
debug(eventString, data1, data2)
|
||||
if (!this.emit) {
|
||||
console.log('Received an event but EventEmitter is already destroyed.')
|
||||
console.log(eventString, data1, data2)
|
||||
return
|
||||
}
|
||||
this.emit(eventString, accountId, data1, data2)
|
||||
}
|
||||
|
||||
startEvents() {
|
||||
if (this.dcn_accounts === null) {
|
||||
throw new Error('dcn_account is null')
|
||||
}
|
||||
binding.dcn_accounts_start_event_handler(
|
||||
this.dcn_accounts,
|
||||
this.handleCoreEvent.bind(this)
|
||||
)
|
||||
debug('Started event handler')
|
||||
}
|
||||
|
||||
startIO() {
|
||||
binding.dcn_accounts_start_io(this.dcn_accounts)
|
||||
}
|
||||
|
||||
stopIO() {
|
||||
binding.dcn_accounts_stop_io(this.dcn_accounts)
|
||||
}
|
||||
|
||||
static maybeValidAddr(addr: string) {
|
||||
debug('DeltaChat.maybeValidAddr')
|
||||
if (addr === null) return false
|
||||
return Boolean(binding.dcn_maybe_valid_addr(addr))
|
||||
}
|
||||
|
||||
static parseGetInfo(info: string) {
|
||||
debug('static _getInfo')
|
||||
const result: { [key: string]: string } = {}
|
||||
|
||||
const regex = /^(\w+)=(.*)$/i
|
||||
info
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.forEach((line) => {
|
||||
const match = regex.exec(line)
|
||||
if (match) {
|
||||
result[match[1]] = match[2]
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static newTemporary() {
|
||||
let directory = null
|
||||
while (true) {
|
||||
const randomString = Math.random().toString(36).substr(2, 5)
|
||||
directory = join(tmpdir(), 'deltachat-' + randomString)
|
||||
if (!existsSync(directory)) break
|
||||
}
|
||||
const dc = new AccountManager(directory)
|
||||
const accountId = dc.addAccount()
|
||||
const context = dc.accountContext(accountId)
|
||||
return { dc, context, accountId, directory }
|
||||
}
|
||||
|
||||
static getSystemInfo() {
|
||||
debug('DeltaChat.getSystemInfo')
|
||||
const { dc, context } = AccountManager.newTemporary()
|
||||
const info = AccountManager.parseGetInfo(
|
||||
binding.dcn_get_info(context.dcn_context)
|
||||
)
|
||||
const {
|
||||
deltachat_core_version,
|
||||
sqlite_version,
|
||||
sqlite_thread_safe,
|
||||
libetpan_version,
|
||||
openssl_version,
|
||||
compile_date,
|
||||
arch,
|
||||
} = info
|
||||
const result = {
|
||||
deltachat_core_version,
|
||||
sqlite_version,
|
||||
sqlite_thread_safe,
|
||||
libetpan_version,
|
||||
openssl_version,
|
||||
compile_date,
|
||||
arch,
|
||||
}
|
||||
context.unref()
|
||||
dc.close()
|
||||
return result
|
||||
}
|
||||
|
||||
/** get information about the provider
|
||||
*
|
||||
* This function creates a temporary context to be standalone,
|
||||
* if posible use `Context.getProviderFromEmail` instead. (otherwise potential proxy settings are not used)
|
||||
* @deprecated
|
||||
*/
|
||||
static getProviderFromEmail(email: string) {
|
||||
debug('DeltaChat.getProviderFromEmail')
|
||||
const { dc, context } = AccountManager.newTemporary()
|
||||
const provider = context.getProviderFromEmail(email)
|
||||
context.unref()
|
||||
dc.close()
|
||||
return provider
|
||||
}
|
||||
}
|
||||
20
node/lib/index.ts
Normal file
20
node/lib/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { AccountManager } from './deltachat'
|
||||
|
||||
export default AccountManager
|
||||
|
||||
export { Context } from './context'
|
||||
export { Chat } from './chat'
|
||||
export { ChatList } from './chatlist'
|
||||
export { C } from './constants'
|
||||
export { Contact } from './contact'
|
||||
export { AccountManager as DeltaChat }
|
||||
export { Locations } from './locations'
|
||||
export { Lot } from './lot'
|
||||
export {
|
||||
Message,
|
||||
MessageState,
|
||||
MessageViewType,
|
||||
MessageDownloadState,
|
||||
} from './message'
|
||||
|
||||
export * from './types'
|
||||
82
node/lib/locations.ts
Normal file
82
node/lib/locations.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
const binding = require('../binding')
|
||||
const debug = require('debug')('deltachat:node:locations')
|
||||
|
||||
interface NativeLocations {}
|
||||
/**
|
||||
* Wrapper around dc_location_t*
|
||||
*/
|
||||
export class Locations {
|
||||
constructor(public dc_locations: NativeLocations) {
|
||||
debug('Locations constructor')
|
||||
if (dc_locations === null) {
|
||||
throw new Error('dc_locations can not be null')
|
||||
}
|
||||
}
|
||||
|
||||
locationToJson(index: number) {
|
||||
debug('locationToJson')
|
||||
return {
|
||||
accuracy: this.getAccuracy(index),
|
||||
latitude: this.getLatitude(index),
|
||||
longitude: this.getLongitude(index),
|
||||
timestamp: this.getTimestamp(index),
|
||||
contactId: this.getContactId(index),
|
||||
msgId: this.getMsgId(index),
|
||||
chatId: this.getChatId(index),
|
||||
isIndependent: this.isIndependent(index),
|
||||
marker: this.getMarker(index),
|
||||
}
|
||||
}
|
||||
|
||||
toJson(): ReturnType<Locations['locationToJson']>[] {
|
||||
debug('toJson')
|
||||
const locations = []
|
||||
const count = this.getCount()
|
||||
for (let index = 0; index < count; index++) {
|
||||
locations.push(this.locationToJson(index))
|
||||
}
|
||||
return locations
|
||||
}
|
||||
|
||||
getCount(): number {
|
||||
return binding.dcn_array_get_cnt(this.dc_locations)
|
||||
}
|
||||
|
||||
getAccuracy(index: number): number {
|
||||
return binding.dcn_array_get_accuracy(this.dc_locations, index)
|
||||
}
|
||||
|
||||
getLatitude(index: number): number {
|
||||
return binding.dcn_array_get_latitude(this.dc_locations, index)
|
||||
}
|
||||
|
||||
getLongitude(index: number): number {
|
||||
return binding.dcn_array_get_longitude(this.dc_locations, index)
|
||||
}
|
||||
|
||||
getTimestamp(index: number): number {
|
||||
return binding.dcn_array_get_timestamp(this.dc_locations, index)
|
||||
}
|
||||
|
||||
getMsgId(index: number): number {
|
||||
return binding.dcn_array_get_msg_id(this.dc_locations, index)
|
||||
}
|
||||
|
||||
getContactId(index: number): number {
|
||||
return binding.dcn_array_get_contact_id(this.dc_locations, index)
|
||||
}
|
||||
|
||||
getChatId(index: number): number {
|
||||
return binding.dcn_array_get_chat_id(this.dc_locations, index)
|
||||
}
|
||||
|
||||
isIndependent(index: number): boolean {
|
||||
return binding.dcn_array_is_independent(this.dc_locations, index)
|
||||
}
|
||||
|
||||
getMarker(index: number): string {
|
||||
return binding.dcn_array_get_marker(this.dc_locations, index)
|
||||
}
|
||||
}
|
||||
52
node/lib/lot.ts
Normal file
52
node/lib/lot.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
const binding = require('../binding')
|
||||
const debug = require('debug')('deltachat:node:lot')
|
||||
|
||||
interface NativeLot {}
|
||||
/**
|
||||
* Wrapper around dc_lot_t*
|
||||
*/
|
||||
export class Lot {
|
||||
constructor(public dc_lot: NativeLot) {
|
||||
debug('Lot constructor')
|
||||
if (dc_lot === null) {
|
||||
throw new Error('dc_lot can not be null')
|
||||
}
|
||||
}
|
||||
|
||||
toJson() {
|
||||
debug('toJson')
|
||||
return {
|
||||
state: this.getState(),
|
||||
text1: this.getText1(),
|
||||
text1Meaning: this.getText1Meaning(),
|
||||
text2: this.getText2(),
|
||||
timestamp: this.getTimestamp(),
|
||||
}
|
||||
}
|
||||
|
||||
getId(): number {
|
||||
return binding.dcn_lot_get_id(this.dc_lot)
|
||||
}
|
||||
|
||||
getState(): number {
|
||||
return binding.dcn_lot_get_state(this.dc_lot)
|
||||
}
|
||||
|
||||
getText1(): string {
|
||||
return binding.dcn_lot_get_text1(this.dc_lot)
|
||||
}
|
||||
|
||||
getText1Meaning(): string {
|
||||
return binding.dcn_lot_get_text1_meaning(this.dc_lot)
|
||||
}
|
||||
|
||||
getText2(): string {
|
||||
return binding.dcn_lot_get_text2(this.dc_lot)
|
||||
}
|
||||
|
||||
getTimestamp(): number {
|
||||
return binding.dcn_lot_get_timestamp(this.dc_lot)
|
||||
}
|
||||
}
|
||||
370
node/lib/message.ts
Normal file
370
node/lib/message.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import binding from './binding'
|
||||
import { C } from './constants'
|
||||
import { Lot } from './lot'
|
||||
import { Chat } from './chat'
|
||||
import { WebxdcInfo } from './context'
|
||||
const debug = require('debug')('deltachat:node:message')
|
||||
|
||||
export enum MessageDownloadState {
|
||||
Available = C.DC_DOWNLOAD_AVAILABLE,
|
||||
Done = C.DC_DOWNLOAD_DONE,
|
||||
Failure = C.DC_DOWNLOAD_FAILURE,
|
||||
InProgress = C.DC_DOWNLOAD_IN_PROGRESS,
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for message states so you can do e.g.
|
||||
*
|
||||
* if (msg.getState().isPending()) { .. }
|
||||
*
|
||||
*/
|
||||
export class MessageState {
|
||||
constructor(public state: number) {
|
||||
debug(`MessageState constructor ${state}`)
|
||||
}
|
||||
|
||||
isUndefined() {
|
||||
return this.state === C.DC_STATE_UNDEFINED
|
||||
}
|
||||
|
||||
isFresh() {
|
||||
return this.state === C.DC_STATE_IN_FRESH
|
||||
}
|
||||
|
||||
isNoticed() {
|
||||
return this.state === C.DC_STATE_IN_NOTICED
|
||||
}
|
||||
|
||||
isSeen() {
|
||||
return this.state === C.DC_STATE_IN_SEEN
|
||||
}
|
||||
|
||||
isPending() {
|
||||
return this.state === C.DC_STATE_OUT_PENDING
|
||||
}
|
||||
|
||||
isFailed() {
|
||||
return this.state === C.DC_STATE_OUT_FAILED
|
||||
}
|
||||
|
||||
isDelivered() {
|
||||
return this.state === C.DC_STATE_OUT_DELIVERED
|
||||
}
|
||||
|
||||
isReceived() {
|
||||
return this.state === C.DC_STATE_OUT_MDN_RCVD
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for message types so you can do e.g.
|
||||
*
|
||||
* if (msg.getViewType().isVideo()) { .. }
|
||||
*
|
||||
*/
|
||||
export class MessageViewType {
|
||||
constructor(public viewType: number) {
|
||||
debug(`MessageViewType constructor ${viewType}`)
|
||||
}
|
||||
|
||||
isText() {
|
||||
return this.viewType === C.DC_MSG_TEXT
|
||||
}
|
||||
|
||||
isImage() {
|
||||
return this.viewType === C.DC_MSG_IMAGE || this.viewType === C.DC_MSG_GIF
|
||||
}
|
||||
|
||||
isGif() {
|
||||
return this.viewType === C.DC_MSG_GIF
|
||||
}
|
||||
|
||||
isAudio() {
|
||||
return this.viewType === C.DC_MSG_AUDIO || this.viewType === C.DC_MSG_VOICE
|
||||
}
|
||||
|
||||
isVoice() {
|
||||
return this.viewType === C.DC_MSG_VOICE
|
||||
}
|
||||
|
||||
isVideo() {
|
||||
return this.viewType === C.DC_MSG_VIDEO
|
||||
}
|
||||
|
||||
isFile() {
|
||||
return this.viewType === C.DC_MSG_FILE
|
||||
}
|
||||
|
||||
isVideochatInvitation() {
|
||||
return this.viewType === C.DC_MSG_VIDEOCHAT_INVITATION
|
||||
}
|
||||
}
|
||||
|
||||
interface NativeMessage {}
|
||||
/**
|
||||
* Wrapper around dc_msg_t*
|
||||
*/
|
||||
export class Message {
|
||||
constructor(public dc_msg: NativeMessage) {
|
||||
debug('Message constructor')
|
||||
if (dc_msg === null) {
|
||||
throw new Error('dc_msg can not be null')
|
||||
}
|
||||
}
|
||||
|
||||
toJson() {
|
||||
debug('toJson')
|
||||
const quotedMessage = this.getQuotedMessage()
|
||||
const viewType = binding.dcn_msg_get_viewtype(this.dc_msg)
|
||||
return {
|
||||
chatId: this.getChatId(),
|
||||
webxdcInfo: viewType == C.DC_MSG_WEBXDC ? this.webxdcInfo : null,
|
||||
downloadState: this.downloadState,
|
||||
duration: this.getDuration(),
|
||||
file: this.getFile(),
|
||||
fromId: this.getFromId(),
|
||||
id: this.getId(),
|
||||
quotedText: this.getQuotedText(),
|
||||
quotedMessageId: quotedMessage ? quotedMessage.getId() : null,
|
||||
receivedTimestamp: this.getReceivedTimestamp(),
|
||||
sortTimestamp: this.getSortTimestamp(),
|
||||
text: this.getText(),
|
||||
timestamp: this.getTimestamp(),
|
||||
hasLocation: this.hasLocation(),
|
||||
hasHTML: this.hasHTML,
|
||||
viewType,
|
||||
state: binding.dcn_msg_get_state(this.dc_msg),
|
||||
hasDeviatingTimestamp: this.hasDeviatingTimestamp(),
|
||||
showPadlock: this.getShowpadlock(),
|
||||
summary: this.getSummary().toJson(),
|
||||
subject: this.subject,
|
||||
isSetupmessage: this.isSetupmessage(),
|
||||
isInfo: this.isInfo(),
|
||||
isForwarded: this.isForwarded(),
|
||||
dimensions: {
|
||||
height: this.getHeight(),
|
||||
width: this.getWidth(),
|
||||
},
|
||||
videochatType: this.getVideochatType(),
|
||||
videochatUrl: this.getVideochatUrl(),
|
||||
overrideSenderName: this.overrideSenderName,
|
||||
parentId: this.parent?.getId(),
|
||||
}
|
||||
}
|
||||
|
||||
getChatId(): number {
|
||||
return binding.dcn_msg_get_chat_id(this.dc_msg)
|
||||
}
|
||||
|
||||
get webxdcInfo(): WebxdcInfo | null {
|
||||
let info = binding.dcn_msg_get_webxdc_info(this.dc_msg)
|
||||
return info
|
||||
? JSON.parse(binding.dcn_msg_get_webxdc_info(this.dc_msg))
|
||||
: null
|
||||
}
|
||||
|
||||
get downloadState(): MessageDownloadState {
|
||||
return binding.dcn_msg_get_download_state(this.dc_msg)
|
||||
}
|
||||
|
||||
get parent(): Message | null {
|
||||
let msg = binding.dcn_msg_get_parent(this.dc_msg)
|
||||
return msg ? new Message(msg) : null
|
||||
}
|
||||
|
||||
getDuration(): number {
|
||||
return binding.dcn_msg_get_duration(this.dc_msg)
|
||||
}
|
||||
|
||||
getFile(): string {
|
||||
return binding.dcn_msg_get_file(this.dc_msg)
|
||||
}
|
||||
|
||||
getFilebytes(): number {
|
||||
return binding.dcn_msg_get_filebytes(this.dc_msg)
|
||||
}
|
||||
|
||||
getFilemime(): string {
|
||||
return binding.dcn_msg_get_filemime(this.dc_msg)
|
||||
}
|
||||
|
||||
getFilename(): string {
|
||||
return binding.dcn_msg_get_filename(this.dc_msg)
|
||||
}
|
||||
|
||||
getFromId(): number {
|
||||
return binding.dcn_msg_get_from_id(this.dc_msg)
|
||||
}
|
||||
|
||||
getHeight(): number {
|
||||
return binding.dcn_msg_get_height(this.dc_msg)
|
||||
}
|
||||
|
||||
getId(): number {
|
||||
return binding.dcn_msg_get_id(this.dc_msg)
|
||||
}
|
||||
|
||||
getQuotedText(): string {
|
||||
return binding.dcn_msg_get_quoted_text(this.dc_msg)
|
||||
}
|
||||
|
||||
getQuotedMessage(): Message | null {
|
||||
const dc_msg = binding.dcn_msg_get_quoted_msg(this.dc_msg)
|
||||
return dc_msg ? new Message(dc_msg) : null
|
||||
}
|
||||
|
||||
getReceivedTimestamp(): number {
|
||||
return binding.dcn_msg_get_received_timestamp(this.dc_msg)
|
||||
}
|
||||
|
||||
getSetupcodebegin() {
|
||||
return binding.dcn_msg_get_setupcodebegin(this.dc_msg)
|
||||
}
|
||||
|
||||
getShowpadlock() {
|
||||
return Boolean(binding.dcn_msg_get_showpadlock(this.dc_msg))
|
||||
}
|
||||
|
||||
getSortTimestamp(): number {
|
||||
return binding.dcn_msg_get_sort_timestamp(this.dc_msg)
|
||||
}
|
||||
|
||||
getState() {
|
||||
return new MessageState(binding.dcn_msg_get_state(this.dc_msg))
|
||||
}
|
||||
|
||||
getSummary(chat?: Chat) {
|
||||
const dc_chat = (chat && chat.dc_chat) || null
|
||||
return new Lot(binding.dcn_msg_get_summary(this.dc_msg, dc_chat))
|
||||
}
|
||||
|
||||
get subject(): string {
|
||||
return binding.dcn_msg_get_subject(this.dc_msg)
|
||||
}
|
||||
|
||||
getSummarytext(approxCharacters: number): string {
|
||||
approxCharacters = approxCharacters || 0
|
||||
return binding.dcn_msg_get_summarytext(this.dc_msg, approxCharacters)
|
||||
}
|
||||
|
||||
getText(): string {
|
||||
return binding.dcn_msg_get_text(this.dc_msg)
|
||||
}
|
||||
|
||||
getTimestamp(): number {
|
||||
return binding.dcn_msg_get_timestamp(this.dc_msg)
|
||||
}
|
||||
|
||||
getViewType() {
|
||||
return new MessageViewType(binding.dcn_msg_get_viewtype(this.dc_msg))
|
||||
}
|
||||
|
||||
getVideochatType(): number {
|
||||
return binding.dcn_msg_get_videochat_type(this.dc_msg)
|
||||
}
|
||||
|
||||
getVideochatUrl(): string {
|
||||
return binding.dcn_msg_get_videochat_url(this.dc_msg)
|
||||
}
|
||||
|
||||
getWidth(): number {
|
||||
return binding.dcn_msg_get_width(this.dc_msg)
|
||||
}
|
||||
|
||||
get overrideSenderName(): string {
|
||||
return binding.dcn_msg_get_override_sender_name(this.dc_msg)
|
||||
}
|
||||
|
||||
hasDeviatingTimestamp() {
|
||||
return binding.dcn_msg_has_deviating_timestamp(this.dc_msg)
|
||||
}
|
||||
|
||||
hasLocation() {
|
||||
return Boolean(binding.dcn_msg_has_location(this.dc_msg))
|
||||
}
|
||||
|
||||
get hasHTML() {
|
||||
return Boolean(binding.dcn_msg_has_html(this.dc_msg))
|
||||
}
|
||||
|
||||
isDeadDrop() {
|
||||
// TODO: Fix
|
||||
//return this.getChatId() === C.DC_CHAT_ID_DEADDROP
|
||||
return false
|
||||
}
|
||||
|
||||
isForwarded() {
|
||||
return Boolean(binding.dcn_msg_is_forwarded(this.dc_msg))
|
||||
}
|
||||
|
||||
isIncreation() {
|
||||
return Boolean(binding.dcn_msg_is_increation(this.dc_msg))
|
||||
}
|
||||
|
||||
isInfo() {
|
||||
return Boolean(binding.dcn_msg_is_info(this.dc_msg))
|
||||
}
|
||||
|
||||
isSent() {
|
||||
return Boolean(binding.dcn_msg_is_sent(this.dc_msg))
|
||||
}
|
||||
|
||||
isSetupmessage() {
|
||||
return Boolean(binding.dcn_msg_is_setupmessage(this.dc_msg))
|
||||
}
|
||||
|
||||
latefilingMediasize(width: number, height: number, duration: number) {
|
||||
binding.dcn_msg_latefiling_mediasize(this.dc_msg, width, height, duration)
|
||||
}
|
||||
|
||||
setDimension(width: number, height: number) {
|
||||
binding.dcn_msg_set_dimension(this.dc_msg, width, height)
|
||||
return this
|
||||
}
|
||||
|
||||
setDuration(duration: number) {
|
||||
binding.dcn_msg_set_duration(this.dc_msg, duration)
|
||||
return this
|
||||
}
|
||||
|
||||
setFile(file: string, mime?: string) {
|
||||
if (typeof file !== 'string') throw new Error('Missing filename')
|
||||
binding.dcn_msg_set_file(this.dc_msg, file, mime || '')
|
||||
return this
|
||||
}
|
||||
|
||||
setLocation(longitude: number, latitude: number) {
|
||||
binding.dcn_msg_set_location(this.dc_msg, longitude, latitude)
|
||||
return this
|
||||
}
|
||||
|
||||
setQuote(quotedMessage: Message | null) {
|
||||
binding.dcn_msg_set_quote(this.dc_msg, quotedMessage?.dc_msg)
|
||||
return this
|
||||
}
|
||||
|
||||
setText(text: string) {
|
||||
binding.dcn_msg_set_text(this.dc_msg, text)
|
||||
return this
|
||||
}
|
||||
|
||||
setHTML(html: string) {
|
||||
binding.dcn_msg_set_html(this.dc_msg, html)
|
||||
return this
|
||||
}
|
||||
|
||||
setOverrideSenderName(senderName: string) {
|
||||
binding.dcn_msg_set_override_sender_name(this.dc_msg, senderName)
|
||||
return this
|
||||
}
|
||||
|
||||
/** Force the message to be sent in plain text.
|
||||
*
|
||||
* This API is for bots, there is no need to expose it in the UI.
|
||||
*/
|
||||
forcePlaintext() {
|
||||
binding.dcn_msg_force_plaintext(this.dc_msg)
|
||||
}
|
||||
}
|
||||
24
node/lib/types.ts
Normal file
24
node/lib/types.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { C } from './constants'
|
||||
|
||||
export type ChatTypes =
|
||||
| C.DC_CHAT_TYPE_GROUP
|
||||
| C.DC_CHAT_TYPE_MAILINGLIST
|
||||
| C.DC_CHAT_TYPE_SINGLE
|
||||
| C.DC_CHAT_TYPE_UNDEFINED
|
||||
|
||||
export interface ChatJSON {
|
||||
archived: boolean
|
||||
pinned: boolean
|
||||
color: string
|
||||
id: number
|
||||
name: string
|
||||
profileImage: string
|
||||
type: number
|
||||
isSelfTalk: boolean
|
||||
isUnpromoted: boolean
|
||||
isProtected: boolean
|
||||
canSend: boolean
|
||||
isDeviceTalk: boolean
|
||||
isContactRequest: boolean
|
||||
muted: boolean
|
||||
}
|
||||
6
node/lib/util.ts
Normal file
6
node/lib/util.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @param integerColor expects a 24bit rgb integer (left to right: 8bits red, 8bits green, 8bits blue)
|
||||
*/
|
||||
export function integerToHexColor(integerColor: number) {
|
||||
return '#' + (integerColor + 16777216).toString(16).substring(1)
|
||||
}
|
||||
13
node/patches/m1_build_use_x86_64.patch
Normal file
13
node/patches/m1_build_use_x86_64.patch
Normal file
@@ -0,0 +1,13 @@
|
||||
diff --git i/node/binding.gyp w/node/binding.gyp
|
||||
index b0d92eae..c5e504fa 100644
|
||||
--- i/node/binding.gyp
|
||||
+++ w/node/binding.gyp
|
||||
@@ -43,7 +43,7 @@
|
||||
"include_dirs": ["../deltachat-ffi"],
|
||||
"ldflags": ["-Wl,-Bsymbolic"], # Prevent sqlite3 from electron from overriding sqlcipher
|
||||
"libraries": [
|
||||
- "../../target/release/libdeltachat.a",
|
||||
+ "../../target/x86_64-apple-darwin/release/libdeltachat.a",
|
||||
"-ldl",
|
||||
],
|
||||
"conditions": [],
|
||||
26
node/scripts/common.js
Normal file
26
node/scripts/common.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const spawnSync = require('child_process').spawnSync
|
||||
|
||||
const verbose = isVerbose()
|
||||
|
||||
function spawn (cmd, args, opts) {
|
||||
log(`>> spawn: ${cmd} ${args.join(' ')}`)
|
||||
const result = spawnSync(cmd, args, opts)
|
||||
if (result.status === null) {
|
||||
console.error(`Could not find ${cmd}`)
|
||||
process.exit(1)
|
||||
} else if (result.status !== 0) {
|
||||
console.error(`${cmd} failed with code ${result.status}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
function log (...args) {
|
||||
if (verbose) console.log(...args)
|
||||
}
|
||||
|
||||
function isVerbose () {
|
||||
const loglevel = process.env.npm_config_loglevel
|
||||
return loglevel === 'verbose' || process.env.CI === 'true'
|
||||
}
|
||||
|
||||
module.exports = { spawn, log, isVerbose, verbose }
|
||||
73
node/scripts/generate-constants.js
Executable file
73
node/scripts/generate-constants.js
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const split = require('split2')
|
||||
|
||||
const data = []
|
||||
const regex = /^#define\s+(\w+)\s+(\w+)/i
|
||||
const header = path.resolve(
|
||||
__dirname,
|
||||
'../../deltachat-ffi/deltachat.h'
|
||||
)
|
||||
|
||||
console.log('Generating constants...')
|
||||
|
||||
fs.createReadStream(header)
|
||||
.pipe(split())
|
||||
.on('data', (line) => {
|
||||
const match = regex.exec(line)
|
||||
if (match) {
|
||||
const key = match[1]
|
||||
const value = parseInt(match[2])
|
||||
if (isNaN(value)) return
|
||||
|
||||
data.push({ key, value })
|
||||
}
|
||||
})
|
||||
.on('end', () => {
|
||||
const constants = data
|
||||
.filter(
|
||||
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
|
||||
)
|
||||
.sort((lhs, rhs) => {
|
||||
if (lhs.key < rhs.key) return -1
|
||||
else if (lhs.key > rhs.key) return 1
|
||||
return 0
|
||||
})
|
||||
.map((row) => {
|
||||
return ` ${row.key}: ${row.value}`
|
||||
})
|
||||
.join(',\n')
|
||||
|
||||
const events = data
|
||||
.sort((lhs, rhs) => {
|
||||
if (lhs.value < rhs.value) return -1
|
||||
else if (lhs.value > rhs.value) return 1
|
||||
return 0
|
||||
})
|
||||
.filter((i) => {
|
||||
return i.key.startsWith('DC_EVENT_')
|
||||
})
|
||||
.map((i) => {
|
||||
return ` ${i.value}: '${i.key}'`
|
||||
})
|
||||
.join(',\n')
|
||||
|
||||
// backwards compat
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../constants.js'),
|
||||
`// Generated!\n\nmodule.exports = {\n${constants}\n}\n`
|
||||
)
|
||||
// backwards compat
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../events.js'),
|
||||
`/* eslint-disable quotes */\n// Generated!\n\nmodule.exports = {\n${events}\n}\n`
|
||||
)
|
||||
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../lib/constants.ts'),
|
||||
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, ' =')},\n}\n
|
||||
// Generated!\n\nexport const EventId2EventName: { [key: number]: string } = {\n${events},\n}\n`
|
||||
)
|
||||
})
|
||||
22
node/scripts/install.js
Normal file
22
node/scripts/install.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const {execSync} = require('child_process')
|
||||
const {existsSync} = require('fs')
|
||||
const {join} = require('path')
|
||||
|
||||
const run = (cmd) => {
|
||||
console.log('[i] running `' + cmd + '`')
|
||||
execSync(cmd, {stdio: 'inherit'})
|
||||
}
|
||||
|
||||
// Build bindings
|
||||
if (process.env.USE_SYSTEM_LIBDELTACHAT === 'true') {
|
||||
console.log('[i] USE_SYSTEM_LIBDELTACHAT is true, rebuilding c bindings and using pkg-config to retrieve lib paths and cflags of libdeltachat')
|
||||
run('npm run build:bindings:c:c')
|
||||
} else {
|
||||
console.log('[i] Building rust core & c bindings, if possible use prebuilds')
|
||||
run('npm run install:prebuilds')
|
||||
}
|
||||
|
||||
if (!existsSync(join(__dirname, '..', 'dist'))) {
|
||||
console.log('[i] Didn\'t find already built typescript bindings. Trying to transpile them. If this fail, make sure typescript is installed ;)')
|
||||
run('npm run build:bindings:ts')
|
||||
}
|
||||
46
node/scripts/postLinksToDetails.js
Normal file
46
node/scripts/postLinksToDetails.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const { readFileSync } = require('fs')
|
||||
|
||||
const sha = JSON.parse(
|
||||
readFileSync(process.env['GITHUB_EVENT_PATH'], 'utf8')
|
||||
).pull_request.head.sha
|
||||
|
||||
const base_url =
|
||||
'https://download.delta.chat/node/'
|
||||
|
||||
const GITHUB_API_URL =
|
||||
'https://api.github.com/repos/deltachat/deltachat-core-rust/statuses/' + sha
|
||||
|
||||
const file_url = process.env['URL']
|
||||
const GITHUB_TOKEN = process.env['GITHUB_TOKEN']
|
||||
|
||||
const STATUS_DATA = {
|
||||
state: 'success',
|
||||
description: '⏩ Click on "Details" to download →',
|
||||
context: 'Download the node-bindings.tar.gz',
|
||||
target_url: base_url + file_url + '.tar.gz',
|
||||
}
|
||||
|
||||
const http = require('https')
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'github-action ci for deltachat deskop',
|
||||
authorization: 'Bearer ' + GITHUB_TOKEN,
|
||||
},
|
||||
}
|
||||
|
||||
const req = http.request(GITHUB_API_URL, options, function(res) {
|
||||
var chunks = []
|
||||
res.on('data', function(chunk) {
|
||||
chunks.push(chunk)
|
||||
})
|
||||
res.on('end', function() {
|
||||
var body = Buffer.concat(chunks)
|
||||
console.log(body.toString())
|
||||
})
|
||||
})
|
||||
|
||||
req.write(JSON.stringify(STATUS_DATA))
|
||||
req.end()
|
||||
57
node/scripts/postinstall.js
Normal file
57
node/scripts/postinstall.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
console.log('postinstall: not windows, so skipping!')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const from = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'target',
|
||||
'release',
|
||||
'deltachat.dll'
|
||||
)
|
||||
|
||||
const getDestination = () => {
|
||||
const argv = process.argv
|
||||
if (argv.length === 3 && argv[2] === '--prebuild') {
|
||||
return path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'prebuilds',
|
||||
'win32-x64',
|
||||
'deltachat.dll'
|
||||
)
|
||||
} else {
|
||||
return path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'build',
|
||||
'Release',
|
||||
'deltachat.dll'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const dest = getDestination()
|
||||
|
||||
copy(from, dest, (err) => {
|
||||
if (err) throw err
|
||||
console.log(`postinstall: copied ${from} to ${dest}`)
|
||||
})
|
||||
|
||||
function copy (from, to, cb) {
|
||||
fs.stat(from, (err, st) => {
|
||||
if (err) return cb(err)
|
||||
fs.readFile(from, (err, buf) => {
|
||||
if (err) return cb(err)
|
||||
fs.writeFile(to, buf, (err) => {
|
||||
if (err) return cb(err)
|
||||
fs.chmod(to, st.mode, cb)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
17
node/scripts/rebuild-core.js
Normal file
17
node/scripts/rebuild-core.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const path = require('path')
|
||||
const { spawn } = require('./common')
|
||||
const opts = {
|
||||
cwd: path.resolve(__dirname, '../..'),
|
||||
stdio: 'inherit'
|
||||
}
|
||||
|
||||
const buildArgs = [
|
||||
'build',
|
||||
'--release',
|
||||
'--features',
|
||||
'vendored',
|
||||
'-p',
|
||||
'deltachat_ffi'
|
||||
]
|
||||
|
||||
spawn('cargo', buildArgs, opts)
|
||||
3505
node/src/module.c
Normal file
3505
node/src/module.c
Normal file
File diff suppressed because it is too large
Load Diff
144
node/src/napi-macros-extensions.h
Normal file
144
node/src/napi-macros-extensions.h
Normal file
@@ -0,0 +1,144 @@
|
||||
#include <napi-macros.h>
|
||||
|
||||
#undef NAPI_STATUS_THROWS
|
||||
|
||||
#define NAPI_STATUS_THROWS(call) \
|
||||
if ((call) != napi_ok) { \
|
||||
napi_throw_error(env, NULL, #call " failed!"); \
|
||||
}
|
||||
|
||||
#define NAPI_DCN_CONTEXT() \
|
||||
dcn_context_t* dcn_context; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dcn_context)); \
|
||||
if (!dcn_context) { \
|
||||
const char* msg = "Provided dnc_context is null"; \
|
||||
NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
|
||||
} \
|
||||
if (!dcn_context->dc_context) { \
|
||||
const char* msg = "Provided dc_context is null, did you close the context or not open it?"; \
|
||||
NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
|
||||
}
|
||||
|
||||
#define NAPI_DCN_ACCOUNTS() \
|
||||
dcn_accounts_t* dcn_accounts; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dcn_accounts)); \
|
||||
if (!dcn_accounts) { \
|
||||
const char* msg = "Provided dnc_acounts is null"; \
|
||||
NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
|
||||
} \
|
||||
if (!dcn_accounts->dc_accounts) { \
|
||||
const char* msg = "Provided dc_accounts is null, did you unref the accounts object?"; \
|
||||
NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
|
||||
}
|
||||
|
||||
|
||||
#define NAPI_DC_CHAT() \
|
||||
dc_chat_t* dc_chat; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_chat));
|
||||
|
||||
#define NAPI_DC_CHATLIST() \
|
||||
dc_chatlist_t* dc_chatlist; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_chatlist));
|
||||
|
||||
#define NAPI_DC_CONTACT() \
|
||||
dc_contact_t* dc_contact; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_contact));
|
||||
|
||||
#define NAPI_DC_LOT() \
|
||||
dc_lot_t* dc_lot; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_lot));
|
||||
|
||||
#define NAPI_DC_MSG() \
|
||||
dc_msg_t* dc_msg; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_msg));
|
||||
|
||||
#define NAPI_ARGV_DC_MSG(name, position) \
|
||||
dc_msg_t* name; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[position], (void**)&name));
|
||||
|
||||
#define NAPI_DC_PROVIDER() \
|
||||
dc_provider_t* dc_provider; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_provider));
|
||||
|
||||
#define NAPI_DC_ARRAY() \
|
||||
dc_array_t* dc_array; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_array));
|
||||
|
||||
#define NAPI_RETURN_UNDEFINED() \
|
||||
return 0;
|
||||
|
||||
#define NAPI_RETURN_UINT64(name) \
|
||||
napi_value return_int64; \
|
||||
NAPI_STATUS_THROWS(napi_create_bigint_int64(env, name, &return_int64)); \
|
||||
return return_int64;
|
||||
|
||||
#define NAPI_RETURN_INT64(name) \
|
||||
napi_value return_int64; \
|
||||
NAPI_STATUS_THROWS(napi_create_int64(env, name, &return_int64)); \
|
||||
return return_int64;
|
||||
|
||||
|
||||
#define NAPI_RETURN_AND_UNREF_STRING(name) \
|
||||
napi_value return_value; \
|
||||
if (name == NULL) { \
|
||||
NAPI_STATUS_THROWS(napi_get_null(env, &return_value)); \
|
||||
return return_value; \
|
||||
} \
|
||||
NAPI_STATUS_THROWS(napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &return_value)); \
|
||||
dc_str_unref(name); \
|
||||
return return_value;
|
||||
|
||||
#define NAPI_ASYNC_CARRIER_BEGIN(name) \
|
||||
typedef struct name##_carrier_t { \
|
||||
napi_ref callback_ref; \
|
||||
napi_async_work async_work; \
|
||||
dcn_context_t* dcn_context;
|
||||
|
||||
#define NAPI_ASYNC_CARRIER_END(name) \
|
||||
} name##_carrier_t;
|
||||
|
||||
#define NAPI_ASYNC_EXECUTE(name) \
|
||||
static void name##_execute(napi_env env, void* data)
|
||||
|
||||
#define NAPI_ASYNC_GET_CARRIER(name) \
|
||||
name##_carrier_t* carrier = (name##_carrier_t*)data;
|
||||
|
||||
#define NAPI_ASYNC_COMPLETE(name) \
|
||||
static void name##_complete(napi_env env, napi_status status, void* data)
|
||||
|
||||
#define NAPI_ASYNC_CALL_AND_DELETE_CB() \
|
||||
napi_value global; \
|
||||
NAPI_STATUS_THROWS(napi_get_global(env, &global)); \
|
||||
napi_value callback; \
|
||||
NAPI_STATUS_THROWS(napi_get_reference_value(env, carrier->callback_ref, &callback)); \
|
||||
NAPI_STATUS_THROWS(napi_call_function(env, global, callback, argc, argv, NULL)); \
|
||||
NAPI_STATUS_THROWS(napi_delete_reference(env, carrier->callback_ref)); \
|
||||
NAPI_STATUS_THROWS(napi_delete_async_work(env, carrier->async_work));
|
||||
|
||||
#define NAPI_ASYNC_NEW_CARRIER(name) \
|
||||
name##_carrier_t* carrier = calloc(1, sizeof(name##_carrier_t)); \
|
||||
carrier->dcn_context = dcn_context;
|
||||
|
||||
#define NAPI_ASYNC_QUEUE_WORK(name, cb) \
|
||||
napi_value callback = cb; \
|
||||
napi_value async_resource_name; \
|
||||
NAPI_STATUS_THROWS(napi_create_reference(env, callback, 1, &carrier->callback_ref)); \
|
||||
NAPI_STATUS_THROWS(napi_create_string_utf8(env, #name "_callback", \
|
||||
NAPI_AUTO_LENGTH, \
|
||||
&async_resource_name)); \
|
||||
NAPI_STATUS_THROWS(napi_create_async_work(env, callback, async_resource_name, \
|
||||
name##_execute, name##_complete, \
|
||||
carrier, &carrier->async_work)); \
|
||||
NAPI_STATUS_THROWS(napi_queue_async_work(env, carrier->async_work));
|
||||
|
||||
/*** this could/should be moved to napi-macros ***/
|
||||
|
||||
#define NAPI_DOUBLE(name, val) \
|
||||
double name; \
|
||||
if (napi_get_value_double(env, val, &name) != napi_ok) { \
|
||||
napi_throw_error(env, "EINVAL", "Expected double"); \
|
||||
return NULL; \
|
||||
}
|
||||
|
||||
#define NAPI_ARGV_DOUBLE(name, i) \
|
||||
NAPI_DOUBLE(name, argv[i])
|
||||
BIN
node/test/fixtures/avatar.png
vendored
Normal file
BIN
node/test/fixtures/avatar.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
BIN
node/test/fixtures/image.jpeg
vendored
Normal file
BIN
node/test/fixtures/image.jpeg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
node/test/fixtures/logo.png
vendored
Normal file
BIN
node/test/fixtures/logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
862
node/test/test.js
Normal file
862
node/test/test.js
Normal file
@@ -0,0 +1,862 @@
|
||||
// @ts-check
|
||||
import DeltaChat, { Message } from '../dist'
|
||||
import binding from '../binding'
|
||||
|
||||
import { strictEqual } from 'assert'
|
||||
import chai, { expect } from 'chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import { EventId2EventName, C } from '../dist/constants'
|
||||
import { join } from 'path'
|
||||
import { mkdtempSync, statSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { Context } from '../dist/context'
|
||||
chai.use(chaiAsPromised)
|
||||
|
||||
async function createTempUser(url) {
|
||||
const fetch = require('node-fetch')
|
||||
|
||||
async function postData(url = '') {
|
||||
// Default options are marked with *
|
||||
const response = await fetch(url, {
|
||||
method: 'POST', // *GET, POST, PUT, DELETE, etc.
|
||||
mode: 'cors', // no-cors, *cors, same-origin
|
||||
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
|
||||
credentials: 'same-origin', // include, *same-origin, omit
|
||||
headers: {
|
||||
'cache-control': 'no-cache',
|
||||
},
|
||||
referrerPolicy: 'no-referrer', // no-referrer, *client
|
||||
})
|
||||
return response.json() // parses JSON response into native JavaScript objects
|
||||
}
|
||||
|
||||
return await postData(url)
|
||||
}
|
||||
|
||||
describe('static tests', function () {
|
||||
it('reverse lookup of events', function () {
|
||||
const eventKeys = Object.keys(EventId2EventName).map((k) => Number(k))
|
||||
const eventValues = Object.values(EventId2EventName)
|
||||
const reverse = eventValues.map((v) => C[v])
|
||||
expect(reverse).to.be.deep.equal(eventKeys)
|
||||
})
|
||||
|
||||
it('event constants are consistent', function () {
|
||||
const eventKeys = Object.keys(C)
|
||||
.filter((k) => k.startsWith('DC_EVENT_'))
|
||||
.sort()
|
||||
const eventValues = Object.values(EventId2EventName).sort()
|
||||
expect(eventKeys).to.be.deep.equal(eventValues)
|
||||
})
|
||||
|
||||
it('static method maybeValidAddr()', function () {
|
||||
expect(DeltaChat.maybeValidAddr(null)).to.equal(false)
|
||||
expect(DeltaChat.maybeValidAddr('')).to.equal(false)
|
||||
expect(DeltaChat.maybeValidAddr('uuu')).to.equal(false)
|
||||
expect(DeltaChat.maybeValidAddr('dd.tt')).to.equal(false)
|
||||
expect(DeltaChat.maybeValidAddr('tt.dd@yggmail')).to.equal(true)
|
||||
expect(DeltaChat.maybeValidAddr('u@d')).to.equal(true)
|
||||
//expect(DeltaChat.maybeValidAddr('u@d.')).to.equal(false)
|
||||
//expect(DeltaChat.maybeValidAddr('u@d.t')).to.equal(false)
|
||||
//expect(DeltaChat.maybeValidAddr('u@.tt')).to.equal(false)
|
||||
expect(DeltaChat.maybeValidAddr('@d.tt')).to.equal(false)
|
||||
expect(DeltaChat.maybeValidAddr('user@domain.tld')).to.equal(true)
|
||||
expect(DeltaChat.maybeValidAddr('u@d.tt')).to.equal(true)
|
||||
})
|
||||
|
||||
it('static getSystemInfo()', function () {
|
||||
const info = Context.getSystemInfo()
|
||||
expect(info).to.contain.keys([
|
||||
'arch',
|
||||
'deltachat_core_version',
|
||||
'sqlite_version',
|
||||
])
|
||||
})
|
||||
|
||||
it('static context.getProviderFromEmail("example@example.com")', function () {
|
||||
const provider = DeltaChat.getProviderFromEmail('example@example.com')
|
||||
|
||||
expect(provider).to.deep.equal({
|
||||
before_login_hint: "Hush this provider doesn't exist!",
|
||||
overview_page: 'https://providers.delta.chat/example-com',
|
||||
status: 3,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Basic offline Tests', function () {
|
||||
it('opens a context', async function () {
|
||||
const { dc, context } = DeltaChat.newTemporary()
|
||||
|
||||
strictEqual(context.isConfigured(), false)
|
||||
dc.close()
|
||||
})
|
||||
|
||||
it('set config', async function () {
|
||||
const { dc, context } = DeltaChat.newTemporary()
|
||||
|
||||
context.setConfig('bot', true)
|
||||
strictEqual(context.getConfig('bot'), '1')
|
||||
context.setConfig('bot', false)
|
||||
strictEqual(context.getConfig('bot'), '0')
|
||||
context.setConfig('bot', '1')
|
||||
strictEqual(context.getConfig('bot'), '1')
|
||||
context.setConfig('bot', '0')
|
||||
strictEqual(context.getConfig('bot'), '0')
|
||||
context.setConfig('bot', 1)
|
||||
strictEqual(context.getConfig('bot'), '1')
|
||||
context.setConfig('bot', 0)
|
||||
strictEqual(context.getConfig('bot'), '0')
|
||||
|
||||
context.setConfig('bot', null)
|
||||
strictEqual(context.getConfig('bot'), '')
|
||||
|
||||
strictEqual(context.getConfig('selfstatus'), '')
|
||||
context.setConfig('selfstatus', 'hello')
|
||||
strictEqual(context.getConfig('selfstatus'), 'hello')
|
||||
context.setConfig('selfstatus', '')
|
||||
strictEqual(context.getConfig('selfstatus'), '')
|
||||
context.setConfig('selfstatus', null)
|
||||
strictEqual(context.getConfig('selfstatus'), '')
|
||||
|
||||
dc.close()
|
||||
})
|
||||
|
||||
it('configure with either missing addr or missing mail_pw throws', async function () {
|
||||
const { dc, context } = DeltaChat.newTemporary()
|
||||
dc.startEvents()
|
||||
|
||||
await expect(
|
||||
context.configure({ addr: 'delta1@delta.localhost' })
|
||||
).to.eventually.be.rejectedWith('Please enter a password.')
|
||||
await expect(context.configure({ mailPw: 'delta1' })).to.eventually.be
|
||||
.rejected
|
||||
|
||||
context.stopOngoingProcess()
|
||||
dc.close()
|
||||
})
|
||||
|
||||
it('context.getInfo()', async function () {
|
||||
const { dc, context } = DeltaChat.newTemporary()
|
||||
|
||||
const info = await context.getInfo()
|
||||
expect(typeof info).to.be.equal('object')
|
||||
expect(info).to.contain.keys([
|
||||
'arch',
|
||||
'bcc_self',
|
||||
'blobdir',
|
||||
'bot',
|
||||
'configured_mvbox_folder',
|
||||
'configured_sentbox_folder',
|
||||
'database_dir',
|
||||
'database_encrypted',
|
||||
'database_version',
|
||||
'delete_device_after',
|
||||
'delete_server_after',
|
||||
'deltachat_core_version',
|
||||
'display_name',
|
||||
'download_limit',
|
||||
'e2ee_enabled',
|
||||
'entered_account_settings',
|
||||
'fetch_existing_msgs',
|
||||
'fingerprint',
|
||||
'folders_configured',
|
||||
'is_configured',
|
||||
'journal_mode',
|
||||
'key_gen_type',
|
||||
'last_housekeeping',
|
||||
'level',
|
||||
'mdns_enabled',
|
||||
'media_quality',
|
||||
'messages_in_contact_requests',
|
||||
'mvbox_move',
|
||||
'num_cpus',
|
||||
'number_of_chat_messages',
|
||||
'number_of_chats',
|
||||
'number_of_contacts',
|
||||
'only_fetch_mvbox',
|
||||
'private_key_count',
|
||||
'public_key_count',
|
||||
'quota_exceeding',
|
||||
'scan_all_folders_debounce_secs',
|
||||
'selfavatar',
|
||||
'send_sync_msgs',
|
||||
'sentbox_watch',
|
||||
'show_emails',
|
||||
'socks5_enabled',
|
||||
'sqlite_version',
|
||||
'uptime',
|
||||
'used_account_settings',
|
||||
'webrtc_instance',
|
||||
])
|
||||
|
||||
dc.close()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Offline Tests with unconfigured account', function () {
|
||||
let [dc, context, accountId, directory] = [null, null, null, null]
|
||||
|
||||
this.beforeEach(async function () {
|
||||
let tmp = DeltaChat.newTemporary()
|
||||
dc = tmp.dc
|
||||
context = tmp.context
|
||||
accountId = tmp.accountId
|
||||
directory = tmp.directory
|
||||
dc.startEvents()
|
||||
})
|
||||
|
||||
this.afterEach(async function () {
|
||||
if (context) {
|
||||
context.stopOngoingProcess()
|
||||
}
|
||||
if (dc) {
|
||||
try {
|
||||
dc.stopIO()
|
||||
dc.close()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
dc = null
|
||||
context = null
|
||||
accountId = null
|
||||
directory = null
|
||||
})
|
||||
|
||||
it('invalid context.joinSecurejoin', async function () {
|
||||
expect(context.joinSecurejoin('test')).to.be.eq(0)
|
||||
})
|
||||
|
||||
it('Device Chat', async function () {
|
||||
const deviceChatMessageText = 'test234'
|
||||
|
||||
expect((await context.getChatList(0, '', null)).getCount()).to.equal(
|
||||
0,
|
||||
'no device chat after setup'
|
||||
)
|
||||
|
||||
await context.addDeviceMessage('test', deviceChatMessageText)
|
||||
|
||||
const chatList = await context.getChatList(0, '', null)
|
||||
expect(chatList.getCount()).to.equal(
|
||||
1,
|
||||
'device chat after adding device msg'
|
||||
)
|
||||
|
||||
const deviceChatId = await chatList.getChatId(0)
|
||||
const deviceChat = await context.getChat(deviceChatId)
|
||||
expect(deviceChat.isDeviceTalk()).to.be.true
|
||||
expect(deviceChat.toJson().isDeviceTalk).to.be.true
|
||||
|
||||
const deviceChatMessages = await context.getChatMessages(deviceChatId, 0, 0)
|
||||
expect(deviceChatMessages.length).to.be.equal(
|
||||
1,
|
||||
'device chat has added message'
|
||||
)
|
||||
|
||||
const deviceChatMessage = await context.getMessage(deviceChatMessages[0])
|
||||
expect(deviceChatMessage.getText()).to.equal(
|
||||
deviceChatMessageText,
|
||||
'device chat message has the inserted text'
|
||||
)
|
||||
})
|
||||
|
||||
it('should have e2ee enabled and right blobdir', function () {
|
||||
expect(context.getConfig('e2ee_enabled')).to.equal(
|
||||
'1',
|
||||
'e2eeEnabled correct'
|
||||
)
|
||||
expect(
|
||||
String(context.getBlobdir()).startsWith(directory),
|
||||
'blobdir should be inside temp directory'
|
||||
)
|
||||
expect(
|
||||
String(context.getBlobdir()).endsWith('db.sqlite-blobs'),
|
||||
'blobdir end with "db.sqlite-blobs"'
|
||||
)
|
||||
})
|
||||
|
||||
it('should create chat from contact and Chat methods', async function () {
|
||||
const contactId = context.createContact('aaa', 'aaa@site.org')
|
||||
|
||||
strictEqual(context.lookupContactIdByAddr('aaa@site.org'), contactId)
|
||||
strictEqual(context.lookupContactIdByAddr('nope@site.net'), 0)
|
||||
|
||||
let chatId = context.createChatByContactId(contactId)
|
||||
let chat = context.getChat(chatId)
|
||||
|
||||
strictEqual(
|
||||
chat.getVisibility(),
|
||||
C.DC_CHAT_VISIBILITY_NORMAL,
|
||||
'not archived'
|
||||
)
|
||||
strictEqual(chat.getId(), chatId, 'chat id matches')
|
||||
strictEqual(chat.getName(), 'aaa', 'chat name matches')
|
||||
strictEqual(chat.getProfileImage(), null, 'no profile image')
|
||||
strictEqual(chat.getType(), C.DC_CHAT_TYPE_SINGLE, 'single chat')
|
||||
strictEqual(chat.isSelfTalk(), false, 'no self talk')
|
||||
// TODO make sure this is really the case!
|
||||
strictEqual(chat.isUnpromoted(), false, 'not unpromoted')
|
||||
strictEqual(chat.isProtected(), false, 'not verified')
|
||||
strictEqual(typeof chat.color, 'string', 'color is a string')
|
||||
|
||||
strictEqual(context.getDraft(chatId), null, 'no draft message')
|
||||
context.setDraft(chatId, context.messageNew().setText('w00t!'))
|
||||
strictEqual(
|
||||
context.getDraft(chatId).toJson().text,
|
||||
'w00t!',
|
||||
'draft text correct'
|
||||
)
|
||||
context.setDraft(chatId, null)
|
||||
strictEqual(context.getDraft(chatId), null, 'draft removed')
|
||||
|
||||
strictEqual(context.getChatIdByContactId(contactId), chatId)
|
||||
expect(context.getChatContacts(chatId)).to.deep.equal([contactId])
|
||||
|
||||
context.setChatVisibility(chatId, C.DC_CHAT_VISIBILITY_ARCHIVED)
|
||||
strictEqual(
|
||||
context.getChat(chatId).getVisibility(),
|
||||
C.DC_CHAT_VISIBILITY_ARCHIVED,
|
||||
'chat archived'
|
||||
)
|
||||
context.setChatVisibility(chatId, C.DC_CHAT_VISIBILITY_NORMAL)
|
||||
strictEqual(
|
||||
chat.getVisibility(),
|
||||
C.DC_CHAT_VISIBILITY_NORMAL,
|
||||
'chat unarchived'
|
||||
)
|
||||
|
||||
chatId = context.createGroupChat('unverified group', false)
|
||||
chat = context.getChat(chatId)
|
||||
strictEqual(chat.isProtected(), false, 'is not verified')
|
||||
strictEqual(chat.getType(), C.DC_CHAT_TYPE_GROUP, 'group chat')
|
||||
expect(context.getChatContacts(chatId)).to.deep.equal([
|
||||
C.DC_CONTACT_ID_SELF,
|
||||
])
|
||||
|
||||
const draft2 = context.getDraft(chatId)
|
||||
expect(draft2 == null, 'unpromoted group has no draft by default')
|
||||
|
||||
context.setChatName(chatId, 'NEW NAME')
|
||||
strictEqual(context.getChat(chatId).getName(), 'NEW NAME', 'name updated')
|
||||
|
||||
chatId = context.createGroupChat('a verified group', true)
|
||||
chat = context.getChat(chatId)
|
||||
strictEqual(chat.isProtected(), true, 'is verified')
|
||||
})
|
||||
|
||||
it('test setting profile image', async function () {
|
||||
const chatId = context.createGroupChat('testing profile image group', false)
|
||||
const image = 'image.jpeg'
|
||||
const imagePath = join(__dirname, 'fixtures', image)
|
||||
const blobs = context.getBlobdir()
|
||||
|
||||
context.setChatProfileImage(chatId, imagePath)
|
||||
const blobPath = context.getChat(chatId).getProfileImage()
|
||||
expect(blobPath.startsWith(blobs)).to.be.true
|
||||
expect(blobPath.endsWith(image)).to.be.true
|
||||
|
||||
context.setChatProfileImage(chatId, null)
|
||||
expect(context.getChat(chatId).getProfileImage()).to.be.equal(
|
||||
null,
|
||||
'image is null'
|
||||
)
|
||||
})
|
||||
|
||||
it('test setting ephemeral timer', function () {
|
||||
const chatId = context.createGroupChat('testing ephemeral timer')
|
||||
|
||||
strictEqual(
|
||||
context.getChatEphemeralTimer(chatId),
|
||||
0,
|
||||
'ephemeral timer is not set by default'
|
||||
)
|
||||
|
||||
context.setChatEphemeralTimer(chatId, 60)
|
||||
strictEqual(
|
||||
context.getChatEphemeralTimer(chatId),
|
||||
60,
|
||||
'ephemeral timer is set to 1 minute'
|
||||
)
|
||||
|
||||
context.setChatEphemeralTimer(chatId, 0)
|
||||
strictEqual(
|
||||
context.getChatEphemeralTimer(chatId),
|
||||
0,
|
||||
'ephemeral timer is reset'
|
||||
)
|
||||
})
|
||||
|
||||
it('should create and delete chat', function () {
|
||||
const chatId = context.createGroupChat('GROUPCHAT')
|
||||
const chat = context.getChat(chatId)
|
||||
strictEqual(chat.getId(), chatId, 'correct chatId')
|
||||
context.deleteChat(chat.getId())
|
||||
strictEqual(context.getChat(chatId), null, 'chat removed')
|
||||
})
|
||||
|
||||
it('new message and Message methods', function () {
|
||||
const text = 'w00t!'
|
||||
const msg = context.messageNew().setText(text)
|
||||
|
||||
strictEqual(msg.getChatId(), 0, 'chat id 0 before sent')
|
||||
strictEqual(msg.getDuration(), 0, 'duration 0 before sent')
|
||||
strictEqual(msg.getFile(), '', 'no file set by default')
|
||||
strictEqual(msg.getFilebytes(), 0, 'and file bytes is 0')
|
||||
strictEqual(msg.getFilemime(), '', 'no filemime by default')
|
||||
strictEqual(msg.getFilename(), '', 'no filename set by default')
|
||||
strictEqual(msg.getFromId(), 0, 'no contact id set by default')
|
||||
strictEqual(msg.getHeight(), 0, 'plain text message have height 0')
|
||||
strictEqual(msg.getId(), 0, 'id 0 before sent')
|
||||
strictEqual(msg.getSetupcodebegin(), '', 'no setupcode begin')
|
||||
strictEqual(msg.getShowpadlock(), false, 'no padlock by default')
|
||||
|
||||
const state = msg.getState()
|
||||
strictEqual(state.isUndefined(), true, 'no state by default')
|
||||
strictEqual(state.isFresh(), false, 'no state by default')
|
||||
strictEqual(state.isNoticed(), false, 'no state by default')
|
||||
strictEqual(state.isSeen(), false, 'no state by default')
|
||||
strictEqual(state.isPending(), false, 'no state by default')
|
||||
strictEqual(state.isFailed(), false, 'no state by default')
|
||||
strictEqual(state.isDelivered(), false, 'no state by default')
|
||||
strictEqual(state.isReceived(), false, 'no state by default')
|
||||
|
||||
const summary = msg.getSummary()
|
||||
strictEqual(summary.getId(), 0, 'no summary id')
|
||||
strictEqual(summary.getState(), 0, 'no summary state')
|
||||
strictEqual(summary.getText1(), null, 'no summary text1')
|
||||
strictEqual(summary.getText1Meaning(), 0, 'no summary text1 meaning')
|
||||
strictEqual(summary.getText2(), '', 'no summary text2')
|
||||
strictEqual(summary.getTimestamp(), 0, 'no summary timestamp')
|
||||
|
||||
//strictEqual(msg.getSummarytext(50), text, 'summary text is text')
|
||||
strictEqual(msg.getText(), text, 'msg text set correctly')
|
||||
strictEqual(msg.getTimestamp(), 0, 'no timestamp')
|
||||
|
||||
const viewType = msg.getViewType()
|
||||
strictEqual(viewType.isText(), true)
|
||||
strictEqual(viewType.isImage(), false)
|
||||
strictEqual(viewType.isGif(), false)
|
||||
strictEqual(viewType.isAudio(), false)
|
||||
strictEqual(viewType.isVoice(), false)
|
||||
strictEqual(viewType.isVideo(), false)
|
||||
strictEqual(viewType.isFile(), false)
|
||||
|
||||
strictEqual(msg.getWidth(), 0, 'no message width')
|
||||
strictEqual(msg.isDeadDrop(), false, 'not deaddrop')
|
||||
strictEqual(msg.isForwarded(), false, 'not forwarded')
|
||||
strictEqual(msg.isIncreation(), false, 'not in creation')
|
||||
strictEqual(msg.isInfo(), false, 'not an info message')
|
||||
strictEqual(msg.isSent(), false, 'messge is not sent')
|
||||
strictEqual(msg.isSetupmessage(), false, 'not an autocrypt setup message')
|
||||
|
||||
msg.latefilingMediasize(10, 20, 30)
|
||||
strictEqual(msg.getWidth(), 10, 'message width set correctly')
|
||||
strictEqual(msg.getHeight(), 20, 'message height set correctly')
|
||||
strictEqual(msg.getDuration(), 30, 'message duration set correctly')
|
||||
|
||||
msg.setDimension(100, 200)
|
||||
strictEqual(msg.getWidth(), 100, 'message width set correctly')
|
||||
strictEqual(msg.getHeight(), 200, 'message height set correctly')
|
||||
|
||||
msg.setDuration(314)
|
||||
strictEqual(msg.getDuration(), 314, 'message duration set correctly')
|
||||
|
||||
expect(() => {
|
||||
msg.setFile(null)
|
||||
}).to.throw('Missing filename')
|
||||
|
||||
const logo = join(__dirname, 'fixtures', 'logo.png')
|
||||
const stat = statSync(logo)
|
||||
msg.setFile(logo)
|
||||
strictEqual(msg.getFilebytes(), stat.size, 'correct file size')
|
||||
strictEqual(msg.getFile(), logo, 'correct file name')
|
||||
strictEqual(msg.getFilemime(), 'image/png', 'mime set implicitly')
|
||||
msg.setFile(logo, 'image/gif')
|
||||
strictEqual(msg.getFilemime(), 'image/gif', 'mime set (in)correctly')
|
||||
msg.setFile(logo, 'image/png')
|
||||
strictEqual(msg.getFilemime(), 'image/png', 'mime set correctly')
|
||||
|
||||
const json = msg.toJson()
|
||||
expect(json).to.not.equal(null, 'not null')
|
||||
strictEqual(typeof json, 'object', 'json object')
|
||||
})
|
||||
|
||||
it('Contact methods', function () {
|
||||
const contactId = context.createContact('First Last', 'first.last@site.org')
|
||||
const contact = context.getContact(contactId)
|
||||
|
||||
strictEqual(contact.getAddress(), 'first.last@site.org', 'correct address')
|
||||
strictEqual(typeof contact.color, 'string', 'color is a string')
|
||||
strictEqual(contact.getDisplayName(), 'First Last', 'correct display name')
|
||||
strictEqual(contact.getId(), contactId, 'contact id matches')
|
||||
strictEqual(contact.getName(), 'First Last', 'correct name')
|
||||
strictEqual(contact.getNameAndAddress(), 'First Last (first.last@site.org)')
|
||||
strictEqual(contact.getProfileImage(), null, 'no contact image')
|
||||
strictEqual(contact.isBlocked(), false, 'not blocked')
|
||||
strictEqual(contact.isVerified(), false, 'unverified status')
|
||||
strictEqual(contact.lastSeen, 0, 'last seen unknown')
|
||||
})
|
||||
|
||||
it('create contacts from address book', function () {
|
||||
const addresses = [
|
||||
'Name One',
|
||||
'name1@site.org',
|
||||
'Name Two',
|
||||
'name2@site.org',
|
||||
'Name Three',
|
||||
'name3@site.org',
|
||||
]
|
||||
const count = context.addAddressBook(addresses.join('\n'))
|
||||
strictEqual(count, addresses.length / 2)
|
||||
context
|
||||
.getContacts(0, 'Name ')
|
||||
.map((id) => context.getContact(id))
|
||||
.forEach((contact) => {
|
||||
expect(contact.getName().startsWith('Name ')).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
it('delete contacts', function () {
|
||||
const id = context.createContact('someuser', 'someuser@site.com')
|
||||
const contact = context.getContact(id)
|
||||
strictEqual(contact.getId(), id, 'contact id matches')
|
||||
strictEqual(context.deleteContact(id), true, 'delete call succesful')
|
||||
strictEqual(context.getContact(id), null, 'contact is gone')
|
||||
})
|
||||
|
||||
it('adding and removing a contact from a chat', function () {
|
||||
const chatId = context.createGroupChat('adding_and_removing')
|
||||
const contactId = context.createContact('Add Remove', 'add.remove@site.com')
|
||||
strictEqual(
|
||||
context.addContactToChat(chatId, contactId),
|
||||
true,
|
||||
'contact added'
|
||||
)
|
||||
strictEqual(
|
||||
context.isContactInChat(chatId, contactId),
|
||||
true,
|
||||
'contact in chat'
|
||||
)
|
||||
strictEqual(
|
||||
context.removeContactFromChat(chatId, contactId),
|
||||
true,
|
||||
'contact removed'
|
||||
)
|
||||
strictEqual(
|
||||
context.isContactInChat(chatId, contactId),
|
||||
false,
|
||||
'contact not in chat'
|
||||
)
|
||||
})
|
||||
|
||||
it('blocking contacts', function () {
|
||||
const id = context.createContact('badcontact', 'bad@site.com')
|
||||
|
||||
strictEqual(context.getBlockedCount(), 0)
|
||||
strictEqual(context.getContact(id).isBlocked(), false)
|
||||
expect(context.getBlockedContacts()).to.be.empty
|
||||
|
||||
context.blockContact(id, true)
|
||||
strictEqual(context.getBlockedCount(), 1)
|
||||
strictEqual(context.getContact(id).isBlocked(), true)
|
||||
expect(context.getBlockedContacts()).to.deep.equal([id])
|
||||
|
||||
context.blockContact(id, false)
|
||||
strictEqual(context.getBlockedCount(), 0)
|
||||
strictEqual(context.getContact(id).isBlocked(), false)
|
||||
expect(context.getBlockedContacts()).to.be.empty
|
||||
})
|
||||
|
||||
it('ChatList methods', function () {
|
||||
const ids = [
|
||||
context.createGroupChat('groupchat1'),
|
||||
context.createGroupChat('groupchat11'),
|
||||
context.createGroupChat('groupchat111'),
|
||||
]
|
||||
|
||||
let chatList = context.getChatList(0, 'groupchat1', null)
|
||||
strictEqual(chatList.getCount(), 3, 'should contain above chats')
|
||||
expect(ids.indexOf(chatList.getChatId(0))).not.to.equal(-1)
|
||||
expect(ids.indexOf(chatList.getChatId(1))).not.to.equal(-1)
|
||||
expect(ids.indexOf(chatList.getChatId(2))).not.to.equal(-1)
|
||||
|
||||
const lot = chatList.getSummary(0)
|
||||
strictEqual(lot.getId(), 0, 'lot has no id')
|
||||
strictEqual(lot.getState(), C.DC_STATE_UNDEFINED, 'correct state')
|
||||
|
||||
const text = 'No messages.'
|
||||
context.createGroupChat('groupchat1111')
|
||||
chatList = context.getChatList(0, 'groupchat1111', null)
|
||||
strictEqual(
|
||||
chatList.getSummary(0).getText2(),
|
||||
text,
|
||||
'custom new group message'
|
||||
)
|
||||
|
||||
context.setChatVisibility(ids[0], C.DC_CHAT_VISIBILITY_ARCHIVED)
|
||||
chatList = context.getChatList(C.DC_GCL_ARCHIVED_ONLY, 'groupchat1', null)
|
||||
strictEqual(chatList.getCount(), 1, 'only one archived')
|
||||
})
|
||||
|
||||
it('Remove qoute from (draft) message', function () {
|
||||
context.addDeviceMessage('test_qoute', 'test')
|
||||
const msgId = context.getChatMessages(10, 0, 0)[0]
|
||||
const msg = context.messageNew()
|
||||
|
||||
msg.setQuote(context.getMessage(msgId))
|
||||
expect(msg.getQuotedMessage()).to.not.be.null
|
||||
msg.setQuote(null)
|
||||
expect(msg.getQuotedMessage()).to.be.null
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration tests', function () {
|
||||
this.timeout(60 * 3000) // increase timeout to 1min
|
||||
|
||||
let [dc, context, accountId, directory, account] = [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
]
|
||||
|
||||
let [dc2, context2, accountId2, directory2, account2] = [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
]
|
||||
|
||||
this.beforeEach(async function () {
|
||||
let tmp = DeltaChat.newTemporary()
|
||||
dc = tmp.dc
|
||||
context = tmp.context
|
||||
accountId = tmp.accountId
|
||||
directory = tmp.directory
|
||||
dc.startEvents()
|
||||
})
|
||||
|
||||
this.afterEach(async function () {
|
||||
if (context) {
|
||||
try {
|
||||
context.stopOngoingProcess()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
if (context2) {
|
||||
try {
|
||||
context2.stopOngoingProcess()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
if (dc) {
|
||||
try {
|
||||
dc.stopIO()
|
||||
dc.close()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
dc = null
|
||||
context = null
|
||||
accountId = null
|
||||
directory = null
|
||||
|
||||
context2 = null
|
||||
accountId2 = null
|
||||
directory2 = null
|
||||
})
|
||||
|
||||
this.beforeAll(async function () {
|
||||
if (!process.env.DCC_NEW_TMP_EMAIL) {
|
||||
console.log(
|
||||
'Missing DCC_NEW_TMP_EMAIL environment variable!, skip intergration tests'
|
||||
)
|
||||
this.skip()
|
||||
}
|
||||
|
||||
account = await createTempUser(process.env.DCC_NEW_TMP_EMAIL)
|
||||
if (!account || !account.email || !account.password) {
|
||||
console.log(
|
||||
"We didn't got back an account from the api, skip intergration tests"
|
||||
)
|
||||
this.skip()
|
||||
}
|
||||
})
|
||||
|
||||
it('configure', async function () {
|
||||
strictEqual(context.isConfigured(), false, 'should not be configured')
|
||||
|
||||
// Not sure what's the best way to check the events
|
||||
// TODO: check the events
|
||||
|
||||
// dc.once('DC_EVENT_CONFIGURE_PROGRESS', (data) => {
|
||||
// t.pass('DC_EVENT_CONFIGURE_PROGRESS called at least once')
|
||||
// })
|
||||
// dc.on('DC_EVENT_ERROR', (error) => {
|
||||
// console.error('DC_EVENT_ERROR', error)
|
||||
// })
|
||||
// dc.on('DC_EVENT_ERROR_NETWORK', (first, error) => {
|
||||
// console.error('DC_EVENT_ERROR_NETWORK', error)
|
||||
// })
|
||||
|
||||
// dc.on('ALL', (event, data1, data2) => console.log('ALL', event, data1, data2))
|
||||
|
||||
await expect(
|
||||
context.configure({
|
||||
addr: account.email,
|
||||
mail_pw: account.password,
|
||||
|
||||
displayname: 'Delta One',
|
||||
selfstatus: 'From Delta One with <3',
|
||||
selfavatar: join(__dirname, 'fixtures', 'avatar.png'),
|
||||
})
|
||||
).to.be.eventually.fulfilled
|
||||
|
||||
strictEqual(context.getConfig('addr'), account.email, 'addr correct')
|
||||
strictEqual(
|
||||
context.getConfig('displayname'),
|
||||
'Delta One',
|
||||
'displayName correct'
|
||||
)
|
||||
strictEqual(
|
||||
context.getConfig('selfstatus'),
|
||||
'From Delta One with <3',
|
||||
'selfStatus correct'
|
||||
)
|
||||
expect(
|
||||
context.getConfig('selfavatar').endsWith('avatar.png'),
|
||||
'selfavatar correct'
|
||||
)
|
||||
strictEqual(context.getConfig('e2ee_enabled'), '1', 'e2ee_enabled correct')
|
||||
strictEqual(
|
||||
context.getConfig('save_mime_headers'),
|
||||
'',
|
||||
'save_mime_headers correct'
|
||||
)
|
||||
|
||||
expect(context.getBlobdir().endsWith('db.sqlite-blobs'), 'correct blobdir')
|
||||
strictEqual(context.isConfigured(), true, 'is configured')
|
||||
|
||||
// whole re-configure to only change displayname: what the heck? (copied this from the old test)
|
||||
await expect(
|
||||
context.configure({
|
||||
addr: account.email,
|
||||
mail_pw: account.password,
|
||||
displayname: 'Delta Two',
|
||||
selfstatus: 'From Delta One with <3',
|
||||
selfavatar: join(__dirname, 'fixtures', 'avatar.png'),
|
||||
})
|
||||
).to.be.eventually.fulfilled
|
||||
strictEqual(
|
||||
context.getConfig('displayname'),
|
||||
'Delta Two',
|
||||
'updated displayName correct'
|
||||
)
|
||||
})
|
||||
|
||||
it('Autocrypt setup - key transfer', async function () {
|
||||
// Spawn a second dc instance with same account
|
||||
// dc.on('ALL', (event, data1, data2) =>
|
||||
// console.log('FIRST ', event, data1, data2)
|
||||
// )
|
||||
dc.stopIO()
|
||||
await expect(
|
||||
context.configure({
|
||||
addr: account.email,
|
||||
mail_pw: account.password,
|
||||
|
||||
displayname: 'Delta One',
|
||||
selfstatus: 'From Delta One with <3',
|
||||
selfavatar: join(__dirname, 'fixtures', 'avatar.png'),
|
||||
})
|
||||
).to.be.eventually.fulfilled
|
||||
|
||||
const accountId2 = dc.addAccount()
|
||||
console.log('accountId2:', accountId2)
|
||||
context2 = dc.accountContext(accountId2)
|
||||
|
||||
let setupCode = null
|
||||
const waitForSetupCode = waitForSomething()
|
||||
const waitForEnd = waitForSomething()
|
||||
|
||||
dc.on('ALL', (event, accountId, data1, data2) => {
|
||||
console.log('[' + accountId + ']', event, data1, data2)
|
||||
})
|
||||
|
||||
dc.on('DC_EVENT_MSGS_CHANGED', async (aId, chatId, msgId) => {
|
||||
console.log('[' + accountId + '] DC_EVENT_MSGS_CHANGED', chatId, msgId)
|
||||
if (
|
||||
aId != accountId ||
|
||||
!context.getChat(chatId).isSelfTalk() ||
|
||||
!context.getMessage(msgId).isSetupmessage()
|
||||
) {
|
||||
return
|
||||
}
|
||||
console.log('Setupcode!')
|
||||
let setupCode = await waitForSetupCode.promise
|
||||
// console.log('incoming msg', { setupCode })
|
||||
const messages = context.getChatMessages(chatId, 0, 0)
|
||||
expect(messages.indexOf(msgId) !== -1, 'msgId is in chat messages').to.be
|
||||
.true
|
||||
const result = await context.continueKeyTransfer(msgId, setupCode)
|
||||
expect(result === true, 'continueKeyTransfer was successful').to.be.true
|
||||
|
||||
waitForEnd.done()
|
||||
})
|
||||
|
||||
dc.stopIO()
|
||||
await expect(
|
||||
context2.configure({
|
||||
addr: account.email,
|
||||
mail_pw: account.password,
|
||||
|
||||
displayname: 'Delta One',
|
||||
selfstatus: 'From Delta One with <3',
|
||||
selfavatar: join(__dirname, 'fixtures', 'avatar.png'),
|
||||
})
|
||||
).to.be.eventually.fulfilled
|
||||
dc.startIO()
|
||||
|
||||
console.log('Sending autocrypt setup code')
|
||||
setupCode = await context2.initiateKeyTransfer()
|
||||
console.log('Sent autocrypt setup code')
|
||||
waitForSetupCode.done(setupCode)
|
||||
console.log('setupCode is: ' + setupCode)
|
||||
expect(typeof setupCode).to.equal('string', 'setupCode is string')
|
||||
|
||||
await waitForEnd.promise
|
||||
})
|
||||
|
||||
it('configure using invalid password should fail', async function () {
|
||||
await expect(
|
||||
context.configure({
|
||||
addr: 'hpk5@testrun.org',
|
||||
mail_pw: 'asd',
|
||||
})
|
||||
).to.be.eventually.rejected
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* @returns {{done: (result?)=>void, promise:Promise<any> }}
|
||||
*/
|
||||
function waitForSomething() {
|
||||
let resolvePromise
|
||||
const promise = new Promise((res, rej) => {
|
||||
resolvePromise = res
|
||||
})
|
||||
return {
|
||||
done: resolvePromise,
|
||||
promise,
|
||||
}
|
||||
}
|
||||
22
node/tsconfig.json
Normal file
22
node/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "./lib",
|
||||
"sourceMap": true,
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"strictNullChecks": true,
|
||||
"strict": true
|
||||
},
|
||||
"exclude": ["node_modules", "deltachat-core-rust", "dist", "scripts"],
|
||||
"typedocOptions": {
|
||||
"mode": "file",
|
||||
"out": "docs",
|
||||
"excludeNotExported": true,
|
||||
"defaultCategory": "index",
|
||||
"includeVersion": true
|
||||
}
|
||||
}
|
||||
37
node/windows.md
Normal file
37
node/windows.md
Normal file
@@ -0,0 +1,37 @@
|
||||
> Steps on how to get windows set up properly for the node bindings
|
||||
|
||||
## install git
|
||||
|
||||
E.g via <https://git-scm.com/download/win>
|
||||
|
||||
## install node
|
||||
|
||||
Download and install `v16` from <https://nodejs.org/en/>
|
||||
|
||||
## install rust
|
||||
|
||||
Download and run `rust-init.exe` from <https://www.rust-lang.org/tools/install>
|
||||
|
||||
## configure node for native addons
|
||||
|
||||
```
|
||||
$ npm i node-gyp -g
|
||||
$ npm i windows-build-tools -g
|
||||
```
|
||||
|
||||
`windows-build-tools` will install `Visual Studio 2017` by default and should not mess with existing installations of `Visual Studio C++`.
|
||||
|
||||
## get the code
|
||||
|
||||
```
|
||||
$ mkdir -p src/deltachat
|
||||
$ cd src/deltachat
|
||||
$ git clone https://github.com/deltachat/deltachat-node
|
||||
```
|
||||
|
||||
## build the code
|
||||
|
||||
```
|
||||
$ cd src/deltachat/deltachat-node
|
||||
$ npm install
|
||||
```
|
||||
65
package.json
Normal file
65
package.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"napi-macros": "^2.0.0",
|
||||
"node-gyp-build": "^4.1.0",
|
||||
"split2": "^3.1.1"
|
||||
},
|
||||
"description": "node.js bindings for deltachat-core",
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/node": "^16.11.26",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"esm": "^3.2.25",
|
||||
"hallmark": "^2.0.0",
|
||||
"mocha": "^8.2.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-gyp": "^9.0.0",
|
||||
"opn-cli": "^5.0.0",
|
||||
"prebuildify": "^3.0.0",
|
||||
"prebuildify-ci": "^1.0.4",
|
||||
"prettier": "^2.0.5",
|
||||
"typedoc": "^0.17.0",
|
||||
"typescript": "^3.9.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"files": [
|
||||
"node/scripts/*",
|
||||
"*"
|
||||
],
|
||||
"homepage": "https://github.com/deltachat/deltachat-core-rust/tree/master/node",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"main": "node/dist/index.js",
|
||||
"name": "deltachat-node",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/deltachat/deltachat-core-rust.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:core && npm run build:bindings",
|
||||
"build:bindings": "npm run build:bindings:c && npm run build:bindings:ts",
|
||||
"build:bindings:c": "npm run build:bindings:c:c && npm run build:bindings:c:postinstall",
|
||||
"build:bindings:c:c": "cd node && npx node-gyp rebuild",
|
||||
"build:bindings:c:postinstall": "node node/scripts/postinstall.js",
|
||||
"build:bindings:ts": "cd node && tsc",
|
||||
"build:core": "npm run build:core:rust && npm run build:core:constants",
|
||||
"build:core:constants": "node node/scripts/generate-constants.js",
|
||||
"build:core:rust": "node node/scripts/rebuild-core.js",
|
||||
"clean": "rm -rf node/dist node/build node/prebuilds node/node_modules ./target",
|
||||
"download-prebuilds": "prebuildify-ci download",
|
||||
"hallmark": "hallmark --fix",
|
||||
"install": "node node/scripts/install.js",
|
||||
"install:prebuilds": "cd node && node-gyp-build \"npm run build:core\" \"npm run build:bindings:c:postinstall\"",
|
||||
"lint": "prettier --check \"node/lib/**/*.{ts,tsx}\"",
|
||||
"lint-fix": "prettier --write \"node/lib/**/*.{ts,tsx}\" \"node/test/**/*.js\"",
|
||||
"prebuildify": "cd node && prebuildify -t 16.13.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
|
||||
"test": "npm run test:lint && npm run test:mocha",
|
||||
"test:lint": "npm run lint",
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.81.0"
|
||||
}
|
||||
@@ -22,7 +22,7 @@ def test_echo_quit_plugin(acfactory, lp):
|
||||
botproc = acfactory.run_bot_process(echo_and_quit)
|
||||
|
||||
lp.sec("creating a temp account to contact the bot")
|
||||
ac1 = acfactory.get_one_online_account()
|
||||
ac1, = acfactory.get_online_accounts(1)
|
||||
|
||||
lp.sec("sending a message to the bot")
|
||||
bot_contact = ac1.create_contact(botproc.addr)
|
||||
@@ -42,7 +42,7 @@ def test_group_tracking_plugin(acfactory, lp):
|
||||
lp.sec("creating one group-tracking bot and two temp accounts")
|
||||
botproc = acfactory.run_bot_process(group_tracking, ffi=False)
|
||||
|
||||
ac1, ac2 = acfactory.get_two_online_accounts(quiet=True)
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
botproc.fnmatch_lines("""
|
||||
*ac_configure_completed*
|
||||
|
||||
@@ -24,7 +24,7 @@ if __name__ == "__main__":
|
||||
|
||||
print("running:", " ".join(cmd))
|
||||
subprocess.check_call(cmd)
|
||||
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)
|
||||
subprocess.check_call("rm -rf build/ src/deltachat/*.so src/deltachat/*.dylib src/deltachat/*.dll" , shell=True)
|
||||
|
||||
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":
|
||||
subprocess.check_call([
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
[mypy]
|
||||
|
||||
[mypy-py.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-deltachat.capi.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
@@ -17,3 +20,7 @@ ignore_missing_imports = True
|
||||
|
||||
[mypy-_pytest.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-imap_tools.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0"]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0", "pkgconfig"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
|
||||
@@ -11,8 +11,11 @@ def main():
|
||||
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
|
||||
long_description=long_description,
|
||||
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
||||
install_requires=['cffi>=1.0.0', 'pluggy', 'imapclient', 'requests'],
|
||||
setup_requires=['setuptools_scm'], # required for compatibility with `python3 setup.py sdist`
|
||||
install_requires=['cffi>=1.0.0', 'pluggy', 'imap-tools', 'requests'],
|
||||
setup_requires=[
|
||||
'setuptools_scm', # required for compatibility with `python3 setup.py sdist`
|
||||
'pkgconfig',
|
||||
],
|
||||
packages=setuptools.find_packages('src'),
|
||||
package_dir={'': 'src'},
|
||||
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
||||
|
||||
@@ -2,7 +2,7 @@ import sys
|
||||
|
||||
from . import capi, const, hookspec # noqa
|
||||
from .capi import ffi # noqa
|
||||
from .account import Account # noqa
|
||||
from .account import Account, get_core_info # noqa
|
||||
from .message import Message # noqa
|
||||
from .contact import Contact # noqa
|
||||
from .chat import Chat # noqa
|
||||
|
||||
@@ -8,9 +8,9 @@ import shutil
|
||||
import subprocess
|
||||
import tempfile
|
||||
import textwrap
|
||||
import types
|
||||
|
||||
import cffi
|
||||
import pkgconfig # type: ignore
|
||||
|
||||
|
||||
def local_build_flags(projdir, target):
|
||||
@@ -19,36 +19,31 @@ def local_build_flags(projdir, target):
|
||||
:param projdir: The root directory of the deltachat-core-rust project.
|
||||
:param target: The rust build target, `debug` or `release`.
|
||||
"""
|
||||
flags = types.SimpleNamespace()
|
||||
flags = {}
|
||||
if platform.system() == 'Darwin':
|
||||
flags.libs = ['resolv', 'dl']
|
||||
flags.extra_link_args = [
|
||||
flags['libraries'] = ['resolv', 'dl']
|
||||
flags['extra_link_args'] = [
|
||||
'-framework', 'CoreFoundation',
|
||||
'-framework', 'CoreServices',
|
||||
'-framework', 'Security',
|
||||
]
|
||||
elif platform.system() == 'Linux':
|
||||
flags.libs = ['rt', 'dl', 'm']
|
||||
flags.extra_link_args = []
|
||||
flags['libraries'] = ['rt', 'dl', 'm']
|
||||
flags['extra_link_args'] = []
|
||||
else:
|
||||
raise NotImplementedError("Compilation not supported yet on Windows, can you help?")
|
||||
target_dir = os.environ.get("CARGO_TARGET_DIR")
|
||||
if target_dir is None:
|
||||
target_dir = os.path.join(projdir, 'target')
|
||||
flags.objs = [os.path.join(target_dir, target, 'libdeltachat.a')]
|
||||
assert os.path.exists(flags.objs[0]), flags.objs
|
||||
flags.incs = [os.path.join(projdir, 'deltachat-ffi')]
|
||||
flags['extra_objects'] = [os.path.join(target_dir, target, 'libdeltachat.a')]
|
||||
assert os.path.exists(flags['extra_objects'][0]), flags['extra_objects']
|
||||
flags['include_dirs'] = [os.path.join(projdir, 'deltachat-ffi')]
|
||||
return flags
|
||||
|
||||
|
||||
def system_build_flags():
|
||||
"""Construct build flags for building against an installed libdeltachat."""
|
||||
flags = types.SimpleNamespace()
|
||||
flags.libs = ['deltachat']
|
||||
flags.objs = []
|
||||
flags.incs = []
|
||||
flags.extra_link_args = []
|
||||
return flags
|
||||
return pkgconfig.parse('deltachat')
|
||||
|
||||
|
||||
def extract_functions(flags):
|
||||
@@ -69,7 +64,7 @@ def extract_functions(flags):
|
||||
src_fp.write('#include <deltachat.h>')
|
||||
cc.preprocess(source=src_name,
|
||||
output_file=dst_name,
|
||||
include_dirs=flags.incs,
|
||||
include_dirs=flags['include_dirs'],
|
||||
macros=[('PY_CFFI', '1')])
|
||||
with open(dst_name, "r") as dst_fp:
|
||||
return dst_fp.read()
|
||||
@@ -105,7 +100,7 @@ def find_header(flags):
|
||||
try:
|
||||
os.chdir(tmpdir)
|
||||
cc.compile(sources=["where.c"],
|
||||
include_dirs=flags.incs,
|
||||
include_dirs=flags['include_dirs'],
|
||||
macros=[("PY_CFFI_INC", "1")])
|
||||
finally:
|
||||
os.chdir(cwd)
|
||||
@@ -183,10 +178,7 @@ def ffibuilder():
|
||||
return DC_EVENT_DATA2_IS_STRING(e);
|
||||
}
|
||||
""",
|
||||
include_dirs=flags.incs,
|
||||
libraries=flags.libs,
|
||||
extra_objects=flags.objs,
|
||||
extra_link_args=flags.extra_link_args,
|
||||
**flags,
|
||||
)
|
||||
builder.cdef("""
|
||||
typedef int... time_t;
|
||||
|
||||
@@ -22,6 +22,29 @@ class MissingCredentials(ValueError):
|
||||
""" Account is missing `addr` and `mail_pw` config values. """
|
||||
|
||||
|
||||
def get_core_info():
|
||||
""" get some system info. """
|
||||
from tempfile import NamedTemporaryFile
|
||||
|
||||
with NamedTemporaryFile() as path:
|
||||
path.close()
|
||||
return get_dc_info_as_dict(ffi.gc(
|
||||
lib.dc_context_new(as_dc_charpointer(""), as_dc_charpointer(path.name), ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
))
|
||||
|
||||
|
||||
def get_dc_info_as_dict(dc_context):
|
||||
lines = from_dc_charpointer(lib.dc_get_info(dc_context))
|
||||
info_dict = {}
|
||||
for line in lines.split("\n"):
|
||||
if not line.strip():
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
info_dict[key.lower()] = value
|
||||
return info_dict
|
||||
|
||||
|
||||
class Account(object):
|
||||
""" Each account is tied to a sqlite database file which is fully managed
|
||||
by the underlying deltachat core library. All public Account methods are
|
||||
@@ -67,6 +90,9 @@ class Account(object):
|
||||
""" re-enable logging. """
|
||||
self._logging = True
|
||||
|
||||
def __repr__(self):
|
||||
return "<Account path={}>".format(self.db_path)
|
||||
|
||||
# def __del__(self):
|
||||
# self.shutdown()
|
||||
|
||||
@@ -81,14 +107,7 @@ class Account(object):
|
||||
|
||||
def get_info(self) -> Dict[str, str]:
|
||||
""" return dictionary of built config parameters. """
|
||||
lines = from_dc_charpointer(lib.dc_get_info(self._dc_context))
|
||||
d = {}
|
||||
for line in lines.split("\n"):
|
||||
if not line.strip():
|
||||
continue
|
||||
key, value = line.split("=", 1)
|
||||
d[key.lower()] = value
|
||||
return d
|
||||
return get_dc_info_as_dict(self._dc_context)
|
||||
|
||||
def dump_account_info(self, logfile):
|
||||
def log(*args, **kwargs):
|
||||
@@ -129,6 +148,8 @@ class Account(object):
|
||||
namebytes = name.encode("utf8")
|
||||
if namebytes == b"addr" and self.is_configured():
|
||||
raise ValueError("can not change 'addr' after account is configured.")
|
||||
if isinstance(value, (int, bool)):
|
||||
value = str(int(value))
|
||||
if value is not None:
|
||||
valuebytes = value.encode("utf8")
|
||||
else:
|
||||
@@ -169,7 +190,7 @@ class Account(object):
|
||||
:returns: None
|
||||
"""
|
||||
for key, value in kwargs.items():
|
||||
self.set_config(key, str(value))
|
||||
self.set_config(key, value)
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
""" determine if the account is configured already; an initial connection
|
||||
@@ -179,6 +200,12 @@ class Account(object):
|
||||
"""
|
||||
return True if lib.dc_is_configured(self._dc_context) else False
|
||||
|
||||
def is_open(self) -> bool:
|
||||
"""Determine if account is open
|
||||
|
||||
:returns True if account is open."""
|
||||
return True if lib.dc_context_is_open(self._dc_context) else False
|
||||
|
||||
def set_avatar(self, img_path: Optional[str]) -> None:
|
||||
"""Set self avatar.
|
||||
|
||||
@@ -563,6 +590,8 @@ class Account(object):
|
||||
""" add an account plugin which implements one or more of
|
||||
the :class:`deltachat.hookspec.PerAccount` hooks.
|
||||
"""
|
||||
if name and self._pm.has_plugin(name=name):
|
||||
self._pm.unregister(name=name)
|
||||
self._pm.register(plugin, name=name)
|
||||
self._pm.check_pending()
|
||||
return plugin
|
||||
@@ -666,28 +695,25 @@ class Account(object):
|
||||
if self._dc_context is None:
|
||||
return
|
||||
|
||||
self.stop_io()
|
||||
|
||||
self.log("remove dc_context references")
|
||||
|
||||
# if _dc_context is unref'ed the event thread should quickly
|
||||
# receive the termination signal. However, some python code might
|
||||
# still hold a reference and so we use a secondary signal
|
||||
# to make sure the even thread terminates if it receives any new
|
||||
# event, indepedently from waiting for the core to send NULL to
|
||||
# get_next_event().
|
||||
# mark the event thread for shutdown (latest on next incoming event)
|
||||
self._event_thread.mark_shutdown()
|
||||
self._dc_context = None
|
||||
|
||||
# stop_io also causes an info event which will wake up
|
||||
# the EventThread's inner loop and let it notice the shutdown marker.
|
||||
self.stop_io()
|
||||
|
||||
self.log("wait for event thread to finish")
|
||||
try:
|
||||
self._event_thread.wait(timeout=2)
|
||||
self._event_thread.wait(timeout=5)
|
||||
except RuntimeError as e:
|
||||
self.log("Waiting for event thread failed: {}".format(e))
|
||||
|
||||
if self._event_thread.is_alive():
|
||||
self.log("WARN: event thread did not terminate yet, ignoring.")
|
||||
|
||||
self.log("remove dc_context references, making the Account unusable")
|
||||
self._dc_context = None
|
||||
|
||||
self._shutdown_event.set()
|
||||
|
||||
hook = hookspec.Global._get_plugin_manager().hook
|
||||
|
||||
@@ -5,7 +5,7 @@ import calendar
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
import os
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
|
||||
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array
|
||||
from .capi import lib, ffi
|
||||
from . import const
|
||||
from .message import Message
|
||||
@@ -517,7 +517,7 @@ class Chat(object):
|
||||
lib.dc_array_get_timestamp(dc_array, i),
|
||||
timezone.utc
|
||||
),
|
||||
marker=from_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
|
||||
marker=from_optional_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
|
||||
)
|
||||
for i in range(lib.dc_array_get_cnt(dc_array))
|
||||
]
|
||||
|
||||
@@ -4,63 +4,20 @@ and for cleaning up inbox/mvbox for each test function run.
|
||||
"""
|
||||
|
||||
import io
|
||||
import email
|
||||
import ssl
|
||||
import pathlib
|
||||
from imapclient import IMAPClient
|
||||
from imapclient.exceptions import IMAPClientError
|
||||
from contextlib import contextmanager
|
||||
from imap_tools import MailBox, MailBoxTls, errors, AND, Header, MailMessageFlags, MailMessage
|
||||
import imaplib
|
||||
import deltachat
|
||||
from deltachat import const, Account
|
||||
from typing import List
|
||||
|
||||
|
||||
SEEN = b'\\Seen'
|
||||
DELETED = b'\\Deleted'
|
||||
FLAGS = b'FLAGS'
|
||||
FETCH = b'FETCH'
|
||||
ALL = "1:*"
|
||||
|
||||
|
||||
@deltachat.global_hookimpl
|
||||
def dc_account_extra_configure(account):
|
||||
""" Reset the account (we reuse accounts across tests)
|
||||
and make 'account.direct_imap' available for direct IMAP ops.
|
||||
"""
|
||||
try:
|
||||
|
||||
if not hasattr(account, "direct_imap"):
|
||||
imap = DirectImap(account)
|
||||
|
||||
for folder in imap.list_folders():
|
||||
if folder.lower() == "inbox" or folder.lower() == "deltachat":
|
||||
assert imap.select_folder(folder)
|
||||
imap.delete(ALL, expunge=True)
|
||||
else:
|
||||
imap.conn.delete_folder(folder)
|
||||
# We just deleted the folder, so we have to make DC forget about it, too
|
||||
if account.get_config("configured_sentbox_folder") == folder:
|
||||
account.set_config("configured_sentbox_folder", None)
|
||||
if account.get_config("configured_spam_folder") == folder:
|
||||
account.set_config("configured_spam_folder", None)
|
||||
|
||||
setattr(account, "direct_imap", imap)
|
||||
|
||||
except Exception as e:
|
||||
# Uncaught exceptions here would lead to a timeout without any note written to the log
|
||||
# start with DC_EVENT_WARNING so that the line is printed in yellow and won't be overlooked when reading
|
||||
account.log("DC_EVENT_WARNING =================== DIRECT_IMAP CAN'T RESET ACCOUNT: ===================")
|
||||
account.log("DC_EVENT_WARNING =================== " + str(e) + " ===================")
|
||||
|
||||
|
||||
@deltachat.global_hookimpl
|
||||
def dc_account_after_shutdown(account):
|
||||
""" shutdown the imap connection if there is one. """
|
||||
imap = getattr(account, "direct_imap", None)
|
||||
if imap is not None:
|
||||
imap.shutdown()
|
||||
del account.direct_imap
|
||||
|
||||
|
||||
class DirectImap:
|
||||
def __init__(self, account: Account) -> None:
|
||||
self.account = account
|
||||
@@ -88,37 +45,30 @@ class DirectImap:
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
|
||||
if security == const.DC_SOCKET_STARTTLS:
|
||||
self.conn = IMAPClient(host, port, ssl=False)
|
||||
self.conn.starttls(ssl_context)
|
||||
elif security == const.DC_SOCKET_PLAIN:
|
||||
self.conn = IMAPClient(host, port, ssl=False)
|
||||
elif security == const.DC_SOCKET_SSL:
|
||||
self.conn = IMAPClient(host, port, ssl_context=ssl_context)
|
||||
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
|
||||
elif security == const.DC_SOCKET_PLAIN or security == const.DC_SOCKET_SSL:
|
||||
self.conn = MailBox(host, port, ssl_context=ssl_context)
|
||||
self.conn.login(user, pw)
|
||||
|
||||
self.select_folder("INBOX")
|
||||
|
||||
def shutdown(self):
|
||||
try:
|
||||
self.conn.idle_done()
|
||||
except (OSError, IMAPClientError):
|
||||
pass
|
||||
try:
|
||||
self.conn.logout()
|
||||
except (OSError, IMAPClientError):
|
||||
except (OSError, imaplib.IMAP4.abort):
|
||||
print("Could not logout direct_imap conn")
|
||||
|
||||
def create_folder(self, foldername):
|
||||
try:
|
||||
self.conn.create_folder(foldername)
|
||||
except imaplib.IMAP4.error as e:
|
||||
self.conn.folder.create(foldername)
|
||||
except errors.MailboxFolderCreateError as e:
|
||||
print("Can't create", foldername, "probably it already exists:", str(e))
|
||||
|
||||
def select_folder(self, foldername):
|
||||
def select_folder(self, foldername: str) -> tuple:
|
||||
assert not self._idling
|
||||
return self.conn.select_folder(foldername)
|
||||
return self.conn.folder.set(foldername)
|
||||
|
||||
def select_config_folder(self, config_name):
|
||||
def select_config_folder(self, config_name: str):
|
||||
""" Return info about selected folder if it is
|
||||
configured, otherwise None. """
|
||||
if "_" not in config_name:
|
||||
@@ -127,50 +77,36 @@ class DirectImap:
|
||||
if foldername:
|
||||
return self.select_folder(foldername)
|
||||
|
||||
def list_folders(self):
|
||||
def list_folders(self) -> List[str]:
|
||||
""" return list of all existing folder names"""
|
||||
assert not self._idling
|
||||
folders = []
|
||||
for meta, sep, foldername in self.conn.list_folders():
|
||||
folders.append(foldername)
|
||||
return folders
|
||||
return [folder.name for folder in self.conn.folder.list()]
|
||||
|
||||
def delete(self, range, expunge=True):
|
||||
def delete(self, uid_list: str, expunge=True):
|
||||
""" delete a range of messages (imap-syntax).
|
||||
If expunge is true, perform the expunge-operation
|
||||
to make sure the messages are really gone and not
|
||||
just flagged as deleted.
|
||||
"""
|
||||
self.conn.set_flags(range, [DELETED])
|
||||
self.conn.client.uid('STORE', uid_list, '+FLAGS', r'(\Deleted)')
|
||||
if expunge:
|
||||
self.conn.expunge()
|
||||
|
||||
def get_all_messages(self):
|
||||
def get_all_messages(self) -> List[MailMessage]:
|
||||
assert not self._idling
|
||||
return [mail for mail in self.conn.fetch()]
|
||||
|
||||
# Flush unsolicited responses. IMAPClient has problems
|
||||
# dealing with them: https://github.com/mjs/imapclient/issues/334
|
||||
# When this NOOP was introduced, next FETCH returned empty
|
||||
# result instead of a single message, even though IMAP server
|
||||
# can only return more untagged responses than required, not
|
||||
# less.
|
||||
self.conn.noop()
|
||||
|
||||
return self.conn.fetch(ALL, [FLAGS])
|
||||
|
||||
def get_unread_messages(self):
|
||||
def get_unread_messages(self) -> List[str]:
|
||||
assert not self._idling
|
||||
res = self.conn.fetch(ALL, [FLAGS])
|
||||
return [uid for uid in res
|
||||
if SEEN not in res[uid][FLAGS]]
|
||||
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
|
||||
|
||||
def mark_all_read(self):
|
||||
messages = self.get_unread_messages()
|
||||
if messages:
|
||||
res = self.conn.set_flags(messages, [SEEN])
|
||||
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
|
||||
print("marked seen:", messages, res)
|
||||
|
||||
def get_unread_cnt(self):
|
||||
def get_unread_cnt(self) -> int:
|
||||
return len(self.get_unread_messages())
|
||||
|
||||
def dump_imap_structures(self, dir, logfile):
|
||||
@@ -192,75 +128,84 @@ class DirectImap:
|
||||
log("---------", imapfolder, len(messages), "messages ---------")
|
||||
# get message content without auto-marking it as seen
|
||||
# fetching 'RFC822' would mark it as seen.
|
||||
requested = [b'BODY.PEEK[]', FLAGS]
|
||||
for uid, data in self.conn.fetch(messages, requested).items():
|
||||
body_bytes = data[b'BODY[]']
|
||||
if not body_bytes:
|
||||
log("Message", uid, "has empty body")
|
||||
for msg in self.conn.fetch(mark_seen=False):
|
||||
body = getattr(msg.obj, "text", None)
|
||||
if not body:
|
||||
body = getattr(msg.obj, "html", None)
|
||||
if not body:
|
||||
log("Message", msg.uid, "has empty body")
|
||||
continue
|
||||
|
||||
flags = data[FLAGS]
|
||||
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
|
||||
path.mkdir(parents=True, exist_ok=True)
|
||||
fn = path.joinpath(str(uid))
|
||||
fn.write_bytes(body_bytes)
|
||||
log("Message", uid, fn)
|
||||
email_message = email.message_from_bytes(body_bytes)
|
||||
log("Message", uid, flags, "Message-Id:", email_message.get("Message-Id"))
|
||||
fn = path.joinpath(str(msg.uid))
|
||||
fn.write_bytes(body)
|
||||
log("Message", msg.uid, fn)
|
||||
log("Message", msg.uid, msg.flags, "Message-Id:", msg.obj.get("Message-Id"))
|
||||
|
||||
if empty_folders:
|
||||
log("--------- EMPTY FOLDERS:", empty_folders)
|
||||
|
||||
print(stream.getvalue(), file=logfile)
|
||||
|
||||
def idle_start(self):
|
||||
""" switch this connection to idle mode. non-blocking. """
|
||||
assert not self._idling
|
||||
res = self.conn.idle()
|
||||
self._idling = True
|
||||
return res
|
||||
@contextmanager
|
||||
def idle(self):
|
||||
""" return Idle ContextManager. """
|
||||
idle_manager = IdleManager(self)
|
||||
try:
|
||||
yield idle_manager
|
||||
finally:
|
||||
idle_manager.done()
|
||||
|
||||
def idle_check(self, terminate=False):
|
||||
""" (blocking) wait for next idle message from server. """
|
||||
assert self._idling
|
||||
self.account.log("imap-direct: calling idle_check")
|
||||
res = self.conn.idle_check(timeout=30)
|
||||
if len(res) == 0:
|
||||
raise TimeoutError
|
||||
if terminate:
|
||||
self.idle_done()
|
||||
self.account.log("imap-direct: idle_check returned {!r}".format(res))
|
||||
return res
|
||||
|
||||
def idle_wait_for_seen(self):
|
||||
""" Return first message with SEEN flag
|
||||
from a running idle-stream REtiurn.
|
||||
"""
|
||||
while 1:
|
||||
for item in self.idle_check():
|
||||
if item[1] == FETCH:
|
||||
if item[2][0] == FLAGS:
|
||||
if SEEN in item[2][1]:
|
||||
return item[0]
|
||||
|
||||
def idle_done(self):
|
||||
""" send idle-done to server if we are currently in idle mode. """
|
||||
if self._idling:
|
||||
res = self.conn.idle_done()
|
||||
self._idling = False
|
||||
return res
|
||||
|
||||
def append(self, folder, msg):
|
||||
def append(self, folder: str, msg: str):
|
||||
"""Upload a message to *folder*.
|
||||
Trailing whitespace or a linebreak at the beginning will be removed automatically.
|
||||
"""
|
||||
if msg.startswith("\n"):
|
||||
msg = msg[1:]
|
||||
msg = '\n'.join([s.lstrip() for s in msg.splitlines()])
|
||||
self.conn.append(folder, msg)
|
||||
self.conn.append(bytes(msg, encoding='ascii'), folder)
|
||||
|
||||
def get_uid_by_message_id(self, message_id):
|
||||
msgs = self.conn.search(['HEADER', 'MESSAGE-ID', message_id])
|
||||
def get_uid_by_message_id(self, message_id) -> str:
|
||||
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header('MESSAGE-ID', message_id)))]
|
||||
if len(msgs) == 0:
|
||||
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
|
||||
return msgs[0]
|
||||
|
||||
|
||||
class IdleManager:
|
||||
def __init__(self, direct_imap):
|
||||
self.direct_imap = direct_imap
|
||||
self.log = direct_imap.account.log
|
||||
# fetch latest messages before starting idle so that it only
|
||||
# returns messages that arrive anew
|
||||
self.direct_imap.conn.fetch("1:*")
|
||||
self.direct_imap.conn.idle.start()
|
||||
|
||||
def check(self, timeout=None) -> List[bytes]:
|
||||
""" (blocking) wait for next idle message from server. """
|
||||
self.log("imap-direct: calling idle_check")
|
||||
res = self.direct_imap.conn.idle.poll(timeout=timeout)
|
||||
self.log("imap-direct: idle_check returned {!r}".format(res))
|
||||
return res
|
||||
|
||||
def wait_for_new_message(self, timeout=None) -> bytes:
|
||||
while 1:
|
||||
for item in self.check(timeout=timeout):
|
||||
if b'EXISTS' in item or b'RECENT' in item:
|
||||
return item
|
||||
|
||||
def wait_for_seen(self, timeout=None) -> int:
|
||||
""" Return first message with SEEN flag from a running idle-stream.
|
||||
"""
|
||||
while 1:
|
||||
for item in self.check(timeout=timeout):
|
||||
if FETCH in item:
|
||||
self.log(str(item))
|
||||
if FLAGS in item and rb'\Seen' in item:
|
||||
return int(item.split(b' ')[1])
|
||||
|
||||
def done(self):
|
||||
""" send idle-done to server if we are currently in idle mode. """
|
||||
res = self.direct_imap.conn.idle.stop()
|
||||
return res
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
import threading
|
||||
import sys
|
||||
import traceback
|
||||
import time
|
||||
import io
|
||||
import re
|
||||
import os
|
||||
from queue import Queue, Empty
|
||||
@@ -29,10 +32,12 @@ class FFIEventLogger:
|
||||
# to prevent garbled logging
|
||||
_loglock = threading.RLock()
|
||||
|
||||
def __init__(self, account) -> None:
|
||||
def __init__(self, account, logid=None, init_time=None) -> None:
|
||||
self.account = account
|
||||
self.logid = self.account.get_config("displayname")
|
||||
self.init_time = time.time()
|
||||
self.logid = logid or self.account.get_config("displayname")
|
||||
if init_time is None:
|
||||
init_time = time.time()
|
||||
self.init_time = init_time
|
||||
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(self, ffi_event: FFIEvent) -> None:
|
||||
@@ -156,14 +161,14 @@ class FFIEventTracker:
|
||||
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), self.account)
|
||||
break
|
||||
|
||||
def wait_all_initial_fetches(self):
|
||||
def wait_idle_inbox_ready(self):
|
||||
"""Has to be called after start_io() to wait for fetch_existing_msgs to run
|
||||
so that new messages are not mistaken for old ones:
|
||||
- ac1 and ac2 are created
|
||||
- ac1 sends a message to ac2
|
||||
- ac2 is still running FetchExsistingMsgs job and thinks it's an existing, old message
|
||||
- therefore no DC_EVENT_INCOMING_MSG is sent"""
|
||||
self.get_info_contains("Done fetching existing messages")
|
||||
self.get_info_contains("INBOX: Idle entering")
|
||||
|
||||
def wait_next_incoming_message(self):
|
||||
""" wait for and return next incoming message. """
|
||||
@@ -225,9 +230,7 @@ class EventThread(threading.Thread):
|
||||
)
|
||||
while not self._marked_for_shutdown:
|
||||
event = lib.dc_get_next_event(event_emitter)
|
||||
if event == ffi.NULL:
|
||||
break
|
||||
if self._marked_for_shutdown:
|
||||
if event == ffi.NULL or self._marked_for_shutdown:
|
||||
break
|
||||
evt = lib.dc_event_get_id(event)
|
||||
data1 = lib.dc_event_get_data1_int(event)
|
||||
@@ -241,15 +244,23 @@ class EventThread(threading.Thread):
|
||||
|
||||
lib.dc_event_unref(event)
|
||||
ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2)
|
||||
try:
|
||||
with self.swallow_and_log_exception("ac_process_ffi_event {}".format(ffi_event)):
|
||||
self.account._pm.hook.ac_process_ffi_event(account=self, ffi_event=ffi_event)
|
||||
for name, kwargs in self._map_ffi_event(ffi_event):
|
||||
self.account.log("calling hook name={} kwargs={}".format(name, kwargs))
|
||||
hook = getattr(self.account._pm.hook, name)
|
||||
for name, kwargs in self._map_ffi_event(ffi_event):
|
||||
hook = getattr(self.account._pm.hook, name)
|
||||
info = "call {} kwargs={} failed".format(name, kwargs)
|
||||
with self.swallow_and_log_exception(info):
|
||||
hook(**kwargs)
|
||||
except Exception:
|
||||
if self.account._dc_context is not None:
|
||||
raise
|
||||
|
||||
@contextmanager
|
||||
def swallow_and_log_exception(self, info):
|
||||
try:
|
||||
yield
|
||||
except Exception as ex:
|
||||
logfile = io.StringIO()
|
||||
traceback.print_exception(*sys.exc_info(), file=logfile)
|
||||
self.account.log("{}\nException {}\nTraceback:\n{}"
|
||||
.format(info, ex, logfile.getvalue()))
|
||||
|
||||
def _map_ffi_event(self, ffi_event: FFIEvent):
|
||||
name = ffi_event.name
|
||||
|
||||
@@ -8,35 +8,43 @@ import threading
|
||||
import fnmatch
|
||||
import time
|
||||
import weakref
|
||||
import tempfile
|
||||
from typing import List, Dict, Callable
|
||||
from queue import Queue
|
||||
from typing import List, Callable
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
import pathlib
|
||||
|
||||
from . import Account, const
|
||||
from .capi import lib
|
||||
from . import Account, const, account_hookimpl, get_core_info
|
||||
from .events import FFIEventLogger, FFIEventTracker
|
||||
from _pytest._code import Source
|
||||
from deltachat import direct_imap
|
||||
|
||||
import deltachat
|
||||
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption(
|
||||
group = parser.getgroup("deltachat testplugin options")
|
||||
group.addoption(
|
||||
"--liveconfig", action="store", default=None,
|
||||
help="a file with >=2 lines where each line "
|
||||
"contains NAME=VALUE config settings for one account"
|
||||
)
|
||||
parser.addoption(
|
||||
group.addoption(
|
||||
"--ignored", action="store_true",
|
||||
help="Also run tests marked with the ignored marker",
|
||||
)
|
||||
parser.addoption(
|
||||
group.addoption(
|
||||
"--strict-tls", action="store_true",
|
||||
help="Never accept invalid TLS certificates for test accounts",
|
||||
)
|
||||
group.addoption(
|
||||
"--extra-info", action="store_true",
|
||||
help="show more info on failures (imap server state, config)"
|
||||
)
|
||||
group.addoption(
|
||||
"--debug-setup", action="store_true",
|
||||
help="show events during configure and start io phases of online accounts"
|
||||
)
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
@@ -91,9 +99,12 @@ def pytest_configure(config):
|
||||
|
||||
@pytest.hookimpl(hookwrapper=True)
|
||||
def pytest_runtest_teardown(self, item):
|
||||
self.enable_logging(item)
|
||||
logging = item.config.getoption("--extra-info")
|
||||
if logging:
|
||||
self.enable_logging(item)
|
||||
yield
|
||||
self.disable_logging(item)
|
||||
if logging:
|
||||
self.disable_logging(item)
|
||||
|
||||
la = LoggingAspect()
|
||||
config.pluginmanager.register(la)
|
||||
@@ -101,20 +112,12 @@ def pytest_configure(config):
|
||||
|
||||
|
||||
def pytest_report_header(config, startdir):
|
||||
summary = []
|
||||
|
||||
t = tempfile.mktemp()
|
||||
try:
|
||||
ac = Account(t)
|
||||
info = ac.get_info()
|
||||
ac.shutdown()
|
||||
finally:
|
||||
os.remove(t)
|
||||
summary.extend(['Deltachat core={} sqlite={} journal_mode={}'.format(
|
||||
info['deltachat_core_version'],
|
||||
info['sqlite_version'],
|
||||
info['journal_mode'],
|
||||
)])
|
||||
info = get_core_info()
|
||||
summary = ['Deltachat core={} sqlite={} journal_mode={}'.format(
|
||||
info['deltachat_core_version'],
|
||||
info['sqlite_version'],
|
||||
info['journal_mode'],
|
||||
)]
|
||||
|
||||
cfg = config.option.liveconfig
|
||||
if cfg:
|
||||
@@ -126,57 +129,97 @@ def pytest_report_header(config, startdir):
|
||||
return summary
|
||||
|
||||
|
||||
class SessionLiveConfigFromFile:
|
||||
def __init__(self, fn) -> None:
|
||||
self.fn = fn
|
||||
self.configlist = []
|
||||
for line in open(fn):
|
||||
if line.strip() and not line.strip().startswith('#'):
|
||||
d = {}
|
||||
for part in line.split():
|
||||
name, value = part.split("=")
|
||||
d[name] = value
|
||||
self.configlist.append(d)
|
||||
|
||||
def get(self, index: int):
|
||||
return self.configlist[index]
|
||||
|
||||
def exists(self) -> bool:
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
class SessionLiveConfigFromURL:
|
||||
configlist: List[Dict[str, str]]
|
||||
|
||||
def __init__(self, url: str) -> None:
|
||||
self.configlist = []
|
||||
self.url = url
|
||||
|
||||
def get(self, index: int):
|
||||
try:
|
||||
return self.configlist[index]
|
||||
except IndexError:
|
||||
assert index == len(self.configlist), index
|
||||
res = requests.post(self.url)
|
||||
if res.status_code != 200:
|
||||
pytest.skip("creating newtmpuser failed with code {}: '{}'".format(res.status_code, res.text))
|
||||
d = res.json()
|
||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
||||
self.configlist.append(config)
|
||||
return config
|
||||
|
||||
def exists(self) -> bool:
|
||||
return bool(self.configlist)
|
||||
|
||||
|
||||
@pytest.fixture(scope="session")
|
||||
def session_liveconfig(request):
|
||||
liveconfig_opt = request.config.option.liveconfig
|
||||
if liveconfig_opt:
|
||||
if liveconfig_opt.startswith("http"):
|
||||
return SessionLiveConfigFromURL(liveconfig_opt)
|
||||
def testprocess(request):
|
||||
return TestProcess(pytestconfig=request.config)
|
||||
|
||||
|
||||
class TestProcess:
|
||||
""" A pytest session-scoped instance to help with managing "live" account configurations.
|
||||
"""
|
||||
def __init__(self, pytestconfig):
|
||||
self.pytestconfig = pytestconfig
|
||||
self._addr2files = {}
|
||||
self._configlist = []
|
||||
|
||||
def get_liveconfig_producer(self):
|
||||
""" provide live account configs, cached on a per-test-process scope
|
||||
so that test functions can re-use already known live configs.
|
||||
Depending on the --liveconfig option this comes from
|
||||
a HTTP provider or a file with a line specifying each accounts config.
|
||||
"""
|
||||
liveconfig_opt = self.pytestconfig.getoption("--liveconfig")
|
||||
if not liveconfig_opt:
|
||||
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig to provide live accounts")
|
||||
|
||||
if not liveconfig_opt.startswith("http"):
|
||||
for line in open(liveconfig_opt):
|
||||
if line.strip() and not line.strip().startswith('#'):
|
||||
d = {}
|
||||
for part in line.split():
|
||||
name, value = part.split("=")
|
||||
d[name] = value
|
||||
self._configlist.append(d)
|
||||
|
||||
yield from iter(self._configlist)
|
||||
else:
|
||||
return SessionLiveConfigFromFile(liveconfig_opt)
|
||||
MAX_LIVE_CREATED_ACCOUNTS = 10
|
||||
for index in range(MAX_LIVE_CREATED_ACCOUNTS):
|
||||
try:
|
||||
yield self._configlist[index]
|
||||
except IndexError:
|
||||
res = requests.post(liveconfig_opt)
|
||||
if res.status_code != 200:
|
||||
pytest.fail("newtmpuser count={} code={}: '{}'".format(
|
||||
index, res.status_code, res.text))
|
||||
d = res.json()
|
||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
||||
print("newtmpuser {}: addr={}".format(index, config["addr"]))
|
||||
self._configlist.append(config)
|
||||
yield config
|
||||
pytest.fail("more than {} live accounts requested.".format(MAX_LIVE_CREATED_ACCOUNTS))
|
||||
|
||||
def cache_maybe_retrieve_configured_db_files(self, cache_addr, db_target_path):
|
||||
db_target_path = pathlib.Path(db_target_path)
|
||||
assert not db_target_path.exists()
|
||||
|
||||
try:
|
||||
filescache = self._addr2files[cache_addr]
|
||||
except KeyError:
|
||||
print("CACHE FAIL for", cache_addr)
|
||||
return False
|
||||
else:
|
||||
print("CACHE HIT for", cache_addr)
|
||||
targetdir = db_target_path.parent
|
||||
write_dict_to_dir(filescache, targetdir)
|
||||
return True
|
||||
|
||||
def cache_maybe_store_configured_db_files(self, acc):
|
||||
addr = acc.get_config("addr")
|
||||
assert acc.is_configured()
|
||||
# don't overwrite existing entries
|
||||
if addr not in self._addr2files:
|
||||
print("storing cache for", addr)
|
||||
basedir = pathlib.Path(acc.get_blobdir()).parent
|
||||
self._addr2files[addr] = create_dict_from_files_in_path(basedir)
|
||||
return True
|
||||
|
||||
|
||||
def create_dict_from_files_in_path(base):
|
||||
cachedict = {}
|
||||
for path in base.glob("**/*"):
|
||||
if path.is_file():
|
||||
cachedict[path.relative_to(base)] = path.read_bytes()
|
||||
return cachedict
|
||||
|
||||
|
||||
def write_dict_to_dir(dic, target_dir):
|
||||
assert dic
|
||||
for relpath, content in dic.items():
|
||||
path = target_dir.joinpath(relpath)
|
||||
if not path.parent.exists():
|
||||
os.makedirs(path.parent)
|
||||
path.write_bytes(content)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -209,257 +252,349 @@ def data(request):
|
||||
return Data()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acfactory(pytestconfig, tmpdir, request, session_liveconfig, data):
|
||||
class ACSetup:
|
||||
""" accounts setup helper to deal with multiple configure-process
|
||||
and io & imap initialization phases. From tests, use the higher level
|
||||
public ACFactory methods instead of its private helper class.
|
||||
"""
|
||||
CONFIGURING = "CONFIGURING"
|
||||
CONFIGURED = "CONFIGURED"
|
||||
IDLEREADY = "IDLEREADY"
|
||||
|
||||
class AccountMaker:
|
||||
_finalizers: List[Callable[[], None]]
|
||||
_accounts: List[Account]
|
||||
def __init__(self, testprocess, init_time):
|
||||
self._configured_events = Queue()
|
||||
self._account2state = {}
|
||||
self._imap_cleaned = set()
|
||||
self.testprocess = testprocess
|
||||
self.init_time = init_time
|
||||
|
||||
def __init__(self) -> None:
|
||||
self.live_count = 0
|
||||
self.offline_count = 0
|
||||
self._finalizers = []
|
||||
self._accounts = []
|
||||
self.init_time = time.time()
|
||||
self._generated_keys = ["alice", "bob", "charlie",
|
||||
def log(self, *args):
|
||||
print("[acsetup]", "{:.3f}".format(time.time() - self.init_time), *args)
|
||||
|
||||
def add_configured(self, account):
|
||||
""" add an already configured account. """
|
||||
assert account.is_configured()
|
||||
self._account2state[account] = self.CONFIGURED
|
||||
self.log("added already configured account", account, account.get_config("addr"))
|
||||
|
||||
def start_configure(self, account, reconfigure=False):
|
||||
""" add an account and start its configure process. """
|
||||
class PendingTracker:
|
||||
@account_hookimpl
|
||||
def ac_configure_completed(this, success):
|
||||
self._configured_events.put((account, success))
|
||||
|
||||
account.add_account_plugin(PendingTracker(), name="pending_tracker")
|
||||
self._account2state[account] = self.CONFIGURING
|
||||
account.configure(reconfigure=reconfigure)
|
||||
self.log("started configure on", account)
|
||||
|
||||
def wait_one_configured(self, account):
|
||||
""" wait until this account has successfully configured. """
|
||||
if self._account2state[account] == self.CONFIGURING:
|
||||
while 1:
|
||||
acc = self._pop_config_success()
|
||||
if acc == account:
|
||||
break
|
||||
self.init_imap(acc)
|
||||
self.init_logging(acc)
|
||||
acc._evtracker.consume_events()
|
||||
|
||||
def bring_online(self):
|
||||
""" Wait for all accounts to become ready to receive messages.
|
||||
|
||||
This will initialize logging, start IO and the direct_imap attribute
|
||||
for each account which either is CONFIGURED already or which is CONFIGURING
|
||||
and successfully completing the configuration process.
|
||||
"""
|
||||
print("wait_all_configured finds accounts=", self._account2state)
|
||||
for acc, state in self._account2state.items():
|
||||
if state == self.CONFIGURED:
|
||||
self._onconfigure_start_io(acc)
|
||||
self._account2state[acc] = self.IDLEREADY
|
||||
|
||||
while self.CONFIGURING in self._account2state.values():
|
||||
acc = self._pop_config_success()
|
||||
self._onconfigure_start_io(acc)
|
||||
self._account2state[acc] = self.IDLEREADY
|
||||
print("finished, account2state", self._account2state)
|
||||
|
||||
def _pop_config_success(self):
|
||||
acc, success = self._configured_events.get()
|
||||
if not success:
|
||||
pytest.fail("configuring online account failed: {}".format(acc))
|
||||
self._account2state[acc] = self.CONFIGURED
|
||||
return acc
|
||||
|
||||
def _onconfigure_start_io(self, acc):
|
||||
self.init_imap(acc)
|
||||
self.init_logging(acc)
|
||||
acc.start_io()
|
||||
print(acc._logid, "waiting for inbox IDLE to become ready")
|
||||
acc._evtracker.wait_idle_inbox_ready()
|
||||
acc._evtracker.consume_events()
|
||||
acc.log("inbox IDLE ready")
|
||||
|
||||
def init_logging(self, acc):
|
||||
""" idempotent function for initializing logging (will replace existing logger). """
|
||||
logger = FFIEventLogger(acc, logid=acc._logid, init_time=self.init_time)
|
||||
acc.add_account_plugin(logger, name="logger-" + acc._logid)
|
||||
|
||||
def init_imap(self, acc):
|
||||
""" initialize direct_imap and cleanup server state. """
|
||||
from deltachat.direct_imap import DirectImap
|
||||
|
||||
assert acc.is_configured()
|
||||
if not hasattr(acc, "direct_imap"):
|
||||
acc.direct_imap = DirectImap(acc)
|
||||
addr = acc.get_config("addr")
|
||||
if addr not in self._imap_cleaned:
|
||||
imap = acc.direct_imap
|
||||
for folder in imap.list_folders():
|
||||
if folder.lower() == "inbox" or folder.lower() == "deltachat":
|
||||
assert imap.select_folder(folder)
|
||||
imap.delete("1:*", expunge=True)
|
||||
else:
|
||||
imap.conn.folder.delete(folder)
|
||||
acc.log("imap cleaned for addr {}".format(addr))
|
||||
self._imap_cleaned.add(addr)
|
||||
|
||||
|
||||
class ACFactory:
|
||||
_finalizers: List[Callable[[], None]]
|
||||
_accounts: List[Account]
|
||||
|
||||
def __init__(self, request, testprocess, tmpdir, data) -> None:
|
||||
self.init_time = time.time()
|
||||
self.tmpdir = tmpdir
|
||||
self.pytestconfig = request.config
|
||||
self.data = data
|
||||
self.testprocess = testprocess
|
||||
self._liveconfig_producer = testprocess.get_liveconfig_producer()
|
||||
|
||||
self._finalizers = []
|
||||
self._accounts = []
|
||||
self._acsetup = ACSetup(testprocess, self.init_time)
|
||||
self._preconfigured_keys = ["alice", "bob", "charlie",
|
||||
"dom", "elena", "fiona"]
|
||||
self.set_logging_default(False)
|
||||
deltachat.register_global_plugin(direct_imap)
|
||||
self.set_logging_default(False)
|
||||
request.addfinalizer(self.finalize)
|
||||
|
||||
def finalize(self):
|
||||
while self._finalizers:
|
||||
fin = self._finalizers.pop()
|
||||
fin()
|
||||
def log(self, *args):
|
||||
print("[acfactory]", "{:.3f}".format(time.time() - self.init_time), *args)
|
||||
|
||||
while self._accounts:
|
||||
acc = self._accounts.pop()
|
||||
def finalize(self):
|
||||
while self._finalizers:
|
||||
fin = self._finalizers.pop()
|
||||
fin()
|
||||
|
||||
while self._accounts:
|
||||
acc = self._accounts.pop()
|
||||
if acc is not None:
|
||||
imap = getattr(acc, "direct_imap", None)
|
||||
if imap is not None:
|
||||
imap.shutdown()
|
||||
del acc.direct_imap
|
||||
acc.shutdown()
|
||||
acc.disable_logging()
|
||||
deltachat.unregister_global_plugin(direct_imap)
|
||||
|
||||
def make_account(self, path, logid, quiet=False):
|
||||
ac = Account(path, logging=self._logging)
|
||||
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
|
||||
ac._evtracker.set_timeout(30)
|
||||
ac.addr = ac.get_self_contact().addr
|
||||
ac.set_config("displayname", logid)
|
||||
if not quiet:
|
||||
logger = FFIEventLogger(ac)
|
||||
logger.init_time = self.init_time
|
||||
ac.add_account_plugin(logger)
|
||||
self._accounts.append(ac)
|
||||
return ac
|
||||
def get_next_liveconfig(self):
|
||||
""" Base function to get functional online configurations
|
||||
where we can make valid SMTP and IMAP connections with.
|
||||
"""
|
||||
configdict = next(self._liveconfig_producer).copy()
|
||||
if "e2ee_enabled" not in configdict:
|
||||
configdict["e2ee_enabled"] = "1"
|
||||
|
||||
def set_logging_default(self, logging):
|
||||
self._logging = bool(logging)
|
||||
if self.pytestconfig.getoption("--strict-tls"):
|
||||
# Enable strict certificate checks for online accounts
|
||||
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
|
||||
def get_unconfigured_account(self):
|
||||
self.offline_count += 1
|
||||
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
|
||||
return self.make_account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
||||
assert "addr" in configdict and "mail_pw" in configdict
|
||||
return configdict
|
||||
|
||||
def _preconfigure_key(self, account, addr):
|
||||
# Only set a key if we haven't used it yet for another account.
|
||||
if self._generated_keys:
|
||||
keyname = self._generated_keys.pop(0)
|
||||
fname_pub = data.read_path("key/{name}-public.asc".format(name=keyname))
|
||||
fname_sec = data.read_path("key/{name}-secret.asc".format(name=keyname))
|
||||
if fname_pub and fname_sec:
|
||||
account._preconfigure_keypair(addr, fname_pub, fname_sec)
|
||||
return True
|
||||
else:
|
||||
print("WARN: could not use preconfigured keys for {!r}".format(addr))
|
||||
def _get_cached_account(self, addr):
|
||||
if addr in self.testprocess._addr2files:
|
||||
return self._getaccount(addr)
|
||||
|
||||
def get_configured_offline_account(self):
|
||||
ac = self.get_unconfigured_account()
|
||||
def get_unconfigured_account(self):
|
||||
return self._getaccount()
|
||||
|
||||
# do a pseudo-configured account
|
||||
addr = "addr{}@offline.org".format(self.offline_count)
|
||||
ac.set_config("addr", addr)
|
||||
self._preconfigure_key(ac, addr)
|
||||
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
|
||||
ac.set_config("mail_pw", "123")
|
||||
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
|
||||
lib.dc_set_config(ac._dc_context, b"configured", b"1")
|
||||
return ac
|
||||
def _getaccount(self, try_cache_addr=None):
|
||||
logid = "ac{}".format(len(self._accounts) + 1)
|
||||
# we need to use fixed database basename for maybe_cache_* functions to work
|
||||
path = self.tmpdir.mkdir(logid).join("dc.db")
|
||||
if try_cache_addr:
|
||||
self.testprocess.cache_maybe_retrieve_configured_db_files(try_cache_addr, path)
|
||||
ac = Account(path.strpath, logging=self._logging)
|
||||
ac._logid = logid # later instantiated FFIEventLogger needs this
|
||||
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
|
||||
if self.pytestconfig.getoption("--debug-setup"):
|
||||
self._acsetup.init_logging(ac)
|
||||
self._accounts.append(ac)
|
||||
return ac
|
||||
|
||||
def get_online_config(self, pre_generated_key=True, quiet=False):
|
||||
if not session_liveconfig:
|
||||
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig")
|
||||
configdict = session_liveconfig.get(self.live_count)
|
||||
self.live_count += 1
|
||||
if "e2ee_enabled" not in configdict:
|
||||
configdict["e2ee_enabled"] = "1"
|
||||
def set_logging_default(self, logging):
|
||||
self._logging = bool(logging)
|
||||
|
||||
if pytestconfig.getoption("--strict-tls"):
|
||||
# Enable strict certificate checks for online accounts
|
||||
configdict["imap_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
configdict["smtp_certificate_checks"] = str(const.DC_CERTCK_STRICT)
|
||||
def remove_preconfigured_keys(self):
|
||||
self._preconfigured_keys = []
|
||||
|
||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count), quiet=quiet)
|
||||
if pre_generated_key:
|
||||
self._preconfigure_key(ac, configdict['addr'])
|
||||
return ac, dict(configdict)
|
||||
def _preconfigure_key(self, account, addr):
|
||||
# Only set a preconfigured key if we haven't used it yet for another account.
|
||||
try:
|
||||
keyname = self._preconfigured_keys.pop(0)
|
||||
except IndexError:
|
||||
pass
|
||||
else:
|
||||
fname_pub = self.data.read_path("key/{name}-public.asc".format(name=keyname))
|
||||
fname_sec = self.data.read_path("key/{name}-secret.asc".format(name=keyname))
|
||||
if fname_pub and fname_sec:
|
||||
account._preconfigure_keypair(addr, fname_pub, fname_sec)
|
||||
return True
|
||||
else:
|
||||
print("WARN: could not use preconfigured keys for {!r}".format(addr))
|
||||
|
||||
def get_online_configuring_account(self, sentbox=False, move=False,
|
||||
pre_generated_key=True, quiet=False, config={}):
|
||||
ac, configdict = self.get_online_config(
|
||||
pre_generated_key=pre_generated_key, quiet=quiet)
|
||||
configdict.update(config)
|
||||
configdict["mvbox_move"] = str(int(move))
|
||||
configdict["sentbox_watch"] = str(int(sentbox))
|
||||
ac.update_config(configdict)
|
||||
ac._configtracker = ac.configure()
|
||||
return ac
|
||||
def get_pseudo_configured_account(self):
|
||||
# do a pseudo-configured account
|
||||
ac = self.get_unconfigured_account()
|
||||
acname = ac._logid
|
||||
addr = "{}@offline.org".format(acname)
|
||||
ac.update_config(dict(
|
||||
addr=addr, displayname=acname, mail_pw="123",
|
||||
configured_addr=addr, configured_mail_pw="123",
|
||||
configured="1",
|
||||
))
|
||||
self._preconfigure_key(ac, addr)
|
||||
self._acsetup.init_logging(ac)
|
||||
return ac
|
||||
|
||||
def get_one_online_account(self, pre_generated_key=True, move=False):
|
||||
ac1 = self.get_online_configuring_account(
|
||||
pre_generated_key=pre_generated_key, move=move)
|
||||
self.wait_configure_and_start_io([ac1])
|
||||
return ac1
|
||||
|
||||
def get_two_online_accounts(self, move=False, quiet=False):
|
||||
ac1 = self.get_online_configuring_account(move=move, quiet=quiet)
|
||||
ac2 = self.get_online_configuring_account(quiet=quiet)
|
||||
self.wait_configure_and_start_io([ac1, ac2])
|
||||
return ac1, ac2
|
||||
|
||||
def get_many_online_accounts(self, num, move=True):
|
||||
accounts = [self.get_online_configuring_account(move=move, quiet=True)
|
||||
for i in range(num)]
|
||||
self.wait_configure_and_start_io(accounts)
|
||||
for acc in accounts:
|
||||
acc.add_account_plugin(FFIEventLogger(acc))
|
||||
return accounts
|
||||
|
||||
def clone_online_account(self, account, pre_generated_key=True):
|
||||
""" Clones addr, mail_pw, mvbox_move, sentbox_watch and the
|
||||
direct_imap object of an online account. This simulates the user setting
|
||||
up a new device without importing a backup.
|
||||
|
||||
`pre_generated_key` only means that a key from python/tests/data/key is
|
||||
used in order to speed things up.
|
||||
"""
|
||||
self.live_count += 1
|
||||
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||
ac = self.make_account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
||||
if pre_generated_key:
|
||||
self._preconfigure_key(ac, account.get_config("addr"))
|
||||
ac.update_config(dict(
|
||||
addr=account.get_config("addr"),
|
||||
mail_pw=account.get_config("mail_pw"),
|
||||
mvbox_move=account.get_config("mvbox_move"),
|
||||
sentbox_watch=account.get_config("sentbox_watch"),
|
||||
))
|
||||
if hasattr(account, "direct_imap"):
|
||||
# Attach the existing direct_imap. If we did not do this, a new one would be created and
|
||||
# delete existing messages (see dc_account_extra_configure(configure))
|
||||
ac.direct_imap = account.direct_imap
|
||||
ac._configtracker = ac.configure()
|
||||
return ac
|
||||
|
||||
def wait_configure_and_start_io(self, accounts=None):
|
||||
if accounts is None:
|
||||
accounts = self._accounts[:]
|
||||
started_accounts = []
|
||||
for acc in accounts:
|
||||
if acc not in started_accounts:
|
||||
self.wait_configure(acc)
|
||||
acc.set_config("bcc_self", "0")
|
||||
if acc.is_configured():
|
||||
acc.start_io()
|
||||
started_accounts.append(acc)
|
||||
print("{}: {} account was started".format(
|
||||
acc.get_config("displayname"), acc.get_config("addr")))
|
||||
for acc in started_accounts:
|
||||
acc._evtracker.wait_all_initial_fetches()
|
||||
|
||||
def wait_configure(self, acc):
|
||||
if hasattr(acc, "_configtracker"):
|
||||
acc._configtracker.wait_finish()
|
||||
acc._evtracker.consume_events()
|
||||
acc.get_device_chat().mark_noticed()
|
||||
del acc._configtracker
|
||||
|
||||
def run_bot_process(self, module, ffi=True):
|
||||
fn = module.__file__
|
||||
|
||||
bot_ac, bot_cfg = self.get_online_config()
|
||||
|
||||
# Avoid starting ac so we don't interfere with the bot operating on
|
||||
# the same database.
|
||||
self._accounts.remove(bot_ac)
|
||||
|
||||
args = [
|
||||
sys.executable,
|
||||
"-u",
|
||||
fn,
|
||||
"--email", bot_cfg["addr"],
|
||||
"--password", bot_cfg["mail_pw"],
|
||||
bot_ac.db_path,
|
||||
]
|
||||
if ffi:
|
||||
args.insert(-1, "--show-ffi")
|
||||
print("$", " ".join(args))
|
||||
popen = subprocess.Popen(
|
||||
args=args,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, # combine stdout/stderr in one stream
|
||||
bufsize=0, # line buffering
|
||||
close_fds=True, # close all FDs other than 0/1/2
|
||||
universal_newlines=True # give back text
|
||||
def new_online_configuring_account(self, cloned_from=None, cache=False, **kwargs):
|
||||
if cloned_from is None:
|
||||
configdict = self.get_next_liveconfig()
|
||||
else:
|
||||
# XXX we might want to transfer the key to the new account
|
||||
configdict = dict(
|
||||
addr=cloned_from.get_config("addr"),
|
||||
mail_pw=cloned_from.get_config("mail_pw"),
|
||||
)
|
||||
bot = BotProcess(popen, bot_cfg)
|
||||
self._finalizers.append(bot.kill)
|
||||
return bot
|
||||
configdict.update(kwargs)
|
||||
ac = self._get_cached_account(addr=configdict["addr"]) if cache else None
|
||||
if ac is not None:
|
||||
# make sure we consume a preconfig key, as if we had created a fresh account
|
||||
self._preconfigured_keys.pop(0)
|
||||
self._acsetup.add_configured(ac)
|
||||
return ac
|
||||
ac = self.prepare_account_from_liveconfig(configdict)
|
||||
self._acsetup.start_configure(ac)
|
||||
return ac
|
||||
|
||||
def dump_imap_summary(self, logfile):
|
||||
for ac in self._accounts:
|
||||
ac.dump_account_info(logfile=logfile)
|
||||
imap = getattr(ac, "direct_imap", None)
|
||||
if imap is not None:
|
||||
try:
|
||||
imap.idle_done()
|
||||
except Exception:
|
||||
pass
|
||||
imap.dump_imap_structures(tmpdir, logfile=logfile)
|
||||
def prepare_account_from_liveconfig(self, configdict):
|
||||
ac = self.get_unconfigured_account()
|
||||
assert "addr" in configdict and "mail_pw" in configdict, configdict
|
||||
configdict.setdefault("bcc_self", False)
|
||||
configdict.setdefault("mvbox_move", False)
|
||||
configdict.setdefault("sentbox_watch", False)
|
||||
ac.update_config(configdict)
|
||||
self._preconfigure_key(ac, configdict["addr"])
|
||||
return ac
|
||||
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account):
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
def wait_configured(self, account):
|
||||
""" Wait until the specified account has successfully completed configure. """
|
||||
self._acsetup.wait_one_configured(account)
|
||||
|
||||
def introduce_each_other(self, accounts, sending=True):
|
||||
to_wait = []
|
||||
for i, acc in enumerate(accounts):
|
||||
for acc2 in accounts[i + 1:]:
|
||||
chat = self.get_accepted_chat(acc, acc2)
|
||||
if sending:
|
||||
chat.send_text("hi")
|
||||
to_wait.append(acc2)
|
||||
acc2.create_chat(acc).send_text("hi back")
|
||||
to_wait.append(acc)
|
||||
for acc in to_wait:
|
||||
acc._evtracker.wait_next_incoming_message()
|
||||
def bring_accounts_online(self):
|
||||
print("bringing accounts online")
|
||||
self._acsetup.bring_online()
|
||||
print("all accounts online")
|
||||
|
||||
am = AccountMaker()
|
||||
request.addfinalizer(am.finalize)
|
||||
def get_online_accounts(self, num):
|
||||
accounts = [self.new_online_configuring_account(cache=True) for i in range(num)]
|
||||
self.bring_accounts_online()
|
||||
# we cache fully configured and started accounts
|
||||
for acc in accounts:
|
||||
self.testprocess.cache_maybe_store_configured_db_files(acc)
|
||||
return accounts
|
||||
|
||||
def run_bot_process(self, module, ffi=True):
|
||||
fn = module.__file__
|
||||
|
||||
bot_cfg = self.get_next_liveconfig()
|
||||
bot_ac = self.prepare_account_from_liveconfig(bot_cfg)
|
||||
|
||||
# Forget ac as it will be opened by the bot subprocess
|
||||
# but keep something in the list to not confuse account generation
|
||||
self._accounts[self._accounts.index(bot_ac)] = None
|
||||
|
||||
args = [
|
||||
sys.executable,
|
||||
"-u",
|
||||
fn,
|
||||
"--email", bot_cfg["addr"],
|
||||
"--password", bot_cfg["mail_pw"],
|
||||
bot_ac.db_path,
|
||||
]
|
||||
if ffi:
|
||||
args.insert(-1, "--show-ffi")
|
||||
print("$", " ".join(args))
|
||||
popen = subprocess.Popen(
|
||||
args=args,
|
||||
stdin=subprocess.DEVNULL,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.STDOUT, # combine stdout/stderr in one stream
|
||||
bufsize=0, # line buffering
|
||||
close_fds=True, # close all FDs other than 0/1/2
|
||||
universal_newlines=True # give back text
|
||||
)
|
||||
bot = BotProcess(popen, addr=bot_cfg["addr"])
|
||||
self._finalizers.append(bot.kill)
|
||||
return bot
|
||||
|
||||
def dump_imap_summary(self, logfile):
|
||||
for ac in self._accounts:
|
||||
ac.dump_account_info(logfile=logfile)
|
||||
imap = getattr(ac, "direct_imap", None)
|
||||
if imap is not None:
|
||||
imap.dump_imap_structures(self.tmpdir, logfile=logfile)
|
||||
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account):
|
||||
ac2.create_chat(ac1)
|
||||
return ac1.create_chat(ac2)
|
||||
|
||||
def introduce_each_other(self, accounts, sending=True):
|
||||
to_wait = []
|
||||
for i, acc in enumerate(accounts):
|
||||
for acc2 in accounts[i + 1:]:
|
||||
chat = self.get_accepted_chat(acc, acc2)
|
||||
if sending:
|
||||
chat.send_text("hi")
|
||||
to_wait.append(acc2)
|
||||
acc2.create_chat(acc).send_text("hi back")
|
||||
to_wait.append(acc)
|
||||
for acc in to_wait:
|
||||
acc.log("waiting for incoming message")
|
||||
acc._evtracker.wait_next_incoming_message()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def acfactory(request, tmpdir, testprocess, data):
|
||||
am = ACFactory(request=request, tmpdir=tmpdir, testprocess=testprocess, data=data)
|
||||
yield am
|
||||
if hasattr(request.node, "rep_call") and request.node.rep_call.failed:
|
||||
logfile = io.StringIO()
|
||||
am.dump_imap_summary(logfile=logfile)
|
||||
print(logfile.getvalue())
|
||||
# request.node.add_report_section("call", "imap-server-state", s)
|
||||
if testprocess.pytestconfig.getoption("--extra-info"):
|
||||
logfile = io.StringIO()
|
||||
am.dump_imap_summary(logfile=logfile)
|
||||
print(logfile.getvalue())
|
||||
# request.node.add_report_section("call", "imap-server-state", s)
|
||||
|
||||
|
||||
class BotProcess:
|
||||
stdout_queue: queue.Queue
|
||||
|
||||
def __init__(self, popen, bot_cfg) -> None:
|
||||
def __init__(self, popen, addr) -> None:
|
||||
self.popen = popen
|
||||
self.addr = bot_cfg["addr"]
|
||||
self.addr = addr
|
||||
|
||||
# we read stdout as quickly as we can in a thread and make
|
||||
# the (unicode) lines available for readers through a queue.
|
||||
@@ -483,7 +618,7 @@ class BotProcess:
|
||||
def kill(self) -> None:
|
||||
self.popen.kill()
|
||||
|
||||
def wait(self, timeout=30) -> None:
|
||||
def wait(self, timeout=None) -> None:
|
||||
self.popen.wait(timeout=timeout)
|
||||
|
||||
def fnmatch_lines(self, pattern_lines):
|
||||
@@ -492,7 +627,7 @@ class BotProcess:
|
||||
print("+++FNMATCH:", next_pattern)
|
||||
ignored = []
|
||||
while 1:
|
||||
line = self.stdout_queue.get(timeout=15)
|
||||
line = self.stdout_queue.get()
|
||||
if line is None:
|
||||
if ignored:
|
||||
print("BOT stdout terminated after these lines")
|
||||
|
||||
@@ -90,11 +90,11 @@ class ConfigureTracker:
|
||||
if data1 is None or evdata == data1:
|
||||
break
|
||||
|
||||
def wait_finish(self):
|
||||
def wait_finish(self, timeout=None):
|
||||
""" wait until configure is completed.
|
||||
|
||||
Raise Exception if Configure failed
|
||||
"""
|
||||
if not self._configure_events.get():
|
||||
if not self._configure_events.get(timeout=timeout):
|
||||
content = "\n".join(map(str, self._ffi_events))
|
||||
raise ConfigureFailed(content)
|
||||
|
||||
20
python/tests/bench_test_setup.py
Normal file
20
python/tests/bench_test_setup.py
Normal file
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
run with:
|
||||
|
||||
pytest -vv --durations=10 bench_empty.py
|
||||
|
||||
to see timings of test setups.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
BENCH_NUM = 3
|
||||
|
||||
|
||||
class TestEmpty:
|
||||
def test_prepare_setup_measurings(self, acfactory):
|
||||
acfactory.get_online_accounts(BENCH_NUM)
|
||||
|
||||
@pytest.mark.parametrize("num", range(0, BENCH_NUM + 1))
|
||||
def test_setup_online_accounts(self, acfactory, num):
|
||||
acfactory.get_online_accounts(num)
|
||||
@@ -17,7 +17,7 @@ def test_db_busy_error(acfactory, tmpdir):
|
||||
print("%3.2f %s" % (time.time() - starttime, string))
|
||||
|
||||
# make a number of accounts
|
||||
accounts = acfactory.get_many_online_accounts(3, quiet=True)
|
||||
accounts = acfactory.get_many_online_accounts(3)
|
||||
log("created %s accounts" % len(accounts))
|
||||
|
||||
# put a bigfile into each account
|
||||
|
||||
481
python/tests/test_0_complex_or_slow.py
Normal file
481
python/tests/test_0_complex_or_slow.py
Normal file
@@ -0,0 +1,481 @@
|
||||
import sys
|
||||
import pytest
|
||||
|
||||
|
||||
class TestGroupStressTests:
|
||||
def test_group_many_members_add_leave_remove(self, acfactory, lp):
|
||||
accounts = acfactory.get_online_accounts(5)
|
||||
acfactory.introduce_each_other(accounts)
|
||||
ac1, ac5 = accounts.pop(), accounts.pop()
|
||||
|
||||
lp.sec("ac1: creating group chat with 3 other members")
|
||||
chat = ac1.create_group_chat("title1", contacts=accounts)
|
||||
|
||||
lp.sec("ac1: send message to new group chat")
|
||||
msg1 = chat.send_text("hello")
|
||||
assert msg1.is_encrypted()
|
||||
gossiped_timestamp = chat.get_summary()["gossiped_timestamp"]
|
||||
assert gossiped_timestamp > 0
|
||||
|
||||
assert chat.num_contacts() == 3 + 1
|
||||
|
||||
lp.sec("ac2: checking that the chat arrived correctly")
|
||||
ac2 = accounts[0]
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "hello"
|
||||
print("chat is", msg2.chat)
|
||||
assert msg2.chat.num_contacts() == 4
|
||||
|
||||
lp.sec("ac3: checking that 'ac4' is a known contact")
|
||||
ac3 = accounts[1]
|
||||
msg3 = ac3._evtracker.wait_next_incoming_message()
|
||||
assert msg3.text == "hello"
|
||||
ac3_contacts = ac3.get_contacts()
|
||||
assert len(ac3_contacts) == 4
|
||||
ac4_contacts = ac3.get_contacts(query=accounts[2].get_config("addr"))
|
||||
assert len(ac4_contacts) == 1
|
||||
|
||||
lp.sec("ac2: removing one contact")
|
||||
to_remove = ac2.create_contact(accounts[-1])
|
||||
msg2.chat.remove_contact(to_remove)
|
||||
|
||||
lp.sec("ac1: receiving system message about contact removal")
|
||||
sysmsg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert to_remove.addr in sysmsg.text
|
||||
assert sysmsg.chat.num_contacts() == 3
|
||||
|
||||
# Receiving message about removed contact does not reset gossip
|
||||
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
|
||||
|
||||
lp.sec("ac1: sending another message to the chat")
|
||||
chat.send_text("hello2")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello2"
|
||||
assert chat.get_summary()["gossiped_timestamp"] == gossiped_timestamp
|
||||
|
||||
lp.sec("ac1: adding fifth member to the chat")
|
||||
chat.add_contact(ac5)
|
||||
# Adding contact to chat resets gossiped_timestamp
|
||||
assert chat.get_summary()["gossiped_timestamp"] >= gossiped_timestamp
|
||||
|
||||
lp.sec("ac2: receiving system message about contact addition")
|
||||
sysmsg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert ac5.get_config("configured_addr") in sysmsg.text
|
||||
assert sysmsg.chat.num_contacts() == 4
|
||||
|
||||
lp.sec("ac5: waiting for message about addition to the chat")
|
||||
sysmsg = ac5._evtracker.wait_next_incoming_message()
|
||||
msg = sysmsg.chat.send_text("hello!")
|
||||
# Message should be encrypted because keys of other members are gossiped
|
||||
assert msg.is_encrypted()
|
||||
|
||||
def test_synchronize_member_list_on_group_rejoin(self, acfactory, lp):
|
||||
"""
|
||||
Test that user recreates group member list when it joins the group again.
|
||||
ac1 creates a group with two other accounts: ac2 and ac3
|
||||
Then it removes ac2, removes ac3 and adds ac2 back.
|
||||
ac2 did not see that ac3 is removed, so it should rebuild member list from scratch.
|
||||
"""
|
||||
lp.sec("setting up accounts, accepted with each other")
|
||||
accounts = acfactory.get_online_accounts(3)
|
||||
acfactory.introduce_each_other(accounts)
|
||||
ac1, ac2, ac3 = accounts
|
||||
|
||||
lp.sec("ac1: creating group chat with 2 other members")
|
||||
chat = ac1.create_group_chat("title1", contacts=[ac2, ac3])
|
||||
assert not chat.is_promoted()
|
||||
|
||||
lp.sec("ac1: send message to new group chat")
|
||||
msg = chat.send_text("hello")
|
||||
assert chat.is_promoted() and msg.is_encrypted()
|
||||
|
||||
assert chat.num_contacts() == 3
|
||||
|
||||
lp.sec("checking that the chat arrived correctly")
|
||||
for ac in accounts[1:]:
|
||||
msg = ac._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
print("chat is", msg.chat)
|
||||
assert msg.chat.num_contacts() == 3
|
||||
|
||||
lp.sec("ac1: removing ac2")
|
||||
chat.remove_contact(ac2)
|
||||
|
||||
lp.sec("ac2: wait for a message about removal from the chat")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("ac1: removing ac3")
|
||||
chat.remove_contact(ac3)
|
||||
|
||||
lp.sec("ac1: adding ac2 back")
|
||||
# Group is promoted, message is sent automatically
|
||||
assert chat.is_promoted()
|
||||
chat.add_contact(ac2)
|
||||
|
||||
lp.sec("ac2: check that ac3 is removed")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
|
||||
assert chat.num_contacts() == 2
|
||||
assert msg.chat.num_contacts() == 2
|
||||
acfactory.dump_imap_summary(sys.stdout)
|
||||
|
||||
|
||||
def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
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)
|
||||
assert chat2.id >= 10
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
lp.sec("ac2: read member added message")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
assert "added" in msg.text.lower()
|
||||
|
||||
lp.sec("ac1: send message")
|
||||
msg_out = chat1.send_text("hello")
|
||||
assert msg_out.is_encrypted()
|
||||
|
||||
lp.sec("ac2: read message and check it's verified chat")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
assert msg.chat.is_protected()
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac2: send message and let ac1 read it")
|
||||
chat2.send_text("world")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "world"
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac1: create QR code and let ac3 scan it, starting the securejoin")
|
||||
qr = ac1.get_setup_contact_qr()
|
||||
|
||||
lp.sec("ac3: start QR-code based setup contact protocol")
|
||||
ch = ac3.qr_setup_contact(qr)
|
||||
assert ch.id >= 10
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
lp.sec("ac1: add ac3 to verified group")
|
||||
chat1.add_contact(ac3)
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.is_encrypted()
|
||||
assert msg.is_system_message()
|
||||
assert not msg.error
|
||||
|
||||
lp.sec("ac2: send message and let ac3 read it")
|
||||
chat2.send_text("hi")
|
||||
# Skip system message about added member
|
||||
ac3._evtracker.wait_next_incoming_message()
|
||||
msg = ac3._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hi"
|
||||
assert msg.is_encrypted()
|
||||
|
||||
|
||||
@pytest.mark.parametrize("mvbox_move", [False, True])
|
||||
def test_fetch_existing(acfactory, lp, mvbox_move):
|
||||
"""Delta Chat reads the recipients from old emails sent by the user and adds them as contacts.
|
||||
This way, we can already offer them some email addresses they can write to.
|
||||
|
||||
Also, the newest existing emails from each folder are fetched during onboarding.
|
||||
|
||||
Additionally tests that bcc_self messages moved to the mvbox/sentbox are marked as read."""
|
||||
|
||||
def assert_folders_configured(ac):
|
||||
"""There was a bug that scan_folders() set the configured folders to None under some circumstances.
|
||||
So, check that they are still configured:"""
|
||||
assert ac.get_config("configured_sentbox_folder") == "Sent"
|
||||
if mvbox_move:
|
||||
assert ac.get_config("configured_mvbox_folder")
|
||||
|
||||
ac1 = acfactory.new_online_configuring_account(mvbox_move=mvbox_move)
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
acfactory.wait_configured(ac1)
|
||||
ac1.direct_imap.create_folder("Sent")
|
||||
ac1.set_config("sentbox_watch", "1")
|
||||
|
||||
# We need to reconfigure to find the new "Sent" folder.
|
||||
# `scan_folders()`, which runs automatically shortly after `start_io()` is invoked,
|
||||
# would also find the "Sent" folder, but it would be too late:
|
||||
# The sentbox thread, started by `start_io()`, would have seen that there is no
|
||||
# ConfiguredSentboxFolder and do nothing.
|
||||
acfactory._acsetup.start_configure(ac1, reconfigure=True)
|
||||
acfactory.bring_accounts_online()
|
||||
assert_folders_configured(ac1)
|
||||
|
||||
assert ac1.direct_imap.select_config_folder("mvbox" if mvbox_move else "inbox")
|
||||
with ac1.direct_imap.idle() as idle1:
|
||||
lp.sec("send out message with bcc to ourselves")
|
||||
ac1.set_config("bcc_self", "1")
|
||||
chat = acfactory.get_accepted_chat(ac1, ac2)
|
||||
chat.send_text("message text")
|
||||
assert_folders_configured(ac1)
|
||||
|
||||
lp.sec("wait until the bcc_self message arrives in correct folder and is marked seen")
|
||||
assert idle1.wait_for_seen()
|
||||
assert_folders_configured(ac1)
|
||||
|
||||
lp.sec("create a cloned ac1 and fetch contact history during configure")
|
||||
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
ac1_clone.set_config("fetch_existing_msgs", "1")
|
||||
acfactory.wait_configured(ac1_clone)
|
||||
ac1_clone.start_io()
|
||||
assert_folders_configured(ac1_clone)
|
||||
|
||||
lp.sec("check that ac2 contact was fetchted during configure")
|
||||
ac1_clone._evtracker.get_matching("DC_EVENT_CONTACTS_CHANGED")
|
||||
ac2_addr = ac2.get_config("addr")
|
||||
assert any(c.addr == ac2_addr for c in ac1_clone.get_contacts())
|
||||
assert_folders_configured(ac1_clone)
|
||||
|
||||
lp.sec("check that messages changed events arrive for the correct message")
|
||||
msg = ac1_clone._evtracker.wait_next_messages_changed()
|
||||
assert msg.text == "message text"
|
||||
assert_folders_configured(ac1)
|
||||
assert_folders_configured(ac1_clone)
|
||||
|
||||
|
||||
def test_fetch_existing_msgs_group_and_single(acfactory, lp):
|
||||
"""There was a bug concerning fetch-existing-msgs:
|
||||
|
||||
A sent a message to you, adding you to a group. This created a contact request.
|
||||
You wrote a message to A, creating a chat.
|
||||
...but the group stayed blocked.
|
||||
So, after fetch-existing-msgs you have one contact request and one chat with the same person.
|
||||
|
||||
See https://github.com/deltachat/deltachat-core-rust/issues/2097"""
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
lp.sec("receive a message")
|
||||
ac2.create_group_chat("group name", contacts=[ac1]).send_text("incoming, unencrypted group message")
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
|
||||
lp.sec("send out message with bcc to ourselves")
|
||||
with ac1.direct_imap.idle() as idle1:
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1_ac2_chat = ac1.create_chat(ac2)
|
||||
ac1_ac2_chat.send_text("outgoing, encrypted direct message, creating a chat")
|
||||
# wait until the bcc_self message arrives
|
||||
assert idle1.wait_for_seen()
|
||||
|
||||
lp.sec("Clone online account and let it fetch the existing messages")
|
||||
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
ac1_clone.set_config("fetch_existing_msgs", "1")
|
||||
acfactory.wait_configured(ac1_clone)
|
||||
|
||||
ac1_clone.start_io()
|
||||
ac1_clone._evtracker.wait_idle_inbox_ready()
|
||||
|
||||
chats = ac1_clone.get_chats()
|
||||
assert len(chats) == 4 # two newly created chats + self-chat + device-chat
|
||||
group_chat = [c for c in chats if c.get_name() == "group name"][0]
|
||||
assert group_chat.is_group()
|
||||
private_chat, = [c for c in chats if c.get_name() == ac1_ac2_chat.get_name()]
|
||||
assert not private_chat.is_group()
|
||||
|
||||
group_messages = group_chat.get_messages()
|
||||
assert len(group_messages) == 1
|
||||
assert group_messages[0].text == "incoming, unencrypted group message"
|
||||
private_messages = private_chat.get_messages()
|
||||
# We can't decrypt the message in this chat, so the chat is empty:
|
||||
assert len(private_messages) == 0
|
||||
|
||||
|
||||
def test_undecipherable_group(acfactory, lp):
|
||||
"""Test how group messages that cannot be decrypted are
|
||||
handled.
|
||||
|
||||
Group name is encrypted and plaintext subject is set to "..." in
|
||||
this case, so we should assign the messages to existing chat
|
||||
instead of creating a new one. Since there is no existing group
|
||||
chat, the messages should be assigned to 1-1 chat with the sender
|
||||
of the message.
|
||||
"""
|
||||
|
||||
lp.sec("creating and configuring three accounts")
|
||||
ac1, ac2, ac3 = acfactory.get_online_accounts(3)
|
||||
|
||||
acfactory.introduce_each_other([ac1, ac2, ac3])
|
||||
|
||||
lp.sec("ac3 reinstalls DC and generates a new key")
|
||||
ac3.stop_io()
|
||||
acfactory.remove_preconfigured_keys()
|
||||
ac4 = acfactory.new_online_configuring_account(cloned_from=ac3)
|
||||
acfactory.wait_configured(ac4)
|
||||
# Create contacts to make sure incoming messages are not treated as contact requests
|
||||
chat41 = ac4.create_chat(ac1)
|
||||
chat42 = ac4.create_chat(ac2)
|
||||
ac4.start_io()
|
||||
ac4._evtracker.wait_idle_inbox_ready()
|
||||
|
||||
lp.sec("ac1: creating group chat with 2 other members")
|
||||
chat = ac1.create_group_chat("title", contacts=[ac2, ac3])
|
||||
|
||||
lp.sec("ac1: send message to new group chat")
|
||||
msg = chat.send_text("hello")
|
||||
|
||||
lp.sec("ac2: checking that the chat arrived correctly")
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "hello"
|
||||
assert msg.is_encrypted(), "Message is not encrypted"
|
||||
|
||||
# ac4 cannot decrypt the message.
|
||||
# Error message should be assigned to the chat with ac1.
|
||||
lp.sec("ac4: checking that message is assigned to the sender chat")
|
||||
error_msg = ac4._evtracker.wait_next_incoming_message()
|
||||
assert error_msg.error # There is an error decrypting the message
|
||||
assert error_msg.chat == chat41
|
||||
|
||||
lp.sec("ac2: sending a reply to the chat")
|
||||
msg.chat.send_text("reply")
|
||||
reply = ac1._evtracker.wait_next_incoming_message()
|
||||
assert reply.text == "reply"
|
||||
assert reply.is_encrypted(), "Reply is not encrypted"
|
||||
|
||||
lp.sec("ac4: checking that reply is assigned to ac2 chat")
|
||||
error_reply = ac4._evtracker.wait_next_incoming_message()
|
||||
assert error_reply.error # There is an error decrypting the message
|
||||
assert error_reply.chat == chat42
|
||||
|
||||
# Test that ac4 replies to error messages don't appear in the
|
||||
# group chat on ac1 and ac2.
|
||||
lp.sec("ac4: replying to ac1 and ac2")
|
||||
|
||||
# Otherwise reply becomes a contact request.
|
||||
chat41.send_text("I can't decrypt your message, ac1!")
|
||||
chat42.send_text("I can't decrypt your message, ac2!")
|
||||
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.error is None
|
||||
assert msg.text == "I can't decrypt your message, ac1!"
|
||||
assert msg.is_encrypted(), "Message is not encrypted"
|
||||
assert msg.chat == ac1.create_chat(ac3)
|
||||
|
||||
msg = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg.error is None
|
||||
assert msg.text == "I can't decrypt your message, ac2!"
|
||||
assert msg.is_encrypted(), "Message is not encrypted"
|
||||
assert msg.chat == ac2.create_chat(ac4)
|
||||
|
||||
|
||||
def test_ephemeral_timer(acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
lp.sec("ac1: create chat with ac2")
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
chat2 = ac2.create_chat(ac1)
|
||||
|
||||
lp.sec("ac1: set ephemeral timer to 60")
|
||||
chat1.set_ephemeral_timer(60)
|
||||
|
||||
lp.sec("ac1: check that ephemeral timer is set for chat")
|
||||
assert chat1.get_ephemeral_timer() == 60
|
||||
chat1_summary = chat1.get_summary()
|
||||
assert chat1_summary["ephemeral_timer"] == {'Enabled': {'duration': 60}}
|
||||
|
||||
lp.sec("ac2: receive system message about ephemeral timer modification")
|
||||
ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
system_message1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert chat2.get_ephemeral_timer() == 60
|
||||
assert system_message1.is_system_message()
|
||||
|
||||
# Disabled until markers are implemented
|
||||
# assert "Ephemeral timer: 60\n" in system_message1.get_message_info()
|
||||
|
||||
lp.sec("ac2: send message to ac1")
|
||||
sent_message = chat2.send_text("message")
|
||||
assert sent_message.ephemeral_timer == 60
|
||||
assert "Ephemeral timer: 60\n" in sent_message.get_message_info()
|
||||
|
||||
# Timer is started immediately for sent messages
|
||||
assert sent_message.ephemeral_timestamp is not None
|
||||
assert "Expires: " in sent_message.get_message_info()
|
||||
|
||||
lp.sec("ac1: waiting for message from ac2")
|
||||
text_message = ac1._evtracker.wait_next_incoming_message()
|
||||
assert text_message.text == "message"
|
||||
assert text_message.ephemeral_timer == 60
|
||||
assert "Ephemeral timer: 60\n" in text_message.get_message_info()
|
||||
|
||||
# Timer should not start until message is displayed
|
||||
assert text_message.ephemeral_timestamp is None
|
||||
assert "Expires: " not in text_message.get_message_info()
|
||||
text_message.mark_seen()
|
||||
text_message = ac1.get_message_by_id(text_message.id)
|
||||
assert text_message.ephemeral_timestamp is not None
|
||||
assert "Expires: " in text_message.get_message_info()
|
||||
|
||||
lp.sec("ac2: set ephemeral timer to 0")
|
||||
chat2.set_ephemeral_timer(0)
|
||||
ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
|
||||
lp.sec("ac1: receive system message about ephemeral timer modification")
|
||||
ac1._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
system_message2 = ac1._evtracker.wait_next_incoming_message()
|
||||
assert system_message2.ephemeral_timer is None
|
||||
assert "Ephemeral timer: " not in system_message2.get_message_info()
|
||||
assert chat1.get_ephemeral_timer() == 0
|
||||
|
||||
|
||||
def test_multidevice_sync_seen(acfactory, lp):
|
||||
"""Test that message marked as seen on one device is marked as seen on another."""
|
||||
ac1 = acfactory.new_online_configuring_account()
|
||||
ac2 = acfactory.new_online_configuring_account()
|
||||
ac1_clone = acfactory.new_online_configuring_account(cloned_from=ac1)
|
||||
acfactory.bring_accounts_online()
|
||||
|
||||
ac1.set_config("bcc_self", "1")
|
||||
ac1_clone.set_config("bcc_self", "1")
|
||||
|
||||
ac1_chat = ac1.create_chat(ac2)
|
||||
ac1_clone_chat = ac1_clone.create_chat(ac2)
|
||||
ac2_chat = ac2.create_chat(ac1)
|
||||
|
||||
lp.sec("Send a message from ac2 to ac1 and check that it's 'fresh'")
|
||||
ac2_chat.send_text("Hi")
|
||||
ac1_message = ac1._evtracker.wait_next_incoming_message()
|
||||
ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
|
||||
assert ac1_chat.count_fresh_messages() == 1
|
||||
assert ac1_clone_chat.count_fresh_messages() == 1
|
||||
assert ac1_message.is_in_fresh
|
||||
assert ac1_clone_message.is_in_fresh
|
||||
|
||||
lp.sec("ac1 marks message as seen on the first device")
|
||||
ac1.mark_seen_messages([ac1_message])
|
||||
assert ac1_message.is_in_seen
|
||||
|
||||
lp.sec("ac1 clone detects that message is marked as seen")
|
||||
ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
assert ev.data1 == ac1_clone_chat.id
|
||||
assert ac1_clone_message.is_in_seen
|
||||
|
||||
lp.sec("Send an ephemeral message from ac2 to ac1")
|
||||
ac2_chat.set_ephemeral_timer(60)
|
||||
ac1._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
ac1._evtracker.wait_next_incoming_message()
|
||||
ac1_clone._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")
|
||||
ac1_clone._evtracker.wait_next_incoming_message()
|
||||
|
||||
ac2_chat.send_text("Foobar")
|
||||
ac1_message = ac1._evtracker.wait_next_incoming_message()
|
||||
ac1_clone_message = ac1_clone._evtracker.wait_next_incoming_message()
|
||||
assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
|
||||
assert "Expires: " not in ac1_clone_message.get_message_info()
|
||||
assert "Ephemeral timer: 60\n" in ac1_message.get_message_info()
|
||||
assert "Expires: " not in ac1_clone_message.get_message_info()
|
||||
|
||||
ac1.mark_seen_messages([ac1_message])
|
||||
assert ac1_message.is_in_seen
|
||||
assert "Expires: " in ac1_message.get_message_info()
|
||||
ev = ac1_clone._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
|
||||
assert ev.data1 == ac1_clone_chat.id
|
||||
assert ac1_clone_message.is_in_seen
|
||||
# Test that the timer is started on the second device after synchronizing the seen status.
|
||||
assert "Expires: " in ac1_clone_message.get_message_info()
|
||||
1976
python/tests/test_1_online.py
Normal file
1976
python/tests/test_1_online.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -33,7 +33,7 @@ def wait_msgs_changed(account, msgs_list):
|
||||
|
||||
class TestOnlineInCreation:
|
||||
def test_increation_not_blobdir(self, tmpdir, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
lp.sec("Creating in-creation file outside of blobdir")
|
||||
@@ -43,7 +43,7 @@ class TestOnlineInCreation:
|
||||
chat.prepare_message_file(src.strpath)
|
||||
|
||||
def test_no_increation_copies_to_blobdir(self, tmpdir, acfactory, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
chat = ac1.create_chat(ac2)
|
||||
|
||||
lp.sec("Creating file outside of blobdir")
|
||||
@@ -56,7 +56,7 @@ class TestOnlineInCreation:
|
||||
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
|
||||
|
||||
def test_forward_increation(self, acfactory, data, lp):
|
||||
ac1, ac2 = acfactory.get_two_online_accounts()
|
||||
ac1, ac2 = acfactory.get_online_accounts(2)
|
||||
|
||||
chat = ac1.create_chat(ac2)
|
||||
wait_msgs_changed(ac1, [(0, 0)]) # why no chat id?
|
||||
634
python/tests/test_3_offline.py
Normal file
634
python/tests/test_3_offline.py
Normal file
@@ -0,0 +1,634 @@
|
||||
from __future__ import print_function
|
||||
import pytest
|
||||
import os
|
||||
import time
|
||||
from deltachat import const, Account
|
||||
from deltachat.message import Message
|
||||
from deltachat.hookspec import account_hookimpl
|
||||
from deltachat.capi import ffi, lib
|
||||
from deltachat.cutil import iter_array
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
|
||||
@pytest.mark.parametrize("msgtext,res", [
|
||||
("Member Me (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by tmp2@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member With space (tmp1@x.org) removed by me",
|
||||
("removed", "tmp1@x.org", "me")),
|
||||
("Group left by some one (tmp1@x.org).",
|
||||
("removed", "tmp1@x.org", "tmp1@x.org")),
|
||||
("Group left by tmp1@x.org.",
|
||||
("removed", "tmp1@x.org", "tmp1@x.org")),
|
||||
("Member tmp1@x.org added by tmp2@x.org.", ("added", "tmp1@x.org", "tmp2@x.org")),
|
||||
("Member nothing bla bla", None),
|
||||
("Another unknown system message", None),
|
||||
])
|
||||
def test_parse_system_add_remove(msgtext, res):
|
||||
from deltachat.message import parse_system_add_remove
|
||||
|
||||
out = parse_system_add_remove(msgtext)
|
||||
assert out == res
|
||||
|
||||
|
||||
class TestOfflineAccountBasic:
|
||||
def test_wrong_db(self, tmpdir):
|
||||
p = tmpdir.join("hello.db")
|
||||
p.write("123")
|
||||
account = Account(p.strpath)
|
||||
assert not account.is_open()
|
||||
|
||||
def test_os_name(self, tmpdir):
|
||||
p = tmpdir.join("hello.db")
|
||||
# we can't easily test if os_name is used in X-Mailer
|
||||
# outgoing messages without a full Online test
|
||||
# but we at least check Account accepts the arg
|
||||
ac1 = Account(p.strpath, 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)
|
||||
|
||||
def test_getinfo(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
d = ac1.get_info()
|
||||
assert d["arch"]
|
||||
assert d["number_of_chats"] == "0"
|
||||
assert d["bcc_self"] == "0"
|
||||
|
||||
def test_is_not_configured(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
assert not ac1.is_configured()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.check_is_configured()
|
||||
|
||||
def test_wrong_config_keys(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
with pytest.raises(KeyError):
|
||||
ac1.set_config("lqkwje", "value")
|
||||
with pytest.raises(KeyError):
|
||||
ac1.get_config("lqkwje")
|
||||
|
||||
def test_set_config_int_conversion(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
ac1.set_config("mvbox_move", False)
|
||||
assert ac1.get_config("mvbox_move") == "0"
|
||||
ac1.set_config("mvbox_move", True)
|
||||
assert ac1.get_config("mvbox_move") == "1"
|
||||
ac1.set_config("mvbox_move", 0)
|
||||
assert ac1.get_config("mvbox_move") == "0"
|
||||
ac1.set_config("mvbox_move", 1)
|
||||
assert ac1.get_config("mvbox_move") == "1"
|
||||
|
||||
def test_update_config(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
ac1.update_config(dict(mvbox_move=False))
|
||||
assert ac1.get_config("mvbox_move") == "0"
|
||||
|
||||
def test_has_savemime(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
assert "save_mime_headers" in ac1.get_config("sys.config_keys").split()
|
||||
|
||||
def test_has_bccself(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
assert "bcc_self" in ac1.get_config("sys.config_keys").split()
|
||||
assert ac1.get_config("bcc_self") == "0"
|
||||
|
||||
def test_selfcontact_if_unconfigured(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
assert not ac1.get_self_contact().addr
|
||||
|
||||
def test_selfcontact_configured(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
me = ac1.get_self_contact()
|
||||
assert me.display_name
|
||||
assert me.addr
|
||||
|
||||
def test_get_config_fails(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
with pytest.raises(KeyError):
|
||||
ac1.get_config("123123")
|
||||
|
||||
def test_empty_group_bcc_self_enabled(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac1.set_config("bcc_self", "1")
|
||||
chat = ac1.create_group_chat(name="group1")
|
||||
msg = chat.send_text("msg1")
|
||||
assert msg in chat.get_messages()
|
||||
|
||||
def test_empty_group_bcc_self_disabled(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac1.set_config("bcc_self", "0")
|
||||
chat = ac1.create_group_chat(name="group1")
|
||||
msg = chat.send_text("msg1")
|
||||
assert msg in chat.get_messages()
|
||||
|
||||
|
||||
class TestOfflineContact:
|
||||
def test_contact_attr(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact2 = ac1.create_contact("some1@example.org", name="some1")
|
||||
str(contact1)
|
||||
repr(contact1)
|
||||
assert contact1 == contact2
|
||||
assert contact1.id
|
||||
assert contact1.addr == "some1@example.org"
|
||||
assert contact1.display_name == "some1"
|
||||
assert not contact1.is_blocked()
|
||||
assert not contact1.is_verified()
|
||||
|
||||
def test_get_blocked(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact2 = ac1.create_contact("some2@example.org", name="some2")
|
||||
ac1.create_contact("some3@example.org", name="some3")
|
||||
assert ac1.get_blocked_contacts() == []
|
||||
contact1.block()
|
||||
assert ac1.get_blocked_contacts() == [contact1]
|
||||
contact2.block()
|
||||
blocked = ac1.get_blocked_contacts()
|
||||
assert len(blocked) == 2 and contact1 in blocked and contact2 in blocked
|
||||
contact2.unblock()
|
||||
assert ac1.get_blocked_contacts() == [contact1]
|
||||
|
||||
def test_create_self_contact(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact(ac1.get_config("addr"))
|
||||
assert contact1.id == 1
|
||||
|
||||
def test_get_contacts_and_delete(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contacts = ac1.get_contacts()
|
||||
assert len(contacts) == 1
|
||||
assert contact1 in contacts
|
||||
|
||||
assert not ac1.get_contacts(query="some2")
|
||||
assert ac1.get_contacts(query="some1")
|
||||
assert not ac1.get_contacts(only_verified=True)
|
||||
assert len(ac1.get_contacts(with_self=True)) == 2
|
||||
|
||||
assert ac1.delete_contact(contact1)
|
||||
assert contact1 not in ac1.get_contacts()
|
||||
|
||||
def test_get_contacts_and_delete_fails(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.com", name="some1")
|
||||
msg = contact1.create_chat().send_text("one message")
|
||||
assert not ac1.delete_contact(contact1)
|
||||
assert not msg.filemime
|
||||
|
||||
def test_create_chat_flexibility(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
chat1 = ac1.create_chat(ac2)
|
||||
chat2 = ac1.create_chat(ac2.get_self_contact().addr)
|
||||
assert chat1 == chat2
|
||||
ac3 = acfactory.get_unconfigured_account()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.create_chat(ac3)
|
||||
|
||||
def test_contact_rename(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact = ac1.create_contact("some1@example.com", name="some1")
|
||||
chat = ac1.create_chat(contact)
|
||||
assert chat.get_name() == "some1"
|
||||
ac1.create_contact("some1@example.com", name="renamed")
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_CHAT_MODIFIED")
|
||||
assert ev.data1 == chat.id
|
||||
assert chat.get_name() == "renamed"
|
||||
|
||||
|
||||
class TestOfflineChat:
|
||||
@pytest.fixture
|
||||
def ac1(self, acfactory):
|
||||
return acfactory.get_pseudo_configured_account()
|
||||
|
||||
@pytest.fixture
|
||||
def chat1(self, ac1):
|
||||
return ac1.create_contact("some1@example.org", name="some1").create_chat()
|
||||
|
||||
def test_display(self, chat1):
|
||||
str(chat1)
|
||||
repr(chat1)
|
||||
|
||||
def test_is_group(self, chat1):
|
||||
assert not chat1.is_group()
|
||||
|
||||
def test_chat_by_id(self, chat1):
|
||||
chat2 = chat1.account.get_chat_by_id(chat1.id)
|
||||
assert chat2 == chat1
|
||||
with pytest.raises(ValueError):
|
||||
chat1.account.get_chat_by_id(123123)
|
||||
|
||||
def test_chat_idempotent(self, chat1, ac1):
|
||||
contact1 = chat1.get_contacts()[0]
|
||||
chat2 = contact1.create_chat()
|
||||
assert chat2.id == chat1.id
|
||||
assert chat2.get_name() == chat1.get_name()
|
||||
assert chat1 == chat2
|
||||
assert not (chat1 != chat2)
|
||||
|
||||
for ichat in ac1.get_chats():
|
||||
if ichat.id == chat1.id:
|
||||
break
|
||||
else:
|
||||
pytest.fail("could not find chat")
|
||||
|
||||
def test_group_chat_add_second_account(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
with pytest.raises(ValueError):
|
||||
chat.add_contact(ac2.get_self_contact())
|
||||
contact = chat.add_contact(ac2)
|
||||
assert contact.addr == ac2.get_config("addr")
|
||||
assert contact.name == ac2.get_config("displayname")
|
||||
assert contact.account == ac1
|
||||
chat.remove_contact(ac2)
|
||||
|
||||
def test_group_chat_creation(self, ac1):
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact2 = ac1.create_contact("some2@example.org", name="some2")
|
||||
chat = ac1.create_group_chat(name="title1", contacts=[contact1, contact2])
|
||||
assert chat.get_name() == "title1"
|
||||
assert contact1 in chat.get_contacts()
|
||||
assert contact2 in chat.get_contacts()
|
||||
assert not chat.is_promoted()
|
||||
chat.set_name("title2")
|
||||
assert chat.get_name() == "title2"
|
||||
|
||||
d = chat.get_summary()
|
||||
print(d)
|
||||
assert d["id"] == chat.id
|
||||
assert d["type"] == chat.get_type()
|
||||
assert d["name"] == chat.get_name()
|
||||
assert d["archived"] == chat.is_archived()
|
||||
# assert d["param"] == chat.param
|
||||
assert d["color"] == chat.get_color()
|
||||
assert d["profile_image"] == "" if chat.get_profile_image() is None else chat.get_profile_image()
|
||||
assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft()
|
||||
|
||||
def test_group_chat_creation_with_translation(self, ac1):
|
||||
ac1.set_stock_translation(const.DC_STR_MSGGRPNAME, "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._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(500, "xyz %1$s")
|
||||
ac1._evtracker.get_matching("DC_EVENT_WARNING")
|
||||
chat = ac1.create_group_chat(name="homework", contacts=[])
|
||||
assert chat.get_name() == "homework"
|
||||
chat.send_text("Now we have a group for homework")
|
||||
assert chat.is_promoted()
|
||||
chat.set_name("Homework")
|
||||
assert chat.get_messages()[-1].text == "abc homework xyz Homework by me."
|
||||
|
||||
@pytest.mark.parametrize("verified", [True, False])
|
||||
def test_group_chat_qr(self, acfactory, ac1, verified):
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_group_chat(name="title1", verified=verified)
|
||||
assert chat.is_group()
|
||||
qr = chat.get_join_qr()
|
||||
assert ac2.check_qr(qr).is_ask_verifygroup
|
||||
|
||||
def test_removing_blocked_user_from_group(self, ac1, lp):
|
||||
"""
|
||||
Test that blocked contact is not unblocked when removed from a group.
|
||||
See https://github.com/deltachat/deltachat-core-rust/issues/2030
|
||||
"""
|
||||
lp.sec("Create a group chat with a contact")
|
||||
contact = ac1.create_contact("some1@example.org")
|
||||
group = ac1.create_group_chat("title", contacts=[contact])
|
||||
group.send_text("First group message")
|
||||
|
||||
lp.sec("ac1 blocks contact")
|
||||
contact.block()
|
||||
assert contact.is_blocked()
|
||||
|
||||
lp.sec("ac1 removes contact from their group")
|
||||
group.remove_contact(contact)
|
||||
assert contact.is_blocked()
|
||||
|
||||
lp.sec("ac1 adding blocked contact unblocks it")
|
||||
group.add_contact(contact)
|
||||
assert not contact.is_blocked()
|
||||
|
||||
def test_get_set_profile_image_simple(self, ac1, data):
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
p = data.get_path("d.png")
|
||||
chat.set_profile_image(p)
|
||||
p2 = chat.get_profile_image()
|
||||
assert open(p, "rb").read() == open(p2, "rb").read()
|
||||
chat.remove_profile_image()
|
||||
assert chat.get_profile_image() is None
|
||||
|
||||
def test_mute(self, ac1):
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
assert not chat.is_muted()
|
||||
assert chat.get_mute_duration() == 0
|
||||
chat.mute()
|
||||
assert chat.is_muted()
|
||||
assert chat.get_mute_duration() == -1
|
||||
chat.unmute()
|
||||
assert not chat.is_muted()
|
||||
chat.mute(50)
|
||||
assert chat.is_muted()
|
||||
assert chat.get_mute_duration() <= 50
|
||||
with pytest.raises(ValueError):
|
||||
chat.mute(-51)
|
||||
|
||||
# Regression test, this caused Rust panic previously
|
||||
chat.mute(2**63 - 1)
|
||||
assert chat.is_muted()
|
||||
assert chat.get_mute_duration() == -1
|
||||
|
||||
def test_delete_and_send_fails(self, ac1, chat1):
|
||||
chat1.delete()
|
||||
ac1._evtracker.wait_next_messages_changed()
|
||||
with pytest.raises(ValueError):
|
||||
chat1.send_text("msg1")
|
||||
|
||||
def test_prepare_message_and_send(self, ac1, chat1):
|
||||
msg = chat1.prepare_message(Message.new_empty(chat1.account, "text"))
|
||||
msg.set_text("hello world")
|
||||
assert msg.text == "hello world"
|
||||
assert msg.id > 0
|
||||
chat1.send_prepared(msg)
|
||||
assert "Sent" in msg.get_message_info()
|
||||
str(msg)
|
||||
repr(msg)
|
||||
assert msg == ac1.get_message_by_id(msg.id)
|
||||
|
||||
def test_prepare_file(self, ac1, chat1):
|
||||
blobdir = ac1.get_blobdir()
|
||||
p = os.path.join(blobdir, "somedata.txt")
|
||||
with open(p, "w") as f:
|
||||
f.write("some data")
|
||||
message = chat1.prepare_message_file(p)
|
||||
assert message.id > 0
|
||||
message.set_text("hello world")
|
||||
assert message.is_out_preparing()
|
||||
assert message.text == "hello world"
|
||||
chat1.send_prepared(message)
|
||||
assert "Sent" in message.get_message_info()
|
||||
|
||||
def test_message_eq_contains(self, chat1):
|
||||
msg = chat1.send_text("msg1")
|
||||
assert msg in chat1.get_messages()
|
||||
assert not (msg not in chat1.get_messages())
|
||||
str(msg)
|
||||
repr(msg)
|
||||
|
||||
def test_message_send_text(self, chat1):
|
||||
msg = chat1.send_text("msg1")
|
||||
assert msg
|
||||
assert msg.is_text()
|
||||
assert not msg.is_audio()
|
||||
assert not msg.is_video()
|
||||
assert not msg.is_gif()
|
||||
assert not msg.is_file()
|
||||
assert not msg.is_image()
|
||||
|
||||
assert not msg.is_in_fresh()
|
||||
assert not msg.is_in_noticed()
|
||||
assert not msg.is_in_seen()
|
||||
assert msg.is_out_pending()
|
||||
assert not msg.is_out_failed()
|
||||
assert not msg.is_out_delivered()
|
||||
assert not msg.is_out_mdn_received()
|
||||
|
||||
def test_message_image(self, chat1, data, lp):
|
||||
with pytest.raises(ValueError):
|
||||
chat1.send_image(path="notexists")
|
||||
fn = data.get_path("d.png")
|
||||
lp.sec("sending image")
|
||||
chat1.account._evtracker.consume_events()
|
||||
msg = chat1.send_image(fn)
|
||||
chat1.account._evtracker.get_matching("DC_EVENT_NEW_BLOB_FILE")
|
||||
assert msg.is_image()
|
||||
assert msg
|
||||
assert msg.id > 0
|
||||
assert os.path.exists(msg.filename)
|
||||
assert msg.filemime == "image/png"
|
||||
|
||||
@pytest.mark.parametrize("typein,typeout", [
|
||||
(None, "application/octet-stream"),
|
||||
("text/plain", "text/plain"),
|
||||
("image/png", "image/png"),
|
||||
])
|
||||
def test_message_file(self, ac1, chat1, data, lp, typein, typeout):
|
||||
lp.sec("sending file")
|
||||
fn = data.get_path("r.txt")
|
||||
msg = chat1.send_file(fn, typein)
|
||||
assert msg
|
||||
assert msg.id > 0
|
||||
assert msg.is_file()
|
||||
assert os.path.exists(msg.filename)
|
||||
assert msg.filename.endswith(msg.basename)
|
||||
assert msg.filemime == typeout
|
||||
msg2 = chat1.send_file(fn, typein)
|
||||
assert msg2 != msg
|
||||
assert msg2.filename != msg.filename
|
||||
|
||||
def test_create_contact(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
email = "hello <hello@example.org>"
|
||||
contact1 = ac1.create_contact(email)
|
||||
assert contact1.addr == "hello@example.org"
|
||||
assert contact1.name == "hello"
|
||||
contact1 = ac1.create_contact(email, name="world")
|
||||
assert contact1.name == "world"
|
||||
contact2 = ac1.create_contact("display1 <x@example.org>", "real")
|
||||
assert contact2.name == "real"
|
||||
|
||||
def test_create_chat_simple(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.org", name="some1")
|
||||
contact1.create_chat().send_text("hello")
|
||||
|
||||
def test_chat_message_distinctions(self, ac1, chat1):
|
||||
past1s = datetime.now(timezone.utc) - timedelta(seconds=1)
|
||||
msg = chat1.send_text("msg1")
|
||||
ts = msg.time_sent
|
||||
assert msg.time_received is None
|
||||
assert ts.strftime("Y")
|
||||
assert past1s < ts
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
|
||||
def test_set_config_after_configure_is_forbidden(self, ac1):
|
||||
assert ac1.get_config("mail_pw")
|
||||
assert ac1.is_configured()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_config("addr", "123@example.org")
|
||||
|
||||
def test_import_export_one_contact(self, acfactory, tmpdir):
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
chat = ac1.create_contact("some1 <some1@example.org>").create_chat()
|
||||
# send a text message
|
||||
msg = chat.send_text("msg1")
|
||||
# send a binary file
|
||||
bin = tmpdir.join("some.bin")
|
||||
with bin.open("w") as f:
|
||||
f.write("\00123" * 10000)
|
||||
msg = chat.send_file(bin.strpath)
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
assert not backupdir.listdir()
|
||||
ac1.stop_io()
|
||||
path = ac1.export_all(backupdir.strpath)
|
||||
assert os.path.exists(path)
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
ac2.import_all(path)
|
||||
contacts = ac2.get_contacts(query="some1")
|
||||
assert len(contacts) == 1
|
||||
contact2 = contacts[0]
|
||||
assert contact2.addr == "some1@example.org"
|
||||
chat2 = contact2.create_chat()
|
||||
messages = chat2.get_messages()
|
||||
assert len(messages) == 2
|
||||
assert messages[0].text == "msg1"
|
||||
assert os.path.exists(messages[1].filename)
|
||||
|
||||
def test_set_get_draft(self, chat1):
|
||||
msg = Message.new_empty(chat1.account, "text")
|
||||
msg1 = chat1.prepare_message(msg)
|
||||
msg1.set_text("hello")
|
||||
chat1.set_draft(msg1)
|
||||
msg1.set_text("obsolete")
|
||||
msg2 = chat1.get_draft()
|
||||
assert msg2.text == "hello"
|
||||
chat1.set_draft(None)
|
||||
assert chat1.get_draft() is None
|
||||
|
||||
def test_qr_setup_contact(self, acfactory, lp):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
ac2 = acfactory.get_pseudo_configured_account()
|
||||
qr = ac1.get_setup_contact_qr()
|
||||
assert qr.startswith("OPENPGP4FPR:")
|
||||
res = ac2.check_qr(qr)
|
||||
assert res.is_ask_verifycontact()
|
||||
assert not res.is_ask_verifygroup()
|
||||
assert res.contact_id == 10
|
||||
|
||||
def test_quote(self, chat1):
|
||||
"""Offline quoting test"""
|
||||
msg = Message.new_empty(chat1.account, "text")
|
||||
msg.set_text("Multi\nline\nmessage")
|
||||
assert msg.quoted_text is None
|
||||
|
||||
# Prepare message to assign it a Message-Id.
|
||||
# Messages without Message-Id cannot be quoted.
|
||||
msg = chat1.prepare_message(msg)
|
||||
|
||||
reply_msg = Message.new_empty(chat1.account, "text")
|
||||
reply_msg.set_text("reply")
|
||||
reply_msg.quote = msg
|
||||
assert reply_msg.quoted_text == "Multi\nline\nmessage"
|
||||
|
||||
def test_group_chat_many_members_add_remove(self, ac1, lp):
|
||||
lp.sec("ac1: creating group chat with 10 other members")
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
# promote chat
|
||||
chat.send_text("hello")
|
||||
assert chat.is_promoted()
|
||||
|
||||
# activate local plugin
|
||||
in_list = []
|
||||
|
||||
class InPlugin:
|
||||
@account_hookimpl
|
||||
def ac_member_added(self, chat, contact, actor):
|
||||
in_list.append(("added", chat, contact, actor))
|
||||
|
||||
@account_hookimpl
|
||||
def ac_member_removed(self, chat, contact, actor):
|
||||
in_list.append(("removed", chat, contact, actor))
|
||||
|
||||
ac1.add_account_plugin(InPlugin())
|
||||
|
||||
# perform add contact many times
|
||||
contacts = []
|
||||
for i in range(10):
|
||||
lp.sec("create contact")
|
||||
contact = ac1.create_contact("some{}@example.org".format(i))
|
||||
contacts.append(contact)
|
||||
lp.sec("add contact")
|
||||
chat.add_contact(contact)
|
||||
|
||||
assert chat.num_contacts() == 11
|
||||
|
||||
# let's make sure the events perform plugin hooks
|
||||
def wait_events(cond):
|
||||
now = time.time()
|
||||
while time.time() < now + 5:
|
||||
if cond():
|
||||
break
|
||||
time.sleep(0.1)
|
||||
else:
|
||||
pytest.fail("failed to get events")
|
||||
|
||||
wait_events(lambda: len(in_list) == 10)
|
||||
|
||||
assert len(in_list) == 10
|
||||
chat_contacts = chat.get_contacts()
|
||||
for in_cmd, in_chat, in_contact, in_actor in in_list:
|
||||
assert in_cmd == "added"
|
||||
assert in_chat == chat
|
||||
assert in_contact in chat_contacts
|
||||
assert in_actor is None
|
||||
chat_contacts.remove(in_contact)
|
||||
|
||||
assert chat_contacts[0].id == 1 # self contact
|
||||
|
||||
in_list[:] = []
|
||||
|
||||
lp.sec("ac1: removing two contacts and checking things are right")
|
||||
chat.remove_contact(contacts[9])
|
||||
chat.remove_contact(contacts[3])
|
||||
assert chat.num_contacts() == 9
|
||||
|
||||
wait_events(lambda: len(in_list) == 2)
|
||||
assert len(in_list) == 2
|
||||
assert in_list[0][0] == "removed"
|
||||
assert in_list[0][1] == chat
|
||||
assert in_list[0][2] == contacts[9]
|
||||
assert in_list[1][0] == "removed"
|
||||
assert in_list[1][1] == chat
|
||||
assert in_list[1][2] == contacts[3]
|
||||
|
||||
def test_audit_log_view_without_daymarker(self, ac1, lp):
|
||||
lp.sec("ac1: test audit log (show only system messages)")
|
||||
chat = ac1.create_group_chat(name="audit log sample data")
|
||||
# promote chat
|
||||
chat.send_text("hello")
|
||||
assert chat.is_promoted()
|
||||
|
||||
lp.sec("create test data")
|
||||
chat.add_contact(ac1.create_contact("some-1@example.org"))
|
||||
chat.set_name("audit log test group")
|
||||
chat.send_text("a message in between")
|
||||
|
||||
lp.sec("check message count of all messages")
|
||||
assert len(chat.get_messages()) == 4
|
||||
|
||||
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_array_unref
|
||||
)
|
||||
assert len(list(iter_array(dc_array, lambda x: x))) == 2
|
||||
215
python/tests/test_4_lowlevel.py
Normal file
215
python/tests/test_4_lowlevel.py
Normal file
@@ -0,0 +1,215 @@
|
||||
|
||||
import os
|
||||
|
||||
from queue import Queue
|
||||
from deltachat import capi, cutil, const
|
||||
from deltachat import register_global_plugin
|
||||
from deltachat.hookspec import global_hookimpl
|
||||
from deltachat.capi import ffi
|
||||
from deltachat.capi import lib
|
||||
from deltachat.testplugin import ACSetup, create_dict_from_files_in_path, write_dict_to_dir
|
||||
# from deltachat.account import EventLogger
|
||||
|
||||
|
||||
class TestACSetup:
|
||||
|
||||
def test_cache_writing(self, tmp_path):
|
||||
base = tmp_path.joinpath("hello")
|
||||
base.mkdir()
|
||||
d1 = base.joinpath("dir1")
|
||||
d1.mkdir()
|
||||
d1.joinpath("file1").write_bytes(b'content1')
|
||||
d2 = d1.joinpath("dir2")
|
||||
d2.mkdir()
|
||||
d2.joinpath("file2").write_bytes(b"123")
|
||||
d = create_dict_from_files_in_path(base)
|
||||
newbase = tmp_path.joinpath("other")
|
||||
write_dict_to_dir(d, newbase)
|
||||
assert newbase.joinpath("dir1", "dir2", "file2").exists()
|
||||
assert newbase.joinpath("dir1", "file1").exists()
|
||||
|
||||
def test_basic_states(self, acfactory, monkeypatch, testprocess):
|
||||
pc = ACSetup(init_time=0.0, testprocess=testprocess)
|
||||
acc = acfactory.get_unconfigured_account()
|
||||
monkeypatch.setattr(acc, "configure", lambda **kwargs: None)
|
||||
pc.start_configure(acc)
|
||||
assert pc._account2state[acc] == pc.CONFIGURING
|
||||
pc._configured_events.put((acc, True))
|
||||
monkeypatch.setattr(pc, "init_imap", lambda *args, **kwargs: None)
|
||||
pc.wait_one_configured(acc)
|
||||
assert pc._account2state[acc] == pc.CONFIGURED
|
||||
monkeypatch.setattr(pc, "_onconfigure_start_io", lambda *args, **kwargs: None)
|
||||
pc.bring_online()
|
||||
assert pc._account2state[acc] == pc.IDLEREADY
|
||||
|
||||
def test_two_accounts_one_waited_all_started(self, monkeypatch, acfactory, testprocess):
|
||||
pc = ACSetup(init_time=0.0, testprocess=testprocess)
|
||||
monkeypatch.setattr(pc, "init_imap", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(pc, "_onconfigure_start_io", lambda *args, **kwargs: None)
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
monkeypatch.setattr(ac1, "configure", lambda **kwargs: None)
|
||||
pc.start_configure(ac1)
|
||||
ac2 = acfactory.get_unconfigured_account()
|
||||
monkeypatch.setattr(ac2, "configure", lambda **kwargs: None)
|
||||
pc.start_configure(ac2)
|
||||
assert pc._account2state[ac1] == pc.CONFIGURING
|
||||
assert pc._account2state[ac2] == pc.CONFIGURING
|
||||
pc._configured_events.put((ac1, True))
|
||||
pc.wait_one_configured(ac1)
|
||||
assert pc._account2state[ac1] == pc.CONFIGURED
|
||||
assert pc._account2state[ac2] == pc.CONFIGURING
|
||||
pc._configured_events.put((ac2, True))
|
||||
pc.bring_online()
|
||||
assert pc._account2state[ac1] == pc.IDLEREADY
|
||||
assert pc._account2state[ac2] == pc.IDLEREADY
|
||||
|
||||
def test_store_and_retrieve_configured_account_cache(self, acfactory, tmpdir):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
holder = acfactory._acsetup.testprocess
|
||||
assert holder.cache_maybe_store_configured_db_files(ac1)
|
||||
assert not holder.cache_maybe_store_configured_db_files(ac1)
|
||||
acdir = tmpdir.mkdir("newaccount")
|
||||
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
|
||||
|
||||
|
||||
def test_liveconfig_caching(acfactory, monkeypatch):
|
||||
prod = [
|
||||
{"addr": "1@example.org", "mail_pw": "123"},
|
||||
]
|
||||
acfactory._liveconfig_producer = iter(prod)
|
||||
d1 = acfactory.get_next_liveconfig()
|
||||
d1["hello"] = "world"
|
||||
acfactory._liveconfig_producer = iter(prod)
|
||||
d2 = acfactory.get_next_liveconfig()
|
||||
assert "hello" not in d2
|
||||
|
||||
|
||||
def test_empty_context():
|
||||
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL, capi.ffi.NULL)
|
||||
capi.lib.dc_context_unref(ctx)
|
||||
|
||||
|
||||
def test_dc_close_events(tmpdir, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
|
||||
# register after_shutdown function
|
||||
shutdowns = Queue()
|
||||
|
||||
class ShutdownPlugin:
|
||||
@global_hookimpl
|
||||
def dc_account_after_shutdown(self, account):
|
||||
assert account._dc_context is None
|
||||
shutdowns.put(account)
|
||||
register_global_plugin(ShutdownPlugin())
|
||||
assert hasattr(ac1, "_dc_context")
|
||||
ac1.shutdown()
|
||||
shutdowns.get(timeout=2)
|
||||
|
||||
|
||||
def test_wrong_db(tmpdir):
|
||||
p = tmpdir.join("hello.db")
|
||||
# write an invalid database file
|
||||
p.write("x123" * 10)
|
||||
|
||||
context = lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
|
||||
assert not lib.dc_context_is_open(context)
|
||||
|
||||
|
||||
def test_empty_blobdir(tmpdir):
|
||||
db_fname = tmpdir.join("hello.db")
|
||||
# Apparently some client code expects this to be the same as passing NULL.
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(ffi.NULL, db_fname.strpath.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
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def test_markseen_invalid_message_ids(acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.com", name="some1")
|
||||
chat = contact1.create_chat()
|
||||
chat.send_text("one messae")
|
||||
ac1._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
msg_ids = [9]
|
||||
lib.dc_markseen_msgs(ac1._dc_context, msg_ids, len(msg_ids))
|
||||
ac1._evtracker.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
|
||||
|
||||
|
||||
def test_get_special_message_id_returns_empty_message(acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
for i in range(1, 10):
|
||||
msg = ac1.get_message_by_id(i)
|
||||
assert msg.id == 0
|
||||
|
||||
|
||||
def test_provider_info_none():
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(ffi.NULL, ffi.NULL, ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
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")
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
|
||||
assert 'deltachat_core_version' in info
|
||||
assert 'database_dir' in info
|
||||
|
||||
|
||||
def test_logged_hook_failure(acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
cap = []
|
||||
ac1.log = cap.append
|
||||
with ac1._event_thread.swallow_and_log_exception("some"):
|
||||
0/0
|
||||
assert cap
|
||||
assert "some" in str(cap)
|
||||
assert "ZeroDivisionError" in str(cap)
|
||||
assert "Traceback" in str(cap)
|
||||
|
||||
|
||||
def test_logged_ac_process_ffi_failure(acfactory):
|
||||
from deltachat import account_hookimpl
|
||||
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
|
||||
class FailPlugin:
|
||||
@account_hookimpl
|
||||
def ac_process_ffi_event(ffi_event):
|
||||
0/0
|
||||
|
||||
cap = Queue()
|
||||
ac1.log = cap.put
|
||||
ac1.add_account_plugin(FailPlugin())
|
||||
# cause any event eg contact added/changed
|
||||
ac1.create_contact("something@example.org")
|
||||
res = cap.get(timeout=10)
|
||||
assert "ac_process_ffi_event" in res
|
||||
assert "ZeroDivisionError" in res
|
||||
assert "Traceback" in res
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,104 +0,0 @@
|
||||
from __future__ import print_function
|
||||
|
||||
from queue import Queue
|
||||
from deltachat import capi, cutil, const
|
||||
from deltachat import register_global_plugin
|
||||
from deltachat.hookspec import global_hookimpl
|
||||
from deltachat.capi import ffi
|
||||
from deltachat.capi import lib
|
||||
# from deltachat.account import EventLogger
|
||||
|
||||
|
||||
def test_empty_context():
|
||||
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL, capi.ffi.NULL)
|
||||
capi.lib.dc_context_unref(ctx)
|
||||
|
||||
|
||||
def test_dc_close_events(tmpdir, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
|
||||
# register after_shutdown function
|
||||
shutdowns = Queue()
|
||||
|
||||
class ShutdownPlugin:
|
||||
@global_hookimpl
|
||||
def dc_account_after_shutdown(self, account):
|
||||
assert account._dc_context is None
|
||||
shutdowns.put(account)
|
||||
register_global_plugin(ShutdownPlugin())
|
||||
assert hasattr(ac1, "_dc_context")
|
||||
ac1.shutdown()
|
||||
shutdowns.get(timeout=2)
|
||||
|
||||
|
||||
def test_wrong_db(tmpdir):
|
||||
p = tmpdir.join("hello.db")
|
||||
# write an invalid database file
|
||||
p.write("x123" * 10)
|
||||
|
||||
assert ffi.NULL == lib.dc_context_new(ffi.NULL, p.strpath.encode("ascii"), ffi.NULL)
|
||||
|
||||
|
||||
def test_empty_blobdir(tmpdir):
|
||||
db_fname = tmpdir.join("hello.db")
|
||||
# Apparently some client code expects this to be the same as passing NULL.
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(ffi.NULL, db_fname.strpath.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
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
def test_markseen_invalid_message_ids(acfactory):
|
||||
ac1 = acfactory.get_configured_offline_account()
|
||||
|
||||
contact1 = ac1.create_contact("some1@example.com", name="some1")
|
||||
chat = contact1.create_chat()
|
||||
chat.send_text("one messae")
|
||||
ac1._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||
msg_ids = [9]
|
||||
lib.dc_markseen_msgs(ac1._dc_context, msg_ids, len(msg_ids))
|
||||
ac1._evtracker.ensure_event_not_queued("DC_EVENT_WARNING|DC_EVENT_ERROR")
|
||||
|
||||
|
||||
def test_get_special_message_id_returns_empty_message(acfactory):
|
||||
ac1 = acfactory.get_configured_offline_account()
|
||||
for i in range(1, 10):
|
||||
msg = ac1.get_message_by_id(i)
|
||||
assert msg.id == 0
|
||||
|
||||
|
||||
def test_provider_info_none():
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(ffi.NULL, ffi.NULL, ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
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")
|
||||
ctx = ffi.gc(
|
||||
lib.dc_context_new(ffi.NULL, db_fname.strpath.encode("ascii"), ffi.NULL),
|
||||
lib.dc_context_unref,
|
||||
)
|
||||
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
|
||||
assert 'deltachat_core_version' in info
|
||||
assert 'database_dir' in info
|
||||
@@ -8,7 +8,7 @@ envlist =
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
pytest -n6 --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
|
||||
pytest -n6 --extra-info --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
|
||||
python tests/package_wheels.py {toxworkdir}/wheelhouse
|
||||
passenv =
|
||||
TRAVIS
|
||||
@@ -78,8 +78,8 @@ commands =
|
||||
addopts = -v -ra --strict-markers
|
||||
norecursedirs = .tox
|
||||
xfail_strict=true
|
||||
timeout = 90
|
||||
timeout_method = thread
|
||||
timeout = 150
|
||||
timeout_func_only = True
|
||||
markers =
|
||||
ignored: ignore this test in default test runs, use --ignored to run.
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
1.54.0
|
||||
1.60.0
|
||||
|
||||
@@ -8,7 +8,7 @@ set -e -x
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.54.0
|
||||
RUST_VERSION=1.60.0
|
||||
|
||||
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
|
||||
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
|
||||
@@ -8,7 +8,7 @@ set -e -x
|
||||
#
|
||||
# Avoid using rustup here as it depends on reading /proc/self/exe and
|
||||
# has problems running under QEMU.
|
||||
RUST_VERSION=1.54.0
|
||||
RUST_VERSION=1.60.0
|
||||
|
||||
curl "https://static.rust-lang.org/dist/rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu.tar.gz" | tar xz
|
||||
cd "rust-${RUST_VERSION}-$(uname -m)-unknown-linux-gnu"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import pathlib
|
||||
import subprocess
|
||||
@@ -41,6 +41,24 @@ def replace_toml_version(relpath, newversion):
|
||||
os.rename(tmp_path, str(p))
|
||||
|
||||
|
||||
def read_json_version(relpath):
|
||||
p = pathlib.Path("package.json")
|
||||
assert p.exists()
|
||||
with open(p, "r") as f:
|
||||
json_data = json.loads(f.read())
|
||||
return json_data["version"]
|
||||
|
||||
|
||||
def update_package_json(newversion):
|
||||
p = pathlib.Path("package.json")
|
||||
assert p.exists()
|
||||
with open(p, "r") as f:
|
||||
json_data = json.loads(f.read())
|
||||
json_data["version"] = newversion
|
||||
with open(p, "w") as f:
|
||||
f.write(json.dumps(json_data, sort_keys=True, indent=2))
|
||||
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(prog="set_core_version")
|
||||
parser.add_argument("newversion")
|
||||
@@ -52,6 +70,7 @@ def main():
|
||||
print()
|
||||
for x in toml_list:
|
||||
print("{}: {}".format(x, read_toml_version(x)))
|
||||
print("package.json:", str(read_json_version("package.json")))
|
||||
print()
|
||||
raise SystemExit("need argument: new version, example: 1.25.0")
|
||||
|
||||
@@ -74,6 +93,7 @@ def main():
|
||||
|
||||
replace_toml_version("Cargo.toml", newversion)
|
||||
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
|
||||
update_package_json(newversion)
|
||||
|
||||
print("running cargo check")
|
||||
subprocess.call(["cargo", "check"])
|
||||
|
||||
124
src/accounts.rs
124
src/accounts.rs
@@ -54,10 +54,19 @@ impl Accounts {
|
||||
ensure!(dir.exists().await, "directory does not exist");
|
||||
|
||||
let config_file = dir.join(CONFIG_NAME);
|
||||
ensure!(config_file.exists().await, "accounts.toml does not exist");
|
||||
ensure!(
|
||||
config_file.exists().await,
|
||||
"{:?} does not exist",
|
||||
config_file
|
||||
);
|
||||
|
||||
let config = Config::from_file(config_file).await?;
|
||||
let accounts = config.load_accounts().await?;
|
||||
let config = Config::from_file(config_file)
|
||||
.await
|
||||
.context("failed to load accounts config")?;
|
||||
let accounts = config
|
||||
.load_accounts()
|
||||
.await
|
||||
.context("failed to load accounts")?;
|
||||
|
||||
let emitter = EventEmitter::new();
|
||||
|
||||
@@ -66,7 +75,9 @@ impl Accounts {
|
||||
emitter.sender.send(events.get_emitter()).await?;
|
||||
|
||||
for account in accounts.values() {
|
||||
emitter.add_account(account).await?;
|
||||
emitter.add_account(account).await.with_context(|| {
|
||||
format!("failed to add account {} to event emitter", account.id)
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
@@ -104,7 +115,9 @@ impl Accounts {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a new account.
|
||||
/// Add a new account and opens it.
|
||||
///
|
||||
/// Returns account ID.
|
||||
pub async fn add_account(&mut self) -> Result<u32> {
|
||||
let account_config = self.config.new_account(&self.dir).await?;
|
||||
|
||||
@@ -115,6 +128,17 @@ impl Accounts {
|
||||
Ok(account_config.id)
|
||||
}
|
||||
|
||||
/// Adds a new closed account.
|
||||
pub async fn add_closed_account(&mut self) -> Result<u32> {
|
||||
let account_config = self.config.new_account(&self.dir).await?;
|
||||
|
||||
let ctx = Context::new_closed(account_config.dbfile().into(), account_config.id).await?;
|
||||
self.emitter.add_account(&ctx).await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
|
||||
Ok(account_config.id)
|
||||
}
|
||||
|
||||
/// Remove an account.
|
||||
pub async fn remove_account(&mut self, id: u32) -> Result<()> {
|
||||
let ctx = self.accounts.remove(&id);
|
||||
@@ -124,9 +148,27 @@ impl Accounts {
|
||||
drop(ctx);
|
||||
|
||||
if let Some(cfg) = self.config.get_account(id).await {
|
||||
fs::remove_dir_all(async_std::path::PathBuf::from(&cfg.dir))
|
||||
.await
|
||||
.context("failed to remove account data")?;
|
||||
// Spend up to 1 minute trying to remove the files.
|
||||
// Files may remain locked up to 30 seconds due to r2d2 bug:
|
||||
// https://github.com/sfackler/r2d2/issues/99
|
||||
let mut counter = 0;
|
||||
loop {
|
||||
counter += 1;
|
||||
|
||||
if let Err(err) = fs::remove_dir_all(async_std::path::PathBuf::from(&cfg.dir))
|
||||
.await
|
||||
.context("failed to remove account data")
|
||||
{
|
||||
if counter > 60 {
|
||||
return Err(err);
|
||||
}
|
||||
|
||||
// Wait 1 second and try again.
|
||||
async_std::task::sleep(std::time::Duration::from_millis(1000)).await;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
self.config.remove_account(id).await?;
|
||||
|
||||
@@ -182,7 +224,7 @@ impl Accounts {
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
let ctx = Context::with_blobdir(new_dbfile, new_blobdir, account_config.id).await?;
|
||||
let ctx = Context::new(new_dbfile, account_config.id).await?;
|
||||
self.emitter.add_account(&ctx).await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
Ok(account_config.id)
|
||||
@@ -383,7 +425,15 @@ impl Config {
|
||||
pub async fn load_accounts(&self) -> Result<BTreeMap<u32, Context>> {
|
||||
let mut accounts = BTreeMap::new();
|
||||
for account_config in &self.inner.accounts {
|
||||
let ctx = Context::new(account_config.dbfile().into(), account_config.id).await?;
|
||||
let ctx = Context::new(account_config.dbfile().into(), account_config.id)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to create context from file {:?}",
|
||||
account_config.dbfile()
|
||||
)
|
||||
})?;
|
||||
|
||||
accounts.insert(account_config.id, ctx);
|
||||
}
|
||||
|
||||
@@ -408,8 +458,13 @@ impl Config {
|
||||
|
||||
self.sync().await?;
|
||||
|
||||
self.select_account(id).await.expect("just added");
|
||||
let cfg = self.get_account(id).await.expect("just added");
|
||||
self.select_account(id)
|
||||
.await
|
||||
.context("failed to select just added account")?;
|
||||
let cfg = self
|
||||
.get_account(id)
|
||||
.await
|
||||
.context("failed to get just added account")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
|
||||
@@ -701,4 +756,49 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_encrypted_account() -> Result<()> {
|
||||
let dir = tempfile::tempdir().context("failed to create tempdir")?;
|
||||
let p: PathBuf = dir.path().join("accounts").into();
|
||||
|
||||
let mut accounts = Accounts::new(p.clone())
|
||||
.await
|
||||
.context("failed to create accounts manager")?;
|
||||
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
let account_id = accounts
|
||||
.add_closed_account()
|
||||
.await
|
||||
.context("failed to add closed account")?;
|
||||
let account = accounts
|
||||
.get_selected_account()
|
||||
.await
|
||||
.context("failed to get account")?;
|
||||
assert_eq!(account.id, account_id);
|
||||
let passphrase_set_success = account
|
||||
.open("foobar".to_string())
|
||||
.await
|
||||
.context("failed to set passphrase")?;
|
||||
assert!(passphrase_set_success);
|
||||
drop(accounts);
|
||||
|
||||
let accounts = Accounts::new(p.clone())
|
||||
.await
|
||||
.context("failed to create second accounts manager")?;
|
||||
let account = accounts
|
||||
.get_selected_account()
|
||||
.await
|
||||
.context("failed to get account")?;
|
||||
assert_eq!(account.is_open().await, false);
|
||||
|
||||
// Try wrong passphrase.
|
||||
assert_eq!(account.open("barfoo".to_string()).await?, false);
|
||||
assert_eq!(account.open("".to_string()).await?, false);
|
||||
|
||||
assert_eq!(account.open("foobar".to_string()).await?, true);
|
||||
assert_eq!(account.is_open().await, true);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
//!
|
||||
//! Parse and create [Autocrypt-headers](https://autocrypt.org/en/latest/level1.html#the-autocrypt-header).
|
||||
|
||||
use anyhow::{bail, format_err, Error, Result};
|
||||
use anyhow::{bail, Context as _, Error, Result};
|
||||
use std::collections::BTreeMap;
|
||||
use std::str::FromStr;
|
||||
use std::{fmt, str};
|
||||
@@ -139,15 +139,14 @@ impl str::FromStr for Aheader {
|
||||
};
|
||||
let public_key: SignedPublicKey = attributes
|
||||
.remove("keydata")
|
||||
.ok_or_else(|| format_err!("keydata attribute is not found"))
|
||||
.context("keydata attribute is not found")
|
||||
.and_then(|raw| {
|
||||
SignedPublicKey::from_base64(&raw)
|
||||
.map_err(|_| format_err!("Autocrypt key cannot be decoded"))
|
||||
SignedPublicKey::from_base64(&raw).context("autocrypt key cannot be decoded")
|
||||
})
|
||||
.and_then(|key| {
|
||||
key.verify()
|
||||
.and(Ok(key))
|
||||
.map_err(|_| format_err!("Autocrypt key cannot be verified"))
|
||||
.context("autocrypt key cannot be verified")
|
||||
})?;
|
||||
|
||||
let prefer_encrypt = attributes
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user