mirror of
https://github.com/chatmail/core.git
synced 2026-04-03 22:12:11 +03:00
Compare commits
59 Commits
link2xt/re
...
simon/inte
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e47e50a7e | ||
|
|
eacbb82399 | ||
|
|
ee279f84ad | ||
|
|
26959d5b75 | ||
|
|
ff5005fa93 | ||
|
|
8f316e12d5 | ||
|
|
5f00fc4e27 | ||
|
|
f279730b0f | ||
|
|
5a5f8b03d1 | ||
|
|
5e73e9cd72 | ||
|
|
129de9182f | ||
|
|
09798df7a0 | ||
|
|
b360225e08 | ||
|
|
09d5e44b13 | ||
|
|
8ba89c0fa1 | ||
|
|
f984a27379 | ||
|
|
425a2310fe | ||
|
|
95571be278 | ||
|
|
7bf44a237e | ||
|
|
47dbac9b50 | ||
|
|
a49282727b | ||
|
|
0d22fc7ac1 | ||
|
|
1040bc551f | ||
|
|
5aa0205c80 | ||
|
|
210a4ebcbe | ||
|
|
7fc2b06b3f | ||
|
|
1177c19a43 | ||
|
|
8a2417f32d | ||
|
|
a154347834 | ||
|
|
d51adf2aa0 | ||
|
|
a5f0c1613e | ||
|
|
7c4bcf9004 | ||
|
|
2dd44d5f89 | ||
|
|
f656cb29be | ||
|
|
53230b6eb0 | ||
|
|
80ca59f152 | ||
|
|
eb624e43c0 | ||
|
|
532e9cb09a | ||
|
|
ef4d2a7ed0 | ||
|
|
6d2ac30461 | ||
|
|
33a203d56e | ||
|
|
a19811f379 | ||
|
|
f23023961e | ||
|
|
b463a0566e | ||
|
|
38d5743c06 | ||
|
|
6990312051 | ||
|
|
a7cf51868b | ||
|
|
815c1b9c49 | ||
|
|
88bba83383 | ||
|
|
b1d517398d | ||
|
|
4e5b41f150 | ||
|
|
56b2361f01 | ||
|
|
968cc65323 | ||
|
|
d0ee21e6dc | ||
|
|
a1345f2542 | ||
|
|
f290fe0871 | ||
|
|
aa78e82fed | ||
|
|
d4e670d5e9 | ||
|
|
4553c6521f |
31
.github/workflows/ci.yml
vendored
31
.github/workflows/ci.yml
vendored
@@ -25,7 +25,7 @@ jobs:
|
||||
name: Lint Rust
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
RUSTUP_TOOLCHAIN: 1.72.0
|
||||
RUSTUP_TOOLCHAIN: 1.73.0
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Install rustfmt and clippy
|
||||
@@ -81,11 +81,11 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
rust: 1.68.2
|
||||
rust: 1.73.0
|
||||
- os: windows-latest
|
||||
rust: 1.68.2
|
||||
rust: 1.73.0
|
||||
- os: macos-latest
|
||||
rust: 1.68.2
|
||||
rust: 1.73.0
|
||||
|
||||
# Minimum Supported Rust Version = 1.65.0
|
||||
#
|
||||
@@ -182,15 +182,15 @@ jobs:
|
||||
include:
|
||||
# Currently used Rust version.
|
||||
- os: ubuntu-latest
|
||||
python: 3.11
|
||||
python: 3.12
|
||||
- os: macos-latest
|
||||
python: 3.11
|
||||
python: 3.12
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
python: pypy3.9
|
||||
python: pypy3.10
|
||||
- os: macos-latest
|
||||
python: pypy3.9
|
||||
python: pypy3.10
|
||||
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
@@ -232,22 +232,19 @@ jobs:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
python: 3.11
|
||||
python: 3.12
|
||||
- os: macos-latest
|
||||
python: 3.11
|
||||
python: 3.12
|
||||
|
||||
# PyPy tests
|
||||
- os: ubuntu-latest
|
||||
python: pypy3.9
|
||||
python: pypy3.10
|
||||
- os: macos-latest
|
||||
python: pypy3.9
|
||||
python: pypy3.10
|
||||
|
||||
# Minimum Supported Python Version = 3.8
|
||||
#
|
||||
# Python 3.7 has at least one known bug related to starting subprocesses
|
||||
# in asyncio programs: <https://bugs.python.org/issue35621>
|
||||
# Minimum Supported Python Version = 3.7
|
||||
- os: ubuntu-latest
|
||||
python: 3.8
|
||||
python: 3.7
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -18,6 +18,9 @@ python/.eggs
|
||||
__pycache__
|
||||
python/src/deltachat/capi*.so
|
||||
python/.venv/
|
||||
python/venv/
|
||||
venv/
|
||||
env/
|
||||
|
||||
python/liveconfig*
|
||||
|
||||
|
||||
63
CHANGELOG.md
63
CHANGELOG.md
@@ -1,5 +1,65 @@
|
||||
# Changelog
|
||||
|
||||
## [1.124.1] - 2023-10-05
|
||||
|
||||
### Fixes
|
||||
|
||||
- Remove footer from reactions on the receiver side ([#4780](https://github.com/deltachat/deltachat-core-rust/pull/4780)).
|
||||
|
||||
### CI
|
||||
|
||||
- Pin `urllib3` version to `<2`. ([#4788](https://github.com/deltachat/deltachat-core-rust/issues/4788))
|
||||
|
||||
## [1.124.0] - 2023-10-04
|
||||
|
||||
### API-Changes
|
||||
|
||||
- [**breaking**] Return `DC_CONTACT_ID_SELF` from `dc_contact_get_verifier_id()` for directly verified contacts.
|
||||
- Deprecate `dc_contact_get_verifier_addr`.
|
||||
- python: use `dc_contact_get_verifier_id()`. `get_verifier()` returns a Contact rather than an address now.
|
||||
- Deprecate `get_next_media()`.
|
||||
- Ignore public key argument in `dc_preconfigure_keypair()`. Public key is extracted from the private key.
|
||||
|
||||
### Fixes
|
||||
|
||||
- Wrap base64-encoded parts to 76 characters.
|
||||
- Require valid email addresses in `dc_provider_new_from_email[_with_dns]`.
|
||||
- Do not trash messages with attachments and no text when `location.kml` is attached ([#4749](https://github.com/deltachat/deltachat-core-rust/issues/4749)).
|
||||
- Initialise `last_msg_id` to the highest known row id. This ensures bots migrated from older version to `dc_get_next_msgs()` API do not process all previous messages from scratch.
|
||||
- Do not put the status footer into reaction MIME parts.
|
||||
- Ignore special chats in `get_similar_chat_ids()`. This prevents trash chat from showing up in similar chat list ([#4756](https://github.com/deltachat/deltachat-core-rust/issues/4756)).
|
||||
- Cap percentage in connectivity layout to 100% ([#4765](https://github.com/deltachat/deltachat-core-rust/pull/4765)).
|
||||
- Add Let's Encrypt root certificate to `reqwest`. This should allow scanning `DCACCOUNT` QR-codes on older Android phones when the server has a Let's Encrypt certificate.
|
||||
- deltachat-rpc-client: Increase stdio buffer to 64 MiB to avoid Python bots crashing when trying to load large messages via a JSON-RPC call.
|
||||
- Add `protected-headers` directive to Content-Type of encrypted messages with attachments ([#2302](https://github.com/deltachat/deltachat-core-rust/issues/2302)). This makes Thunderbird show encrypted Subject for Delta Chat messages.
|
||||
- webxdc: Reset `document.update` on forwarding. This fixes the test `test_forward_webxdc_instance()`.
|
||||
|
||||
### Features / Changes
|
||||
|
||||
- Remove extra members from the local list in sake of group membership consistency ([#3782](https://github.com/deltachat/deltachat-core-rust/issues/3782)).
|
||||
- deltachat-rpc-client: Log exceptions when long-running tasks die.
|
||||
|
||||
### Build
|
||||
|
||||
- Build wheels for Python 3.12 and PyPy 3.10.
|
||||
|
||||
## [1.123.0] - 2023-09-22
|
||||
|
||||
### API-Changes
|
||||
|
||||
- Make it possible to import secret key from a file with `DC_IMEX_IMPORT_SELF_KEYS`.
|
||||
- [**breaking**] Make `dc_jsonrpc_blocking_call` accept JSON-RPC request.
|
||||
|
||||
### Fixes
|
||||
|
||||
- `lookup_chat_by_reply()`: Skip not fully downloaded and undecipherable messages ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
|
||||
- `lookup_chat_by_reply()`: Skip undecipherable parent messages created by older versions ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
|
||||
- imex: Use "default" in the filename of the default key.
|
||||
|
||||
### Miscellaneous Tasks
|
||||
|
||||
- Update OpenSSL from 3.1.2 to 3.1.3.
|
||||
|
||||
## [1.122.0] - 2023-09-12
|
||||
|
||||
### API-Changes
|
||||
@@ -2816,3 +2876,6 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||
[1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0
|
||||
[1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0
|
||||
[1.122.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.121.0...v1.122.0
|
||||
[1.123.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.122.0...v1.123.0
|
||||
[1.124.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.123.0...v1.124.0
|
||||
[1.124.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.0...v1.124.1
|
||||
|
||||
53
Cargo.lock
generated
53
Cargo.lock
generated
@@ -312,9 +312,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
|
||||
|
||||
[[package]]
|
||||
name = "axum"
|
||||
version = "0.6.18"
|
||||
version = "0.6.20"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8175979259124331c1d7bf6586ee7e0da434155e4b2d48ec2c8386281d8df39"
|
||||
checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
@@ -1103,7 +1103,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.122.0"
|
||||
version = "1.124.1"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1118,6 +1118,7 @@ dependencies = [
|
||||
"chrono",
|
||||
"criterion",
|
||||
"deltachat_derive",
|
||||
"deltachat_message_parser",
|
||||
"email",
|
||||
"encoded-words",
|
||||
"escaper",
|
||||
@@ -1179,7 +1180,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.122.0"
|
||||
version = "1.124.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-channel",
|
||||
@@ -1203,7 +1204,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-repl"
|
||||
version = "1.122.0"
|
||||
version = "1.124.1"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1218,7 +1219,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.122.0"
|
||||
version = "1.124.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1243,7 +1244,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.122.0"
|
||||
version = "1.124.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"deltachat",
|
||||
@@ -1259,6 +1260,18 @@ dependencies = [
|
||||
"yerpc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_message_parser"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "826f80f4b32f51457773351b2d821dc1f45273a38235e8fd3bdf662b67b70bcd"
|
||||
dependencies = [
|
||||
"nom",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
"unic-idna-punycode",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "der"
|
||||
version = "0.6.1"
|
||||
@@ -1605,7 +1618,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "email"
|
||||
version = "0.0.21"
|
||||
source = "git+https://github.com/deltachat/rust-email?branch=master#25702df99254d059483b41417cd80696a258df8e"
|
||||
source = "git+https://github.com/deltachat/rust-email?branch=master#37778c89d5eb5a94b7983f3f37ff67769bde3cf9"
|
||||
dependencies = [
|
||||
"base64 0.11.0",
|
||||
"chrono",
|
||||
@@ -3110,9 +3123,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf"
|
||||
|
||||
[[package]]
|
||||
name = "openssl-src"
|
||||
version = "300.1.3+3.1.2"
|
||||
version = "300.1.5+3.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cd2c101a165fff9935e34def4669595ab1c7847943c42be86e21503e482be107"
|
||||
checksum = "559068e4c12950d7dcaa1857a61725c0d38d4fc03ff8e070ab31a75d6e316491"
|
||||
dependencies = [
|
||||
"cc",
|
||||
]
|
||||
@@ -4947,9 +4960,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-tungstenite"
|
||||
version = "0.18.0"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "54319c93411147bced34cb5609a80e0a8e44c5999c93903a81cd866630ec0bfd"
|
||||
checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c"
|
||||
dependencies = [
|
||||
"futures-util",
|
||||
"log",
|
||||
@@ -5159,13 +5172,13 @@ checksum = "3528ecfd12c466c6f163363caf2d02a71161dd5e1cc6ae7b34207ea2d42d81ed"
|
||||
|
||||
[[package]]
|
||||
name = "tungstenite"
|
||||
version = "0.18.0"
|
||||
version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "30ee6ab729cd4cf0fd55218530c4522ed30b7b6081752839b68fcec8d0960788"
|
||||
checksum = "9e3dac10fd62eaf6617d3a904ae222845979aec67c615d1c842b4002c7666fb9"
|
||||
dependencies = [
|
||||
"base64 0.13.1",
|
||||
"byteorder",
|
||||
"bytes",
|
||||
"data-encoding",
|
||||
"http",
|
||||
"httparse",
|
||||
"log",
|
||||
@@ -5221,6 +5234,12 @@ version = "0.1.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94"
|
||||
|
||||
[[package]]
|
||||
name = "unic-idna-punycode"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "06feaedcbf9f1fc259144d833c0d630b8b15207b0486ab817d29258bc89f2f8a"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.13"
|
||||
@@ -5448,9 +5467,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "webpki"
|
||||
version = "0.22.1"
|
||||
version = "0.22.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0e74f82d49d545ad128049b7e88f6576df2da6b02e9ce565c6f533be576957e"
|
||||
checksum = "07ecc0cd7cac091bf682ec5efa18b1cff79d617b84181f38b3951dbe135f607f"
|
||||
dependencies = [
|
||||
"ring",
|
||||
"untrusted",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.122.0"
|
||||
version = "1.124.1"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.65"
|
||||
@@ -94,6 +94,7 @@ toml = "0.7"
|
||||
trust-dns-resolver = "0.22"
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
deltachat_message_parser = "0.8.0"
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
@@ -154,8 +155,4 @@ harness = false
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
internals = []
|
||||
vendored = [
|
||||
"async-native-tls/vendored",
|
||||
"rusqlite/bundled-sqlcipher-vendored-openssl",
|
||||
"reqwest/native-tls-vendored"
|
||||
]
|
||||
vendored = ["async-native-tls/vendored", "rusqlite/bundled-sqlcipher-vendored-openssl", "reqwest/native-tls-vendored"]
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.122.0"
|
||||
version = "1.124.1"
|
||||
description = "Deltachat FFI"
|
||||
edition = "2018"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -492,6 +492,9 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
|
||||
* 0=do not fetch existing messages on configure.
|
||||
* In both cases, existing recipients are added to the contact database.
|
||||
* - `disable_idle` = 1=disable IMAP IDLE even if the server supports it,
|
||||
* 0=use IMAP IDLE if the server supports it.
|
||||
* This is a developer option used for testing polling used as an IDLE fallback.
|
||||
* - `download_limit` = Messages up to this number of bytes are downloaded automatically.
|
||||
* For larger messages, only the header is downloaded and a placeholder is shown.
|
||||
* These messages can be downloaded fully using dc_download_full_msg() later.
|
||||
@@ -824,7 +827,7 @@ void dc_maybe_network (dc_context_t* context);
|
||||
* @param context The context as created by dc_context_new().
|
||||
* @param addr The e-mail address of the user. This must match the
|
||||
* configured_addr setting of the context as well as the UID of the key.
|
||||
* @param public_data ASCII armored public key.
|
||||
* @param public_data Ignored, actual public key is extracted from secret_data.
|
||||
* @param secret_data ASCII armored secret key.
|
||||
* @return 1 on success, 0 on failure.
|
||||
*/
|
||||
@@ -1175,24 +1178,6 @@ int dc_send_webxdc_status_update (dc_context_t* context, uint32_t msg_id, const
|
||||
*/
|
||||
char* dc_get_webxdc_status_updates (dc_context_t* context, uint32_t msg_id, uint32_t serial);
|
||||
|
||||
|
||||
/**
|
||||
* Replaces webxdc app with a new version.
|
||||
*
|
||||
* On the JavaScript side this API could be used like this:
|
||||
* ```
|
||||
* window.webxdc.replaceWebxdc(blob);
|
||||
* ```
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id The ID of the WebXDC message to be replaced.
|
||||
* @param blob New blob to replace WebXDC with.
|
||||
* @param n Blob size.
|
||||
*/
|
||||
void dc_replace_webxdc(dc_context_t* context, uint32_t msg_id, uint8_t *blob, size_t n);
|
||||
|
||||
|
||||
/**
|
||||
* Save a draft for a chat in the database.
|
||||
*
|
||||
@@ -1499,6 +1484,7 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
|
||||
* Typically used to implement the "next" and "previous" buttons
|
||||
* in a gallery or in a media player.
|
||||
*
|
||||
* @deprecated Deprecated 2023-10-03, use dc_get_chat_media() and navigate the returned array instead.
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param msg_id The ID of the current message from which the next or previous message should be searched.
|
||||
@@ -4007,6 +3993,36 @@ int64_t dc_msg_get_sort_timestamp (const dc_msg_t* msg);
|
||||
char* dc_msg_get_text (const dc_msg_t* msg);
|
||||
|
||||
|
||||
#define MESSAGE_PARSER_MODE_ONLY_TEXT 0x00
|
||||
#define MESSAGE_PARSER_MODE_DESKTOP_SET 0x01
|
||||
#define MESSAGE_PARSER_MODE_MARKDOWN 0x02
|
||||
|
||||
/**
|
||||
* Parse text with the message parser.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param input The text to parse.
|
||||
* @param mode Sets the parsing mode, you can choose between MESSAGE_PARSER_MODE_ONLY_TEXT, MESSAGE_PARSER_MODE_DESKTOP_SET and MESSAGE_PARSER_MODE_MARKDOWN.
|
||||
* Look at https://github.com/deltachat/message-parser/blob/master/spec.md#modes-of-the-parser to learn more about the parser modes.
|
||||
* @return Abstract Syntax Tree for your message that you can use to display parts of a message specially like links.
|
||||
* This ast is returned in json (look at the sourcecode for reference for the format: https://github.com/deltachat/message-parser/blob/master/src/parser/mod.rs#L11)
|
||||
*/
|
||||
char* dc_parse_message_text_to_ast_json (const char* input, int mode);
|
||||
|
||||
|
||||
/**
|
||||
* Parse the text of a message with the message parser.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The message object.
|
||||
* @param mode Sets the parsing mode, you can choose between MESSAGE_PARSER_MODE_ONLY_TEXT, MESSAGE_PARSER_MODE_DESKTOP_SET and MESSAGE_PARSER_MODE_MARKDOWN.
|
||||
* Look at https://github.com/deltachat/message-parser/blob/master/spec.md#modes-of-the-parser to learn more about the parser modes.
|
||||
* @return Abstract Syntax Tree for your message that you can use to display parts of a message specially like links.
|
||||
* This ast is returned in json (look at the sourcecode for reference for the format: https://github.com/deltachat/message-parser/blob/master/src/parser/mod.rs#L11)
|
||||
*/
|
||||
char* dc_msg_get_parsed_text_as_json (const dc_msg_t* msg, int mode);
|
||||
|
||||
|
||||
/**
|
||||
* Get the subject of the e-mail.
|
||||
* If there is no subject associated with the message, an empty string is returned.
|
||||
@@ -5065,6 +5081,7 @@ int dc_contact_is_verified (dc_contact_t* contact);
|
||||
* A string containing the verifiers address. If it is the same address as the contact itself,
|
||||
* we verified the contact ourself. If it is an empty string, we don't have verifier
|
||||
* information or the contact is not verified.
|
||||
* @deprecated 2023-09-28, use dc_contact_get_verifier_id instead
|
||||
*/
|
||||
char* dc_contact_get_verifier_addr (dc_contact_t* contact);
|
||||
|
||||
@@ -5077,7 +5094,7 @@ char* dc_contact_get_verifier_addr (dc_contact_t* contact);
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return
|
||||
* The `ContactId` of the verifiers address. If it is the same address as the contact itself,
|
||||
* The contact ID of the verifier. If it is DC_CONTACT_ID_SELF,
|
||||
* we verified the contact ourself. If it is 0, we don't have verifier information or
|
||||
* the contact is not verified.
|
||||
*/
|
||||
@@ -5773,12 +5790,11 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
|
||||
*
|
||||
* @memberof dc_jsonrpc_instance_t
|
||||
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
|
||||
* @param method JSON-RPC method name, e.g. `check_email_validity`.
|
||||
* @param params JSON-RPC method parameters, e.g. `["alice@example.org"]`.
|
||||
* @param input JSON-RPC request.
|
||||
* @return JSON-RPC response as string, must be freed using dc_str_unref() after usage.
|
||||
* On error, NULL is returned.
|
||||
* If there is no response, NULL is returned.
|
||||
*/
|
||||
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *method, const char *params);
|
||||
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input);
|
||||
|
||||
/**
|
||||
* @class dc_event_emitter_t
|
||||
|
||||
@@ -29,14 +29,15 @@ use deltachat::contact::{Contact, ContactId, Origin};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::imex::BackupProvider;
|
||||
use deltachat::key::DcKey;
|
||||
use deltachat::key::{DcKey, DcSecretKey};
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::message_parser::parser;
|
||||
use deltachat::net::read_url_blob;
|
||||
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::webxdc::{replace_webxdc, StatusUpdateSerial};
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
use deltachat::{accounts::Accounts, log::LogExt};
|
||||
use num_traits::{FromPrimitive, ToPrimitive};
|
||||
@@ -805,7 +806,7 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) {
|
||||
pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
context: *mut dc_context_t,
|
||||
addr: *const libc::c_char,
|
||||
public_data: *const libc::c_char,
|
||||
_public_data: *const libc::c_char,
|
||||
secret_data: *const libc::c_char,
|
||||
) -> i32 {
|
||||
if context.is_null() {
|
||||
@@ -815,8 +816,8 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||
let ctx = &*context;
|
||||
block_on(async move {
|
||||
let addr = tools::EmailAddress::new(&to_string_lossy(addr))?;
|
||||
let public = key::SignedPublicKey::from_asc(&to_string_lossy(public_data))?.0;
|
||||
let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
|
||||
let public = secret.split_public_key()?;
|
||||
let keypair = key::KeyPair {
|
||||
addr,
|
||||
public,
|
||||
@@ -1097,32 +1098,6 @@ pub unsafe extern "C" fn dc_get_webxdc_status_updates(
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_replace_webxdc(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
blob: *const u8,
|
||||
n: libc::size_t,
|
||||
) {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_replace_webxdc()");
|
||||
return;
|
||||
}
|
||||
|
||||
let msg_id = MsgId::new(msg_id);
|
||||
let blob_slice = std::slice::from_raw_parts(blob, n);
|
||||
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
replace_webxdc(ctx, msg_id, blob_slice)
|
||||
.await
|
||||
.context("Failed to replace WebXDC")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_set_draft(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1457,6 +1432,7 @@ pub unsafe extern "C" fn dc_get_chat_media(
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
#[allow(deprecated)]
|
||||
pub unsafe extern "C" fn dc_get_next_media(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
@@ -1565,10 +1541,14 @@ pub unsafe extern "C" fn dc_delete_chat(context: *mut dc_context_t, chat_id: u32
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(ChatId::new(chat_id).delete(ctx))
|
||||
.context("Failed chat delete")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
block_on(async move {
|
||||
ChatId::new(chat_id)
|
||||
.delete(ctx)
|
||||
.await
|
||||
.context("Failed chat delete")
|
||||
.log_err(ctx)
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2589,7 +2569,12 @@ pub unsafe extern "C" fn dc_set_location(
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(location::set(ctx, latitude, longitude, accuracy)) as _
|
||||
block_on(async move {
|
||||
location::set(ctx, latitude, longitude, accuracy)
|
||||
.await
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
}) as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3372,6 +3357,58 @@ pub unsafe extern "C" fn dc_msg_get_text(msg: *mut dc_msg_t) -> *mut libc::c_cha
|
||||
ffi_msg.message.get_text().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_parse_message_text_to_ast_json(
|
||||
text: *const libc::c_char,
|
||||
mode: u32,
|
||||
) -> *mut libc::c_char {
|
||||
if text.is_null() {
|
||||
eprintln!("ignoring careless call to dc_parse_message_text_to_ast_json()");
|
||||
}
|
||||
let text = to_string_lossy(text);
|
||||
let result = match mode {
|
||||
0 /* OnlyText */ => parser::parse_only_text(&text),
|
||||
1 /* DesktopSet */ => parser::parse_desktop_set(&text),
|
||||
2 /* Markdown */ => parser::parse_markdown_text(&text),
|
||||
_ => {
|
||||
eprintln!("ignoring careless call to dc_parse_message_text_to_ast_json() - invalid mode");
|
||||
return "".strdup();
|
||||
}
|
||||
};
|
||||
if let Ok(result) = serde_json::to_string(&result) {
|
||||
result.strdup()
|
||||
} else {
|
||||
"".strdup()
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_parsed_text_as_json(
|
||||
msg: *mut dc_msg_t,
|
||||
mode: u32,
|
||||
) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
eprintln!("ignoring careless call to dc_msg_get_parsed_text_as_json()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_msg = &*msg;
|
||||
let text = ffi_msg.message.get_text();
|
||||
let result = match mode {
|
||||
0 /* OnlyText */ => parser::parse_only_text(&text),
|
||||
1 /* DesktopSet */ => parser::parse_desktop_set(&text),
|
||||
2 /* Markdown */ => parser::parse_markdown_text(&text),
|
||||
_ => {
|
||||
eprintln!("ignoring careless call to dc_msg_get_parsed_text_as_json() - invalid mode");
|
||||
return "".strdup();
|
||||
}
|
||||
};
|
||||
if let Ok(result) = serde_json::to_string(&result) {
|
||||
result.strdup()
|
||||
} else {
|
||||
"".strdup()
|
||||
}
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_msg_get_subject(msg: *mut dc_msg_t) -> *mut libc::c_char {
|
||||
if msg.is_null() {
|
||||
@@ -4553,7 +4590,14 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
|
||||
|
||||
let ctx = &*context;
|
||||
|
||||
match block_on(provider::get_provider_info(ctx, addr.as_str(), true)) {
|
||||
match block_on(provider::get_provider_info_by_addr(
|
||||
ctx,
|
||||
addr.as_str(),
|
||||
true,
|
||||
))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
@@ -4580,11 +4624,14 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
|
||||
|
||||
match socks5_enabled {
|
||||
Ok(socks5_enabled) => {
|
||||
match block_on(provider::get_provider_info(
|
||||
match block_on(provider::get_provider_info_by_addr(
|
||||
ctx,
|
||||
addr.as_str(),
|
||||
socks5_enabled,
|
||||
)) {
|
||||
))
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
Some(provider) => provider,
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
@@ -5032,7 +5079,7 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||
#[cfg(feature = "jsonrpc")]
|
||||
mod jsonrpc {
|
||||
use deltachat_jsonrpc::api::CommandApi;
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcServer, RpcSession};
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -5108,25 +5155,24 @@ mod jsonrpc {
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
|
||||
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
|
||||
method: *const libc::c_char,
|
||||
params: *const libc::c_char,
|
||||
input: *const libc::c_char,
|
||||
) -> *mut libc::c_char {
|
||||
if jsonrpc_instance.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let api = &*jsonrpc_instance;
|
||||
let method = to_string_lossy(method);
|
||||
let params = to_string_lossy(params);
|
||||
let params: Option<yerpc::Params> = match serde_json::from_str(¶ms) {
|
||||
Ok(params) => Some(params),
|
||||
Err(_) => None,
|
||||
};
|
||||
let params = params.map(yerpc::Params::into_value).unwrap_or_default();
|
||||
let res = block_on(api.handle.server().handle_request(method, params));
|
||||
let input = to_string_lossy(input);
|
||||
let res = block_on(api.handle.process_incoming(&input));
|
||||
match res {
|
||||
Ok(res) => res.to_string().strdup(),
|
||||
Err(_) => ptr::null_mut(),
|
||||
Some(message) => {
|
||||
if let Ok(message) = serde_json::to_string(&message) {
|
||||
message.strdup()
|
||||
} else {
|
||||
ptr::null_mut()
|
||||
}
|
||||
}
|
||||
None => ptr::null_mut(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.122.0"
|
||||
version = "1.124.1"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
edition = "2021"
|
||||
default-run = "deltachat-jsonrpc-server"
|
||||
|
||||
@@ -43,11 +43,13 @@ use types::contact::ContactObject;
|
||||
use types::events::Event;
|
||||
use types::http::HttpResponse;
|
||||
use types::message::{MessageData, MessageObject, MessageReadReceipt};
|
||||
use types::message_parser;
|
||||
use types::provider_info::ProviderInfo;
|
||||
use types::reactions::JSONRPCReactions;
|
||||
use types::webxdc::WebxdcMessageInfo;
|
||||
|
||||
use self::types::message::MessageLoadResult;
|
||||
use self::types::message_parser::MessageParserMode;
|
||||
use self::types::{
|
||||
chat::{BasicChat, JSONRPCChatVisibility, MuteDuration},
|
||||
location::JsonrpcLocation,
|
||||
@@ -1081,6 +1083,31 @@ impl CommandApi {
|
||||
MsgId::new(message_id).get_html(&ctx).await
|
||||
}
|
||||
|
||||
// no specific typings because of https://github.com/dbeckwith/rust-typescript-type-def/issues/18
|
||||
async fn get_parsed_message_text_ast_json(
|
||||
&self,
|
||||
account_id: u32,
|
||||
message_id: u32,
|
||||
mode: MessageParserMode,
|
||||
) -> Result<serde_json::Value> {
|
||||
let ctx = self.get_context(account_id).await?;
|
||||
let msg_text = Message::load_from_db(&ctx, MsgId::new(message_id))
|
||||
.await?
|
||||
.get_text();
|
||||
let result = message_parser::parse_text(&msg_text, mode);
|
||||
Ok(serde_json::to_value(result)?)
|
||||
}
|
||||
|
||||
// no specific typings because of https://github.com/dbeckwith/rust-typescript-type-def/issues/18
|
||||
async fn parse_text_to_ast_json(
|
||||
&self,
|
||||
text: String,
|
||||
mode: MessageParserMode,
|
||||
) -> Result<serde_json::Value> {
|
||||
let result = message_parser::parse_text(&text, mode);
|
||||
Ok(serde_json::to_value(result)?)
|
||||
}
|
||||
|
||||
/// get multiple messages in one call,
|
||||
/// if loading one message fails the error is stored in the result object in it's place.
|
||||
///
|
||||
@@ -1427,6 +1454,10 @@ impl CommandApi {
|
||||
///
|
||||
/// one combined call for getting chat::get_next_media for both directions
|
||||
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
|
||||
///
|
||||
/// Deprecated 2023-10-03, use `get_chat_media` method
|
||||
/// and navigate the returned array instead.
|
||||
#[allow(deprecated)]
|
||||
async fn get_neighboring_chat_media(
|
||||
&self,
|
||||
account_id: u32,
|
||||
|
||||
19
deltachat-jsonrpc/src/api/types/message_parser.rs
Normal file
19
deltachat-jsonrpc/src/api/types/message_parser.rs
Normal file
@@ -0,0 +1,19 @@
|
||||
use deltachat::message_parser::parser::{self, Element};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[repr(u8)]
|
||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||
pub enum MessageParserMode {
|
||||
OnlyText,
|
||||
DesktopSet,
|
||||
Markdown,
|
||||
}
|
||||
|
||||
pub fn parse_text(input: &str, mode: MessageParserMode) -> std::vec::Vec<Element> {
|
||||
match mode {
|
||||
MessageParserMode::OnlyText => parser::parse_only_text(input),
|
||||
MessageParserMode::DesktopSet => parser::parse_desktop_set(input),
|
||||
MessageParserMode::Markdown => parser::parse_markdown_text(input),
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ pub mod events;
|
||||
pub mod http;
|
||||
pub mod location;
|
||||
pub mod message;
|
||||
pub mod message_parser;
|
||||
pub mod provider_info;
|
||||
pub mod qr;
|
||||
pub mod reactions;
|
||||
|
||||
@@ -55,5 +55,5 @@
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.122.0"
|
||||
"version": "1.124.1"
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-repl"
|
||||
version = "1.122.0"
|
||||
version = "1.124.1"
|
||||
license = "MPL-2.0"
|
||||
edition = "2021"
|
||||
|
||||
|
||||
@@ -138,11 +138,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
/* import a directory */
|
||||
let dir_name = std::path::Path::new(&real_spec);
|
||||
let dir = fs::read_dir(dir_name).await;
|
||||
if dir.is_err() {
|
||||
error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
|
||||
return false;
|
||||
} else {
|
||||
let mut dir = dir.unwrap();
|
||||
if let Ok(mut dir) = dir {
|
||||
while let Ok(Some(entry)) = dir.next_entry().await {
|
||||
let name_f = entry.file_name();
|
||||
let name = name_f.to_string_lossy();
|
||||
@@ -154,6 +150,9 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
|
||||
@@ -894,7 +893,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
let latitude = arg1.parse()?;
|
||||
let longitude = arg2.parse()?;
|
||||
|
||||
let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
|
||||
let continue_streaming = location::set(&context, latitude, longitude, 0.).await?;
|
||||
if continue_streaming {
|
||||
println!("Success, streaming should be continued.");
|
||||
} else {
|
||||
|
||||
@@ -37,19 +37,14 @@ $ tox --devenv env
|
||||
$ . env/bin/activate
|
||||
```
|
||||
|
||||
It is recommended to use IPython, because it supports using `await` directly
|
||||
from the REPL.
|
||||
|
||||
```
|
||||
$ pip install ipython
|
||||
$ PATH="../target/debug:$PATH" ipython
|
||||
...
|
||||
In [1]: from deltachat_rpc_client import *
|
||||
In [2]: rpc = Rpc()
|
||||
In [3]: await rpc.start()
|
||||
In [4]: dc = DeltaChat(rpc)
|
||||
In [5]: system_info = await dc.get_system_info()
|
||||
In [6]: system_info["level"]
|
||||
Out[6]: 'awesome'
|
||||
In [7]: await rpc.close()
|
||||
$ python
|
||||
>>> from deltachat_rpc_client import *
|
||||
>>> rpc = Rpc()
|
||||
>>> rpc.start()
|
||||
>>> dc = DeltaChat(rpc)
|
||||
>>> system_info = dc.get_system_info()
|
||||
>>> system_info["level"]
|
||||
'awesome'
|
||||
>>> rpc.close()
|
||||
```
|
||||
|
||||
@@ -4,23 +4,21 @@
|
||||
it will echo back any text send to it, it also will print to console all Delta Chat core events.
|
||||
Pass --help to the CLI to see available options.
|
||||
"""
|
||||
import asyncio
|
||||
|
||||
from deltachat_rpc_client import events, run_bot_cli
|
||||
|
||||
hooks = events.HookCollection()
|
||||
|
||||
|
||||
@hooks.on(events.RawEvent)
|
||||
async def log_event(event):
|
||||
def log_event(event):
|
||||
print(event)
|
||||
|
||||
|
||||
@hooks.on(events.NewMessage)
|
||||
async def echo(event):
|
||||
def echo(event):
|
||||
snapshot = event.message_snapshot
|
||||
await snapshot.chat.send_text(snapshot.text)
|
||||
snapshot.chat.send_text(snapshot.text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(run_bot_cli(hooks))
|
||||
run_bot_cli(hooks)
|
||||
|
||||
@@ -3,9 +3,9 @@
|
||||
|
||||
it will echo back any message that has non-empty text and also supports the /help command.
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
from threading import Thread
|
||||
|
||||
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
|
||||
|
||||
@@ -13,7 +13,7 @@ hooks = events.HookCollection()
|
||||
|
||||
|
||||
@hooks.on(events.RawEvent)
|
||||
async def log_event(event):
|
||||
def log_event(event):
|
||||
if event.type == EventType.INFO:
|
||||
logging.info(event.msg)
|
||||
elif event.type == EventType.WARNING:
|
||||
@@ -21,54 +21,54 @@ async def log_event(event):
|
||||
|
||||
|
||||
@hooks.on(events.RawEvent(EventType.ERROR))
|
||||
async def log_error(event):
|
||||
def log_error(event):
|
||||
logging.error(event.msg)
|
||||
|
||||
|
||||
@hooks.on(events.MemberListChanged)
|
||||
async def on_memberlist_changed(event):
|
||||
def on_memberlist_changed(event):
|
||||
logging.info("member %s was %s", event.member, "added" if event.member_added else "removed")
|
||||
|
||||
|
||||
@hooks.on(events.GroupImageChanged)
|
||||
async def on_group_image_changed(event):
|
||||
def on_group_image_changed(event):
|
||||
logging.info("group image %s", "deleted" if event.image_deleted else "changed")
|
||||
|
||||
|
||||
@hooks.on(events.GroupNameChanged)
|
||||
async def on_group_name_changed(event):
|
||||
def on_group_name_changed(event):
|
||||
logging.info("group name changed, old name: %s", event.old_name)
|
||||
|
||||
|
||||
@hooks.on(events.NewMessage(func=lambda e: not e.command))
|
||||
async def echo(event):
|
||||
def echo(event):
|
||||
snapshot = event.message_snapshot
|
||||
if snapshot.text or snapshot.file:
|
||||
await snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
|
||||
snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
|
||||
|
||||
|
||||
@hooks.on(events.NewMessage(command="/help"))
|
||||
async def help_command(event):
|
||||
def help_command(event):
|
||||
snapshot = event.message_snapshot
|
||||
await snapshot.chat.send_text("Send me any message and I will echo it back")
|
||||
snapshot.chat.send_text("Send me any message and I will echo it back")
|
||||
|
||||
|
||||
async def main():
|
||||
async with Rpc() as rpc:
|
||||
def main():
|
||||
with Rpc() as rpc:
|
||||
deltachat = DeltaChat(rpc)
|
||||
system_info = await deltachat.get_system_info()
|
||||
system_info = deltachat.get_system_info()
|
||||
logging.info("Running deltachat core %s", system_info.deltachat_core_version)
|
||||
|
||||
accounts = await deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else await deltachat.add_account()
|
||||
accounts = deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else deltachat.add_account()
|
||||
|
||||
bot = Bot(account, hooks)
|
||||
if not await bot.is_configured():
|
||||
# Save a reference to avoid garbage collection of the task.
|
||||
_configure_task = asyncio.create_task(bot.configure(email=sys.argv[1], password=sys.argv[2]))
|
||||
await bot.run_forever()
|
||||
if not bot.is_configured():
|
||||
configure_thread = Thread(run=bot.configure, kwargs={"email": sys.argv[1], "password": sys.argv[2]})
|
||||
configure_thread.start()
|
||||
bot.run_forever()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
asyncio.run(main())
|
||||
main()
|
||||
|
||||
@@ -2,45 +2,44 @@
|
||||
"""
|
||||
Example echo bot without using hooks
|
||||
"""
|
||||
import asyncio
|
||||
import logging
|
||||
import sys
|
||||
|
||||
from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId
|
||||
|
||||
|
||||
async def main():
|
||||
async with Rpc() as rpc:
|
||||
def main():
|
||||
with Rpc() as rpc:
|
||||
deltachat = DeltaChat(rpc)
|
||||
system_info = await deltachat.get_system_info()
|
||||
system_info = deltachat.get_system_info()
|
||||
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
|
||||
|
||||
accounts = await deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else await deltachat.add_account()
|
||||
accounts = deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else deltachat.add_account()
|
||||
|
||||
await account.set_config("bot", "1")
|
||||
if not await account.is_configured():
|
||||
account.set_config("bot", "1")
|
||||
if not account.is_configured():
|
||||
logging.info("Account is not configured, configuring")
|
||||
await account.set_config("addr", sys.argv[1])
|
||||
await account.set_config("mail_pw", sys.argv[2])
|
||||
await account.configure()
|
||||
account.set_config("addr", sys.argv[1])
|
||||
account.set_config("mail_pw", sys.argv[2])
|
||||
account.configure()
|
||||
logging.info("Configured")
|
||||
else:
|
||||
logging.info("Account is already configured")
|
||||
await deltachat.start_io()
|
||||
deltachat.start_io()
|
||||
|
||||
async def process_messages():
|
||||
for message in await account.get_next_messages():
|
||||
snapshot = await message.get_snapshot()
|
||||
def process_messages():
|
||||
for message in account.get_next_messages():
|
||||
snapshot = message.get_snapshot()
|
||||
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
|
||||
await snapshot.chat.send_text(snapshot.text)
|
||||
await snapshot.message.mark_seen()
|
||||
snapshot.chat.send_text(snapshot.text)
|
||||
snapshot.message.mark_seen()
|
||||
|
||||
# Process old messages.
|
||||
await process_messages()
|
||||
process_messages()
|
||||
|
||||
while True:
|
||||
event = await account.wait_for_event()
|
||||
event = account.wait_for_event()
|
||||
if event["type"] == EventType.INFO:
|
||||
logging.info("%s", event["msg"])
|
||||
elif event["type"] == EventType.WARNING:
|
||||
@@ -49,9 +48,9 @@ async def main():
|
||||
logging.error("%s", event["msg"])
|
||||
elif event["type"] == EventType.INCOMING_MSG:
|
||||
logging.info("Got an incoming message")
|
||||
await process_messages()
|
||||
process_messages()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
asyncio.run(main())
|
||||
main()
|
||||
|
||||
@@ -5,12 +5,8 @@ build-backend = "setuptools.build_meta"
|
||||
[project]
|
||||
name = "deltachat-rpc-client"
|
||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||
dependencies = [
|
||||
"aiohttp"
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Framework :: AsyncIO",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||
"Operating System :: POSIX :: Linux",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Delta Chat asynchronous high-level API"""
|
||||
"""Delta Chat JSON-RPC high-level API"""
|
||||
from ._utils import AttrDict, run_bot_cli, run_client_cli
|
||||
from .account import Account
|
||||
from .chat import Chat
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import re
|
||||
import sys
|
||||
from threading import Thread
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -43,7 +43,7 @@ class AttrDict(dict):
|
||||
super().__setattr__(attr, val)
|
||||
|
||||
|
||||
async def run_client_cli(
|
||||
def run_client_cli(
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs,
|
||||
@@ -54,10 +54,10 @@ async def run_client_cli(
|
||||
"""
|
||||
from .client import Client
|
||||
|
||||
await _run_cli(Client, hooks, argv, **kwargs)
|
||||
_run_cli(Client, hooks, argv, **kwargs)
|
||||
|
||||
|
||||
async def run_bot_cli(
|
||||
def run_bot_cli(
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
**kwargs,
|
||||
@@ -68,10 +68,10 @@ async def run_bot_cli(
|
||||
"""
|
||||
from .client import Bot
|
||||
|
||||
await _run_cli(Bot, hooks, argv, **kwargs)
|
||||
_run_cli(Bot, hooks, argv, **kwargs)
|
||||
|
||||
|
||||
async def _run_cli(
|
||||
def _run_cli(
|
||||
client_type: Type["Client"],
|
||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||
argv: Optional[list] = None,
|
||||
@@ -93,20 +93,20 @@ async def _run_cli(
|
||||
parser.add_argument("--password", action="store", help="password")
|
||||
args = parser.parse_args(argv[1:])
|
||||
|
||||
async with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
|
||||
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
|
||||
deltachat = DeltaChat(rpc)
|
||||
core_version = (await deltachat.get_system_info()).deltachat_core_version
|
||||
accounts = await deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else await deltachat.add_account()
|
||||
core_version = (deltachat.get_system_info()).deltachat_core_version
|
||||
accounts = deltachat.get_all_accounts()
|
||||
account = accounts[0] if accounts else deltachat.add_account()
|
||||
|
||||
client = client_type(account, hooks)
|
||||
client.logger.debug("Running deltachat core %s", core_version)
|
||||
if not await client.is_configured():
|
||||
if not client.is_configured():
|
||||
assert args.email, "Account is not configured and email must be provided"
|
||||
assert args.password, "Account is not configured and password must be provided"
|
||||
# Save a reference to avoid garbage collection of the task.
|
||||
_configure_task = asyncio.create_task(client.configure(email=args.email, password=args.password))
|
||||
await client.run_forever()
|
||||
configure_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password})
|
||||
configure_thread.start()
|
||||
client.run_forever()
|
||||
|
||||
|
||||
def extract_addr(text: str) -> str:
|
||||
|
||||
@@ -24,63 +24,63 @@ class Account:
|
||||
def _rpc(self) -> "Rpc":
|
||||
return self.manager.rpc
|
||||
|
||||
async def wait_for_event(self) -> AttrDict:
|
||||
def wait_for_event(self) -> AttrDict:
|
||||
"""Wait until the next event and return it."""
|
||||
return AttrDict(await self._rpc.wait_for_event(self.id))
|
||||
return AttrDict(self._rpc.wait_for_event(self.id))
|
||||
|
||||
async def remove(self) -> None:
|
||||
def remove(self) -> None:
|
||||
"""Remove the account."""
|
||||
await self._rpc.remove_account(self.id)
|
||||
self._rpc.remove_account(self.id)
|
||||
|
||||
async def start_io(self) -> None:
|
||||
def start_io(self) -> None:
|
||||
"""Start the account I/O."""
|
||||
await self._rpc.start_io(self.id)
|
||||
self._rpc.start_io(self.id)
|
||||
|
||||
async def stop_io(self) -> None:
|
||||
def stop_io(self) -> None:
|
||||
"""Stop the account I/O."""
|
||||
await self._rpc.stop_io(self.id)
|
||||
self._rpc.stop_io(self.id)
|
||||
|
||||
async def get_info(self) -> AttrDict:
|
||||
def get_info(self) -> AttrDict:
|
||||
"""Return dictionary of this account configuration parameters."""
|
||||
return AttrDict(await self._rpc.get_info(self.id))
|
||||
return AttrDict(self._rpc.get_info(self.id))
|
||||
|
||||
async def get_size(self) -> int:
|
||||
def get_size(self) -> int:
|
||||
"""Get the combined filesize of an account in bytes."""
|
||||
return await self._rpc.get_account_file_size(self.id)
|
||||
return self._rpc.get_account_file_size(self.id)
|
||||
|
||||
async def is_configured(self) -> bool:
|
||||
def is_configured(self) -> bool:
|
||||
"""Return True if this account is configured."""
|
||||
return await self._rpc.is_configured(self.id)
|
||||
return self._rpc.is_configured(self.id)
|
||||
|
||||
async def set_config(self, key: str, value: Optional[str] = None) -> None:
|
||||
def set_config(self, key: str, value: Optional[str] = None) -> None:
|
||||
"""Set configuration value."""
|
||||
await self._rpc.set_config(self.id, key, value)
|
||||
self._rpc.set_config(self.id, key, value)
|
||||
|
||||
async def get_config(self, key: str) -> Optional[str]:
|
||||
def get_config(self, key: str) -> Optional[str]:
|
||||
"""Get configuration value."""
|
||||
return await self._rpc.get_config(self.id, key)
|
||||
return self._rpc.get_config(self.id, key)
|
||||
|
||||
async def update_config(self, **kwargs) -> None:
|
||||
def update_config(self, **kwargs) -> None:
|
||||
"""update config values."""
|
||||
for key, value in kwargs.items():
|
||||
await self.set_config(key, value)
|
||||
self.set_config(key, value)
|
||||
|
||||
async def set_avatar(self, img_path: Optional[str] = None) -> None:
|
||||
def set_avatar(self, img_path: Optional[str] = None) -> None:
|
||||
"""Set self avatar.
|
||||
|
||||
Passing None will discard the currently set avatar.
|
||||
"""
|
||||
await self.set_config("selfavatar", img_path)
|
||||
self.set_config("selfavatar", img_path)
|
||||
|
||||
async def get_avatar(self) -> Optional[str]:
|
||||
def get_avatar(self) -> Optional[str]:
|
||||
"""Get self avatar."""
|
||||
return await self.get_config("selfavatar")
|
||||
return self.get_config("selfavatar")
|
||||
|
||||
async def configure(self) -> None:
|
||||
def configure(self) -> None:
|
||||
"""Configure an account."""
|
||||
await self._rpc.configure(self.id)
|
||||
self._rpc.configure(self.id)
|
||||
|
||||
async def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
||||
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
||||
"""Create a new Contact or return an existing one.
|
||||
|
||||
Calling this method will always result in the same
|
||||
@@ -94,24 +94,24 @@ class Account:
|
||||
if isinstance(obj, int):
|
||||
obj = Contact(self, obj)
|
||||
if isinstance(obj, Contact):
|
||||
obj = (await obj.get_snapshot()).address
|
||||
return Contact(self, await self._rpc.create_contact(self.id, obj, name))
|
||||
obj = obj.get_snapshot().address
|
||||
return Contact(self, self._rpc.create_contact(self.id, obj, name))
|
||||
|
||||
def get_contact_by_id(self, contact_id: int) -> Contact:
|
||||
"""Return Contact instance for the given contact ID."""
|
||||
return Contact(self, contact_id)
|
||||
|
||||
async def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
||||
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
||||
"""Check if an e-mail address belongs to a known and unblocked contact."""
|
||||
contact_id = await self._rpc.lookup_contact_id_by_addr(self.id, address)
|
||||
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
||||
return contact_id and Contact(self, contact_id)
|
||||
|
||||
async def get_blocked_contacts(self) -> List[AttrDict]:
|
||||
def get_blocked_contacts(self) -> List[AttrDict]:
|
||||
"""Return a list with snapshots of all blocked contacts."""
|
||||
contacts = await self._rpc.get_blocked_contacts(self.id)
|
||||
contacts = self._rpc.get_blocked_contacts(self.id)
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
|
||||
async def get_contacts(
|
||||
def get_contacts(
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
with_self: bool = False,
|
||||
@@ -133,9 +133,9 @@ class Account:
|
||||
flags |= ContactFlag.ADD_SELF
|
||||
|
||||
if snapshot:
|
||||
contacts = await self._rpc.get_contacts(self.id, flags, query)
|
||||
contacts = self._rpc.get_contacts(self.id, flags, query)
|
||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||
contacts = await self._rpc.get_contact_ids(self.id, flags, query)
|
||||
contacts = self._rpc.get_contact_ids(self.id, flags, query)
|
||||
return [Contact(self, contact_id) for contact_id in contacts]
|
||||
|
||||
@property
|
||||
@@ -143,7 +143,7 @@ class Account:
|
||||
"""This account's identity as a Contact."""
|
||||
return Contact(self, SpecialContactId.SELF)
|
||||
|
||||
async def get_chatlist(
|
||||
def get_chatlist(
|
||||
self,
|
||||
query: Optional[str] = None,
|
||||
contact: Optional[Contact] = None,
|
||||
@@ -175,29 +175,29 @@ class Account:
|
||||
if alldone_hint:
|
||||
flags |= ChatlistFlag.ADD_ALLDONE_HINT
|
||||
|
||||
entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
|
||||
entries = self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
|
||||
if not snapshot:
|
||||
return [Chat(self, entry) for entry in entries]
|
||||
|
||||
items = await self._rpc.get_chatlist_items_by_entries(self.id, entries)
|
||||
items = self._rpc.get_chatlist_items_by_entries(self.id, entries)
|
||||
chats = []
|
||||
for item in items.values():
|
||||
item["chat"] = Chat(self, item["id"])
|
||||
chats.append(AttrDict(item))
|
||||
return chats
|
||||
|
||||
async def create_group(self, name: str, protect: bool = False) -> Chat:
|
||||
def create_group(self, name: str, protect: bool = False) -> Chat:
|
||||
"""Create a new group chat.
|
||||
|
||||
After creation, the group has only self-contact as member and is in unpromoted state.
|
||||
"""
|
||||
return Chat(self, await self._rpc.create_group_chat(self.id, name, protect))
|
||||
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
|
||||
|
||||
def get_chat_by_id(self, chat_id: int) -> Chat:
|
||||
"""Return the Chat instance with the given ID."""
|
||||
return Chat(self, chat_id)
|
||||
|
||||
async def secure_join(self, qrdata: str) -> Chat:
|
||||
def secure_join(self, qrdata: str) -> Chat:
|
||||
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on
|
||||
another device.
|
||||
|
||||
@@ -208,62 +208,62 @@ class Account:
|
||||
|
||||
:param qrdata: The text of the scanned QR code.
|
||||
"""
|
||||
return Chat(self, await self._rpc.secure_join(self.id, qrdata))
|
||||
return Chat(self, self._rpc.secure_join(self.id, qrdata))
|
||||
|
||||
async def get_qr_code(self) -> Tuple[str, str]:
|
||||
def get_qr_code(self) -> Tuple[str, str]:
|
||||
"""Get Setup-Contact QR Code text and SVG data.
|
||||
|
||||
this data needs to be transferred to another Delta Chat account
|
||||
in a second channel, typically used by mobiles with QRcode-show + scan UX.
|
||||
"""
|
||||
return await self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
|
||||
return self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
|
||||
|
||||
def get_message_by_id(self, msg_id: int) -> Message:
|
||||
"""Return the Message instance with the given ID."""
|
||||
return Message(self, msg_id)
|
||||
|
||||
async def mark_seen_messages(self, messages: List[Message]) -> None:
|
||||
def mark_seen_messages(self, messages: List[Message]) -> None:
|
||||
"""Mark the given set of messages as seen."""
|
||||
await self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
|
||||
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
|
||||
|
||||
async def delete_messages(self, messages: List[Message]) -> None:
|
||||
def delete_messages(self, messages: List[Message]) -> None:
|
||||
"""Delete messages (local and remote)."""
|
||||
await self._rpc.delete_messages(self.id, [msg.id for msg in messages])
|
||||
self._rpc.delete_messages(self.id, [msg.id for msg in messages])
|
||||
|
||||
async def get_fresh_messages(self) -> List[Message]:
|
||||
def get_fresh_messages(self) -> List[Message]:
|
||||
"""Return the list of fresh messages, newest messages first.
|
||||
|
||||
This call is intended for displaying notifications.
|
||||
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
|
||||
to process oldest messages first.
|
||||
"""
|
||||
fresh_msg_ids = await self._rpc.get_fresh_msgs(self.id)
|
||||
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
|
||||
async def get_next_messages(self) -> List[Message]:
|
||||
def get_next_messages(self) -> List[Message]:
|
||||
"""Return a list of next messages."""
|
||||
next_msg_ids = await self._rpc.get_next_msgs(self.id)
|
||||
next_msg_ids = self._rpc.get_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
async def wait_next_messages(self) -> List[Message]:
|
||||
def wait_next_messages(self) -> List[Message]:
|
||||
"""Wait for new messages and return a list of them."""
|
||||
next_msg_ids = await self._rpc.wait_next_msgs(self.id)
|
||||
next_msg_ids = self._rpc.wait_next_msgs(self.id)
|
||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||
|
||||
async def get_fresh_messages_in_arrival_order(self) -> List[Message]:
|
||||
def get_fresh_messages_in_arrival_order(self) -> List[Message]:
|
||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||
warn(
|
||||
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
|
||||
DeprecationWarning,
|
||||
stacklevel=2,
|
||||
)
|
||||
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id))
|
||||
fresh_msg_ids = sorted(self._rpc.get_fresh_msgs(self.id))
|
||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||
|
||||
async def export_backup(self, path, passphrase: str = "") -> None:
|
||||
def export_backup(self, path, passphrase: str = "") -> None:
|
||||
"""Export backup."""
|
||||
await self._rpc.export_backup(self.id, str(path), passphrase)
|
||||
self._rpc.export_backup(self.id, str(path), passphrase)
|
||||
|
||||
async def import_backup(self, path, passphrase: str = "") -> None:
|
||||
def import_backup(self, path, passphrase: str = "") -> None:
|
||||
"""Import backup."""
|
||||
await self._rpc.import_backup(self.id, str(path), passphrase)
|
||||
self._rpc.import_backup(self.id, str(path), passphrase)
|
||||
|
||||
@@ -25,7 +25,7 @@ class Chat:
|
||||
def _rpc(self) -> "Rpc":
|
||||
return self.account._rpc
|
||||
|
||||
async def delete(self) -> None:
|
||||
def delete(self) -> None:
|
||||
"""Delete this chat and all its messages.
|
||||
|
||||
Note:
|
||||
@@ -33,21 +33,21 @@ class Chat:
|
||||
- does not delete messages on server
|
||||
- the chat or contact is not blocked, new message will arrive
|
||||
"""
|
||||
await self._rpc.delete_chat(self.account.id, self.id)
|
||||
self._rpc.delete_chat(self.account.id, self.id)
|
||||
|
||||
async def block(self) -> None:
|
||||
def block(self) -> None:
|
||||
"""Block this chat."""
|
||||
await self._rpc.block_chat(self.account.id, self.id)
|
||||
self._rpc.block_chat(self.account.id, self.id)
|
||||
|
||||
async def accept(self) -> None:
|
||||
def accept(self) -> None:
|
||||
"""Accept this contact request chat."""
|
||||
await self._rpc.accept_chat(self.account.id, self.id)
|
||||
self._rpc.accept_chat(self.account.id, self.id)
|
||||
|
||||
async def leave(self) -> None:
|
||||
def leave(self) -> None:
|
||||
"""Leave this chat."""
|
||||
await self._rpc.leave_group(self.account.id, self.id)
|
||||
self._rpc.leave_group(self.account.id, self.id)
|
||||
|
||||
async def mute(self, duration: Optional[int] = None) -> None:
|
||||
def mute(self, duration: Optional[int] = None) -> None:
|
||||
"""Mute this chat, if a duration is not provided the chat is muted forever.
|
||||
|
||||
:param duration: mute duration from now in seconds. Must be greater than zero.
|
||||
@@ -57,59 +57,59 @@ class Chat:
|
||||
dur: Union[str, dict] = {"Until": duration}
|
||||
else:
|
||||
dur = "Forever"
|
||||
await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
|
||||
self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
|
||||
|
||||
async def unmute(self) -> None:
|
||||
def unmute(self) -> None:
|
||||
"""Unmute this chat."""
|
||||
await self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
|
||||
self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
|
||||
|
||||
async def pin(self) -> None:
|
||||
def pin(self) -> None:
|
||||
"""Pin this chat."""
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
|
||||
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
|
||||
|
||||
async def unpin(self) -> None:
|
||||
def unpin(self) -> None:
|
||||
"""Unpin this chat."""
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||
|
||||
async def archive(self) -> None:
|
||||
def archive(self) -> None:
|
||||
"""Archive this chat."""
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
|
||||
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
|
||||
|
||||
async def unarchive(self) -> None:
|
||||
def unarchive(self) -> None:
|
||||
"""Unarchive this chat."""
|
||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||
|
||||
async def set_name(self, name: str) -> None:
|
||||
def set_name(self, name: str) -> None:
|
||||
"""Set name of this chat."""
|
||||
await self._rpc.set_chat_name(self.account.id, self.id, name)
|
||||
self._rpc.set_chat_name(self.account.id, self.id, name)
|
||||
|
||||
async def set_ephemeral_timer(self, timer: int) -> None:
|
||||
def set_ephemeral_timer(self, timer: int) -> None:
|
||||
"""Set ephemeral timer of this chat."""
|
||||
await self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
||||
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
||||
|
||||
async def get_encryption_info(self) -> str:
|
||||
def get_encryption_info(self) -> str:
|
||||
"""Return encryption info for this chat."""
|
||||
return await self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
||||
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
||||
|
||||
async def get_qr_code(self) -> Tuple[str, str]:
|
||||
def get_qr_code(self) -> Tuple[str, str]:
|
||||
"""Get Join-Group QR code text and SVG data."""
|
||||
return await self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
|
||||
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
|
||||
|
||||
async def get_basic_snapshot(self) -> AttrDict:
|
||||
def get_basic_snapshot(self) -> AttrDict:
|
||||
"""Get a chat snapshot with basic info about this chat."""
|
||||
info = await self._rpc.get_basic_chat_info(self.account.id, self.id)
|
||||
info = self._rpc.get_basic_chat_info(self.account.id, self.id)
|
||||
return AttrDict(chat=self, **info)
|
||||
|
||||
async def get_full_snapshot(self) -> AttrDict:
|
||||
def get_full_snapshot(self) -> AttrDict:
|
||||
"""Get a full snapshot of this chat."""
|
||||
info = await self._rpc.get_full_chat_by_id(self.account.id, self.id)
|
||||
info = self._rpc.get_full_chat_by_id(self.account.id, self.id)
|
||||
return AttrDict(chat=self, **info)
|
||||
|
||||
async def can_send(self) -> bool:
|
||||
def can_send(self) -> bool:
|
||||
"""Return true if messages can be sent to the chat."""
|
||||
return await self._rpc.can_send(self.account.id, self.id)
|
||||
return self._rpc.can_send(self.account.id, self.id)
|
||||
|
||||
async def send_message(
|
||||
def send_message(
|
||||
self,
|
||||
text: Optional[str] = None,
|
||||
html: Optional[str] = None,
|
||||
@@ -132,30 +132,30 @@ class Chat:
|
||||
"overrideSenderName": override_sender_name,
|
||||
"quotedMessageId": quoted_msg,
|
||||
}
|
||||
msg_id = await self._rpc.send_msg(self.account.id, self.id, draft)
|
||||
msg_id = self._rpc.send_msg(self.account.id, self.id, draft)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
async def send_text(self, text: str) -> Message:
|
||||
def send_text(self, text: str) -> Message:
|
||||
"""Send a text message and return the resulting Message instance."""
|
||||
msg_id = await self._rpc.misc_send_text_message(self.account.id, self.id, text)
|
||||
msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
async def send_videochat_invitation(self) -> Message:
|
||||
def send_videochat_invitation(self) -> Message:
|
||||
"""Send a videochat invitation and return the resulting Message instance."""
|
||||
msg_id = await self._rpc.send_videochat_invitation(self.account.id, self.id)
|
||||
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
async def send_sticker(self, path: str) -> Message:
|
||||
def send_sticker(self, path: str) -> Message:
|
||||
"""Send an sticker and return the resulting Message instance."""
|
||||
msg_id = await self._rpc.send_sticker(self.account.id, self.id, path)
|
||||
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
||||
return Message(self.account, msg_id)
|
||||
|
||||
async def forward_messages(self, messages: List[Message]) -> None:
|
||||
def forward_messages(self, messages: List[Message]) -> None:
|
||||
"""Forward a list of messages to this chat."""
|
||||
msg_ids = [msg.id for msg in messages]
|
||||
await self._rpc.forward_messages(self.account.id, msg_ids, self.id)
|
||||
self._rpc.forward_messages(self.account.id, msg_ids, self.id)
|
||||
|
||||
async def set_draft(
|
||||
def set_draft(
|
||||
self,
|
||||
text: Optional[str] = None,
|
||||
file: Optional[str] = None,
|
||||
@@ -164,15 +164,15 @@ class Chat:
|
||||
"""Set draft message."""
|
||||
if isinstance(quoted_msg, Message):
|
||||
quoted_msg = quoted_msg.id
|
||||
await self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
|
||||
self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
|
||||
|
||||
async def remove_draft(self) -> None:
|
||||
def remove_draft(self) -> None:
|
||||
"""Remove draft message."""
|
||||
await self._rpc.remove_draft(self.account.id, self.id)
|
||||
self._rpc.remove_draft(self.account.id, self.id)
|
||||
|
||||
async def get_draft(self) -> Optional[AttrDict]:
|
||||
def get_draft(self) -> Optional[AttrDict]:
|
||||
"""Get draft message."""
|
||||
snapshot = await self._rpc.get_draft(self.account.id, self.id)
|
||||
snapshot = self._rpc.get_draft(self.account.id, self.id)
|
||||
if not snapshot:
|
||||
return None
|
||||
snapshot = AttrDict(snapshot)
|
||||
@@ -181,61 +181,61 @@ class Chat:
|
||||
snapshot["message"] = Message(self.account, snapshot.id)
|
||||
return snapshot
|
||||
|
||||
async def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
|
||||
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
|
||||
"""get the list of messages in this chat."""
|
||||
msgs = await self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
||||
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
||||
return [Message(self.account, msg_id) for msg_id in msgs]
|
||||
|
||||
async def get_fresh_message_count(self) -> int:
|
||||
def get_fresh_message_count(self) -> int:
|
||||
"""Get number of fresh messages in this chat"""
|
||||
return await self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
||||
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
||||
|
||||
async def mark_noticed(self) -> None:
|
||||
def mark_noticed(self) -> None:
|
||||
"""Mark all messages in this chat as noticed."""
|
||||
await self._rpc.marknoticed_chat(self.account.id, self.id)
|
||||
self._rpc.marknoticed_chat(self.account.id, self.id)
|
||||
|
||||
async def add_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||
def add_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||
"""Add contacts to this group."""
|
||||
for cnt in contact:
|
||||
if isinstance(cnt, str):
|
||||
contact_id = (await self.account.create_contact(cnt)).id
|
||||
contact_id = self.account.create_contact(cnt).id
|
||||
elif not isinstance(cnt, int):
|
||||
contact_id = cnt.id
|
||||
else:
|
||||
contact_id = cnt
|
||||
await self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
|
||||
self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
|
||||
|
||||
async def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||
def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||
"""Remove members from this group."""
|
||||
for cnt in contact:
|
||||
if isinstance(cnt, str):
|
||||
contact_id = (await self.account.create_contact(cnt)).id
|
||||
contact_id = self.account.create_contact(cnt).id
|
||||
elif not isinstance(cnt, int):
|
||||
contact_id = cnt.id
|
||||
else:
|
||||
contact_id = cnt
|
||||
await self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
|
||||
self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
|
||||
|
||||
async def get_contacts(self) -> List[Contact]:
|
||||
def get_contacts(self) -> List[Contact]:
|
||||
"""Get the contacts belonging to this chat.
|
||||
|
||||
For single/direct chats self-address is not included.
|
||||
"""
|
||||
contacts = await self._rpc.get_chat_contacts(self.account.id, self.id)
|
||||
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
|
||||
return [Contact(self.account, contact_id) for contact_id in contacts]
|
||||
|
||||
async def set_image(self, path: str) -> None:
|
||||
def set_image(self, path: str) -> None:
|
||||
"""Set profile image of this chat.
|
||||
|
||||
:param path: Full path of the image to use as the group image.
|
||||
"""
|
||||
await self._rpc.set_chat_profile_image(self.account.id, self.id, path)
|
||||
self._rpc.set_chat_profile_image(self.account.id, self.id, path)
|
||||
|
||||
async def remove_image(self) -> None:
|
||||
def remove_image(self) -> None:
|
||||
"""Remove profile image of this chat."""
|
||||
await self._rpc.set_chat_profile_image(self.account.id, self.id, None)
|
||||
self._rpc.set_chat_profile_image(self.account.id, self.id, None)
|
||||
|
||||
async def get_locations(
|
||||
def get_locations(
|
||||
self,
|
||||
contact: Optional[Contact] = None,
|
||||
timestamp_from: Optional["datetime"] = None,
|
||||
@@ -246,7 +246,7 @@ class Chat:
|
||||
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
|
||||
contact_id = contact.id if contact else 0
|
||||
|
||||
result = await self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
|
||||
result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
|
||||
locations = []
|
||||
contacts: Dict[int, Contact] = {}
|
||||
for loc in result:
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""Event loop implementations offering high level event handling/hooking."""
|
||||
import inspect
|
||||
import logging
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -78,22 +77,22 @@ class Client:
|
||||
)
|
||||
self._hooks.get(type(event), set()).remove((hook, event))
|
||||
|
||||
async def is_configured(self) -> bool:
|
||||
return await self.account.is_configured()
|
||||
def is_configured(self) -> bool:
|
||||
return self.account.is_configured()
|
||||
|
||||
async def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
await self.account.set_config("addr", email)
|
||||
await self.account.set_config("mail_pw", password)
|
||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
self.account.set_config("addr", email)
|
||||
self.account.set_config("mail_pw", password)
|
||||
for key, value in kwargs.items():
|
||||
await self.account.set_config(key, value)
|
||||
await self.account.configure()
|
||||
self.account.set_config(key, value)
|
||||
self.account.configure()
|
||||
self.logger.debug("Account configured")
|
||||
|
||||
async def run_forever(self) -> None:
|
||||
def run_forever(self) -> None:
|
||||
"""Process events forever."""
|
||||
await self.run_until(lambda _: False)
|
||||
self.run_until(lambda _: False)
|
||||
|
||||
async def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
|
||||
def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
|
||||
"""Process events until the given callable evaluates to True.
|
||||
|
||||
The callable should accept an AttrDict object representing the
|
||||
@@ -101,39 +100,37 @@ class Client:
|
||||
evaluates to True.
|
||||
"""
|
||||
self.logger.debug("Listening to incoming events...")
|
||||
if await self.is_configured():
|
||||
await self.account.start_io()
|
||||
await self._process_messages() # Process old messages.
|
||||
if self.is_configured():
|
||||
self.account.start_io()
|
||||
self._process_messages() # Process old messages.
|
||||
while True:
|
||||
event = await self.account.wait_for_event()
|
||||
event = self.account.wait_for_event()
|
||||
event["type"] = EventType(event.type)
|
||||
event["account"] = self.account
|
||||
await self._on_event(event)
|
||||
self._on_event(event)
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
await self._process_messages()
|
||||
self._process_messages()
|
||||
|
||||
stop = func(event)
|
||||
if inspect.isawaitable(stop):
|
||||
stop = await stop
|
||||
if stop:
|
||||
return event
|
||||
|
||||
async def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
|
||||
def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
|
||||
for hook, evfilter in self._hooks.get(filter_type, []):
|
||||
if await evfilter.filter(event):
|
||||
if evfilter.filter(event):
|
||||
try:
|
||||
await hook(event)
|
||||
hook(event)
|
||||
except Exception as ex:
|
||||
self.logger.exception(ex)
|
||||
|
||||
async def _parse_command(self, event: AttrDict) -> None:
|
||||
def _parse_command(self, event: AttrDict) -> None:
|
||||
cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
|
||||
parts = event.message_snapshot.text.split(maxsplit=1)
|
||||
payload = parts[1] if len(parts) > 1 else ""
|
||||
cmd = parts.pop(0)
|
||||
|
||||
if "@" in cmd:
|
||||
suffix = "@" + (await self.account.self_contact.get_snapshot()).address
|
||||
suffix = "@" + self.account.self_contact.get_snapshot().address
|
||||
if cmd.endswith(suffix):
|
||||
cmd = cmd[: -len(suffix)]
|
||||
else:
|
||||
@@ -153,32 +150,32 @@ class Client:
|
||||
|
||||
event["command"], event["payload"] = cmd, payload
|
||||
|
||||
async def _on_new_msg(self, snapshot: AttrDict) -> None:
|
||||
def _on_new_msg(self, snapshot: AttrDict) -> None:
|
||||
event = AttrDict(command="", payload="", message_snapshot=snapshot)
|
||||
if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX):
|
||||
await self._parse_command(event)
|
||||
await self._on_event(event, NewMessage)
|
||||
self._parse_command(event)
|
||||
self._on_event(event, NewMessage)
|
||||
|
||||
async def _handle_info_msg(self, snapshot: AttrDict) -> None:
|
||||
def _handle_info_msg(self, snapshot: AttrDict) -> None:
|
||||
event = AttrDict(message_snapshot=snapshot)
|
||||
|
||||
img_changed = parse_system_image_changed(snapshot.text)
|
||||
if img_changed:
|
||||
_, event["image_deleted"] = img_changed
|
||||
await self._on_event(event, GroupImageChanged)
|
||||
self._on_event(event, GroupImageChanged)
|
||||
return
|
||||
|
||||
title_changed = parse_system_title_changed(snapshot.text)
|
||||
if title_changed:
|
||||
_, event["old_name"] = title_changed
|
||||
await self._on_event(event, GroupNameChanged)
|
||||
self._on_event(event, GroupNameChanged)
|
||||
return
|
||||
|
||||
members_changed = parse_system_add_remove(snapshot.text)
|
||||
if members_changed:
|
||||
action, event["member"], _ = members_changed
|
||||
event["member_added"] = action == "added"
|
||||
await self._on_event(event, MemberListChanged)
|
||||
self._on_event(event, MemberListChanged)
|
||||
return
|
||||
|
||||
self.logger.warning(
|
||||
@@ -187,20 +184,20 @@ class Client:
|
||||
snapshot.text,
|
||||
)
|
||||
|
||||
async def _process_messages(self) -> None:
|
||||
def _process_messages(self) -> None:
|
||||
if self._should_process_messages:
|
||||
for message in await self.account.get_next_messages():
|
||||
snapshot = await message.get_snapshot()
|
||||
for message in self.account.get_next_messages():
|
||||
snapshot = message.get_snapshot()
|
||||
if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]:
|
||||
await self._on_new_msg(snapshot)
|
||||
self._on_new_msg(snapshot)
|
||||
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
|
||||
await self._handle_info_msg(snapshot)
|
||||
await snapshot.message.mark_seen()
|
||||
self._handle_info_msg(snapshot)
|
||||
snapshot.message.mark_seen()
|
||||
|
||||
|
||||
class Bot(Client):
|
||||
"""Simple bot implementation that listent to events of a single account."""
|
||||
|
||||
async def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||
kwargs.setdefault("bot", "1")
|
||||
await super().configure(email, password, **kwargs)
|
||||
super().configure(email, password, **kwargs)
|
||||
|
||||
@@ -24,39 +24,39 @@ class Contact:
|
||||
def _rpc(self) -> "Rpc":
|
||||
return self.account._rpc
|
||||
|
||||
async def block(self) -> None:
|
||||
def block(self) -> None:
|
||||
"""Block contact."""
|
||||
await self._rpc.block_contact(self.account.id, self.id)
|
||||
self._rpc.block_contact(self.account.id, self.id)
|
||||
|
||||
async def unblock(self) -> None:
|
||||
def unblock(self) -> None:
|
||||
"""Unblock contact."""
|
||||
await self._rpc.unblock_contact(self.account.id, self.id)
|
||||
self._rpc.unblock_contact(self.account.id, self.id)
|
||||
|
||||
async def delete(self) -> None:
|
||||
def delete(self) -> None:
|
||||
"""Delete contact."""
|
||||
await self._rpc.delete_contact(self.account.id, self.id)
|
||||
self._rpc.delete_contact(self.account.id, self.id)
|
||||
|
||||
async def set_name(self, name: str) -> None:
|
||||
def set_name(self, name: str) -> None:
|
||||
"""Change the name of this contact."""
|
||||
await self._rpc.change_contact_name(self.account.id, self.id, name)
|
||||
self._rpc.change_contact_name(self.account.id, self.id, name)
|
||||
|
||||
async def get_encryption_info(self) -> str:
|
||||
def get_encryption_info(self) -> str:
|
||||
"""Get a multi-line encryption info, containing your fingerprint and
|
||||
the fingerprint of the contact.
|
||||
"""
|
||||
return await self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
||||
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
||||
|
||||
async def get_snapshot(self) -> AttrDict:
|
||||
def get_snapshot(self) -> AttrDict:
|
||||
"""Return a dictionary with a snapshot of all contact properties."""
|
||||
snapshot = AttrDict(await self._rpc.get_contact(self.account.id, self.id))
|
||||
snapshot = AttrDict(self._rpc.get_contact(self.account.id, self.id))
|
||||
snapshot["contact"] = self
|
||||
return snapshot
|
||||
|
||||
async def create_chat(self) -> "Chat":
|
||||
def create_chat(self) -> "Chat":
|
||||
"""Create or get an existing 1:1 chat for this contact."""
|
||||
from .chat import Chat
|
||||
|
||||
return Chat(
|
||||
self.account,
|
||||
await self._rpc.create_chat_by_contact_id(self.account.id, self.id),
|
||||
self._rpc.create_chat_by_contact_id(self.account.id, self.id),
|
||||
)
|
||||
|
||||
@@ -16,34 +16,34 @@ class DeltaChat:
|
||||
def __init__(self, rpc: "Rpc") -> None:
|
||||
self.rpc = rpc
|
||||
|
||||
async def add_account(self) -> Account:
|
||||
def add_account(self) -> Account:
|
||||
"""Create a new account database."""
|
||||
account_id = await self.rpc.add_account()
|
||||
account_id = self.rpc.add_account()
|
||||
return Account(self, account_id)
|
||||
|
||||
async def get_all_accounts(self) -> List[Account]:
|
||||
def get_all_accounts(self) -> List[Account]:
|
||||
"""Return a list of all available accounts."""
|
||||
account_ids = await self.rpc.get_all_account_ids()
|
||||
account_ids = self.rpc.get_all_account_ids()
|
||||
return [Account(self, account_id) for account_id in account_ids]
|
||||
|
||||
async def start_io(self) -> None:
|
||||
def start_io(self) -> None:
|
||||
"""Start the I/O of all accounts."""
|
||||
await self.rpc.start_io_for_all_accounts()
|
||||
self.rpc.start_io_for_all_accounts()
|
||||
|
||||
async def stop_io(self) -> None:
|
||||
def stop_io(self) -> None:
|
||||
"""Stop the I/O of all accounts."""
|
||||
await self.rpc.stop_io_for_all_accounts()
|
||||
self.rpc.stop_io_for_all_accounts()
|
||||
|
||||
async def maybe_network(self) -> None:
|
||||
def maybe_network(self) -> None:
|
||||
"""Indicate that the network likely has come back or just that the network
|
||||
conditions might have changed.
|
||||
"""
|
||||
await self.rpc.maybe_network()
|
||||
self.rpc.maybe_network()
|
||||
|
||||
async def get_system_info(self) -> AttrDict:
|
||||
def get_system_info(self) -> AttrDict:
|
||||
"""Get information about the Delta Chat core in this system."""
|
||||
return AttrDict(await self.rpc.get_system_info())
|
||||
return AttrDict(self.rpc.get_system_info())
|
||||
|
||||
async def set_translations(self, translations: Dict[str, str]) -> None:
|
||||
def set_translations(self, translations: Dict[str, str]) -> None:
|
||||
"""Set stock translation strings."""
|
||||
await self.rpc.set_stock_strings(translations)
|
||||
self.rpc.set_stock_strings(translations)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
"""High-level classes for event processing and filtering."""
|
||||
import inspect
|
||||
import re
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
|
||||
@@ -24,7 +23,7 @@ def _tuple_of(obj, type_: type) -> tuple:
|
||||
class EventFilter(ABC):
|
||||
"""The base event filter.
|
||||
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
:param func: A Callable function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -43,16 +42,13 @@ class EventFilter(ABC):
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
async def _call_func(self, event) -> bool:
|
||||
def _call_func(self, event) -> bool:
|
||||
if not self.func:
|
||||
return True
|
||||
res = self.func(event)
|
||||
if inspect.isawaitable(res):
|
||||
return await res
|
||||
return res
|
||||
return self.func(event)
|
||||
|
||||
@abstractmethod
|
||||
async def filter(self, event):
|
||||
def filter(self, event):
|
||||
"""Return True-like value if the event passed the filter and should be
|
||||
used, or False-like value otherwise.
|
||||
"""
|
||||
@@ -62,7 +58,7 @@ class RawEvent(EventFilter):
|
||||
"""Matches raw core events.
|
||||
|
||||
:param types: The types of event to match.
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
:param func: A Callable function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -82,10 +78,10 @@ class RawEvent(EventFilter):
|
||||
return (self.types, self.func) == (other.types, other.func)
|
||||
return False
|
||||
|
||||
async def filter(self, event: "AttrDict") -> bool:
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
if self.types and event.type not in self.types:
|
||||
return False
|
||||
return await self._call_func(event)
|
||||
return self._call_func(event)
|
||||
|
||||
|
||||
class NewMessage(EventFilter):
|
||||
@@ -104,7 +100,7 @@ class NewMessage(EventFilter):
|
||||
:param is_info: If set to True only match info/system messages, if set to False
|
||||
only match messages that are not info/system messages. If omitted
|
||||
info/system messages as well as normal messages will be matched.
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
:param func: A Callable function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -159,7 +155,7 @@ class NewMessage(EventFilter):
|
||||
)
|
||||
return False
|
||||
|
||||
async def filter(self, event: "AttrDict") -> bool:
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
|
||||
return False
|
||||
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
|
||||
@@ -168,11 +164,9 @@ class NewMessage(EventFilter):
|
||||
return False
|
||||
if self.pattern:
|
||||
match = self.pattern(event.message_snapshot.text)
|
||||
if inspect.isawaitable(match):
|
||||
match = await match
|
||||
if not match:
|
||||
return False
|
||||
return await super()._call_func(event)
|
||||
return super()._call_func(event)
|
||||
|
||||
|
||||
class MemberListChanged(EventFilter):
|
||||
@@ -184,7 +178,7 @@ class MemberListChanged(EventFilter):
|
||||
:param added: If set to True only match if a member was added, if set to False
|
||||
only match if a member was removed. If omitted both, member additions
|
||||
and removals, will be matched.
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
:param func: A Callable function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -201,10 +195,10 @@ class MemberListChanged(EventFilter):
|
||||
return (self.added, self.func) == (other.added, other.func)
|
||||
return False
|
||||
|
||||
async def filter(self, event: "AttrDict") -> bool:
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
if self.added is not None and self.added != event.member_added:
|
||||
return False
|
||||
return await self._call_func(event)
|
||||
return self._call_func(event)
|
||||
|
||||
|
||||
class GroupImageChanged(EventFilter):
|
||||
@@ -216,7 +210,7 @@ class GroupImageChanged(EventFilter):
|
||||
:param deleted: If set to True only match if the image was deleted, if set to False
|
||||
only match if a new image was set. If omitted both, image changes and
|
||||
removals, will be matched.
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
:param func: A Callable function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -233,10 +227,10 @@ class GroupImageChanged(EventFilter):
|
||||
return (self.deleted, self.func) == (other.deleted, other.func)
|
||||
return False
|
||||
|
||||
async def filter(self, event: "AttrDict") -> bool:
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
if self.deleted is not None and self.deleted != event.image_deleted:
|
||||
return False
|
||||
return await self._call_func(event)
|
||||
return self._call_func(event)
|
||||
|
||||
|
||||
class GroupNameChanged(EventFilter):
|
||||
@@ -245,7 +239,7 @@ class GroupNameChanged(EventFilter):
|
||||
Warning: registering a handler for this event will cause the messages
|
||||
to be marked as read. Its usage is mainly intended for bots.
|
||||
|
||||
:param func: A Callable (async or not) function that should accept the event as input
|
||||
:param func: A Callable function that should accept the event as input
|
||||
parameter, and return a bool value indicating whether the event
|
||||
should be dispatched or not.
|
||||
"""
|
||||
@@ -258,8 +252,8 @@ class GroupNameChanged(EventFilter):
|
||||
return self.func == other.func
|
||||
return False
|
||||
|
||||
async def filter(self, event: "AttrDict") -> bool:
|
||||
return await self._call_func(event)
|
||||
def filter(self, event: "AttrDict") -> bool:
|
||||
return self._call_func(event)
|
||||
|
||||
|
||||
class HookCollection:
|
||||
|
||||
@@ -21,39 +21,39 @@ class Message:
|
||||
def _rpc(self) -> "Rpc":
|
||||
return self.account._rpc
|
||||
|
||||
async def send_reaction(self, *reaction: str):
|
||||
def send_reaction(self, *reaction: str):
|
||||
"""Send a reaction to this message."""
|
||||
await self._rpc.send_reaction(self.account.id, self.id, reaction)
|
||||
self._rpc.send_reaction(self.account.id, self.id, reaction)
|
||||
|
||||
async def get_snapshot(self) -> AttrDict:
|
||||
def get_snapshot(self) -> AttrDict:
|
||||
"""Get a snapshot with the properties of this message."""
|
||||
from .chat import Chat
|
||||
|
||||
snapshot = AttrDict(await self._rpc.get_message(self.account.id, self.id))
|
||||
snapshot = AttrDict(self._rpc.get_message(self.account.id, self.id))
|
||||
snapshot["chat"] = Chat(self.account, snapshot.chat_id)
|
||||
snapshot["sender"] = Contact(self.account, snapshot.from_id)
|
||||
snapshot["message"] = self
|
||||
return snapshot
|
||||
|
||||
async def get_reactions(self) -> Optional[AttrDict]:
|
||||
def get_reactions(self) -> Optional[AttrDict]:
|
||||
"""Get message reactions."""
|
||||
reactions = await self._rpc.get_message_reactions(self.account.id, self.id)
|
||||
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
|
||||
if reactions:
|
||||
return AttrDict(reactions)
|
||||
return None
|
||||
|
||||
async def mark_seen(self) -> None:
|
||||
def mark_seen(self) -> None:
|
||||
"""Mark the message as seen."""
|
||||
await self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||
self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||
|
||||
async def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
||||
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
||||
"""Send a webxdc status update. This message must be a webxdc."""
|
||||
if not isinstance(update, str):
|
||||
update = json.dumps(update)
|
||||
await self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
|
||||
self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
|
||||
|
||||
async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
|
||||
return json.loads(await self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
|
||||
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
|
||||
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
|
||||
|
||||
async def get_webxdc_info(self) -> dict:
|
||||
return await self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||
def get_webxdc_info(self) -> dict:
|
||||
return self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||
|
||||
@@ -1,70 +1,67 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
import urllib.request
|
||||
from typing import AsyncGenerator, List, Optional
|
||||
|
||||
import aiohttp
|
||||
import pytest_asyncio
|
||||
import pytest
|
||||
|
||||
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
|
||||
from .rpc import Rpc
|
||||
|
||||
|
||||
async def get_temp_credentials() -> dict:
|
||||
def get_temp_credentials() -> dict:
|
||||
url = os.getenv("DCC_NEW_TMP_EMAIL")
|
||||
assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set"
|
||||
|
||||
# Replace default 5 minute timeout with a 1 minute timeout.
|
||||
timeout = aiohttp.ClientTimeout(total=60)
|
||||
async with aiohttp.ClientSession() as session, session.post(url, timeout=timeout) as response:
|
||||
return json.loads(await response.text())
|
||||
request = urllib.request.Request(url, method="POST")
|
||||
with urllib.request.urlopen(request, timeout=60) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
class ACFactory:
|
||||
def __init__(self, deltachat: DeltaChat) -> None:
|
||||
self.deltachat = deltachat
|
||||
|
||||
async def get_unconfigured_account(self) -> Account:
|
||||
return await self.deltachat.add_account()
|
||||
def get_unconfigured_account(self) -> Account:
|
||||
return self.deltachat.add_account()
|
||||
|
||||
async def get_unconfigured_bot(self) -> Bot:
|
||||
return Bot(await self.get_unconfigured_account())
|
||||
def get_unconfigured_bot(self) -> Bot:
|
||||
return Bot(self.get_unconfigured_account())
|
||||
|
||||
async def new_preconfigured_account(self) -> Account:
|
||||
def new_preconfigured_account(self) -> Account:
|
||||
"""Make a new account with configuration options set, but configuration not started."""
|
||||
credentials = await get_temp_credentials()
|
||||
account = await self.get_unconfigured_account()
|
||||
await account.set_config("addr", credentials["email"])
|
||||
await account.set_config("mail_pw", credentials["password"])
|
||||
assert not await account.is_configured()
|
||||
credentials = get_temp_credentials()
|
||||
account = self.get_unconfigured_account()
|
||||
account.set_config("addr", credentials["email"])
|
||||
account.set_config("mail_pw", credentials["password"])
|
||||
assert not account.is_configured()
|
||||
return account
|
||||
|
||||
async def new_configured_account(self) -> Account:
|
||||
account = await self.new_preconfigured_account()
|
||||
await account.configure()
|
||||
assert await account.is_configured()
|
||||
def new_configured_account(self) -> Account:
|
||||
account = self.new_preconfigured_account()
|
||||
account.configure()
|
||||
assert account.is_configured()
|
||||
return account
|
||||
|
||||
async def new_configured_bot(self) -> Bot:
|
||||
credentials = await get_temp_credentials()
|
||||
bot = await self.get_unconfigured_bot()
|
||||
await bot.configure(credentials["email"], credentials["password"])
|
||||
def new_configured_bot(self) -> Bot:
|
||||
credentials = get_temp_credentials()
|
||||
bot = self.get_unconfigured_bot()
|
||||
bot.configure(credentials["email"], credentials["password"])
|
||||
return bot
|
||||
|
||||
async def get_online_account(self) -> Account:
|
||||
account = await self.new_configured_account()
|
||||
await account.start_io()
|
||||
def get_online_account(self) -> Account:
|
||||
account = self.new_configured_account()
|
||||
account.start_io()
|
||||
while True:
|
||||
event = await account.wait_for_event()
|
||||
print(event)
|
||||
event = account.wait_for_event()
|
||||
if event.type == EventType.IMAP_INBOX_IDLE:
|
||||
break
|
||||
return account
|
||||
|
||||
async def get_online_accounts(self, num: int) -> List[Account]:
|
||||
return await asyncio.gather(*[self.get_online_account() for _ in range(num)])
|
||||
def get_online_accounts(self, num: int) -> List[Account]:
|
||||
return [self.get_online_account() for _ in range(num)]
|
||||
|
||||
async def send_message(
|
||||
def send_message(
|
||||
self,
|
||||
to_account: Account,
|
||||
from_account: Optional[Account] = None,
|
||||
@@ -73,16 +70,16 @@ class ACFactory:
|
||||
group: Optional[str] = None,
|
||||
) -> Message:
|
||||
if not from_account:
|
||||
from_account = (await self.get_online_accounts(1))[0]
|
||||
to_contact = await from_account.create_contact(await to_account.get_config("addr"))
|
||||
from_account = (self.get_online_accounts(1))[0]
|
||||
to_contact = from_account.create_contact(to_account.get_config("addr"))
|
||||
if group:
|
||||
to_chat = await from_account.create_group(group)
|
||||
await to_chat.add_contact(to_contact)
|
||||
to_chat = from_account.create_group(group)
|
||||
to_chat.add_contact(to_contact)
|
||||
else:
|
||||
to_chat = await to_contact.create_chat()
|
||||
return await to_chat.send_message(text=text, file=file)
|
||||
to_chat = to_contact.create_chat()
|
||||
return to_chat.send_message(text=text, file=file)
|
||||
|
||||
async def process_message(
|
||||
def process_message(
|
||||
self,
|
||||
to_client: Client,
|
||||
from_account: Optional[Account] = None,
|
||||
@@ -90,7 +87,7 @@ class ACFactory:
|
||||
file: Optional[str] = None,
|
||||
group: Optional[str] = None,
|
||||
) -> AttrDict:
|
||||
await self.send_message(
|
||||
self.send_message(
|
||||
to_account=to_client.account,
|
||||
from_account=from_account,
|
||||
text=text,
|
||||
@@ -98,16 +95,16 @@ class ACFactory:
|
||||
group=group,
|
||||
)
|
||||
|
||||
return await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
|
||||
return to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def rpc(tmp_path) -> AsyncGenerator:
|
||||
@pytest.fixture()
|
||||
def rpc(tmp_path) -> AsyncGenerator:
|
||||
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
|
||||
async with rpc_server:
|
||||
with rpc_server:
|
||||
yield rpc_server
|
||||
|
||||
|
||||
@pytest_asyncio.fixture
|
||||
async def acfactory(rpc) -> AsyncGenerator:
|
||||
yield ACFactory(DeltaChat(rpc))
|
||||
@pytest.fixture()
|
||||
def acfactory(rpc) -> AsyncGenerator:
|
||||
return ACFactory(DeltaChat(rpc))
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
from queue import Queue
|
||||
from threading import Event, Thread
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
@@ -10,7 +14,7 @@ class JsonRpcError(Exception):
|
||||
|
||||
class Rpc:
|
||||
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
||||
"""The given arguments will be passed to asyncio.create_subprocess_exec()"""
|
||||
"""The given arguments will be passed to subprocess.Popen()"""
|
||||
if accounts_dir:
|
||||
kwargs["env"] = {
|
||||
**kwargs.get("env", os.environ),
|
||||
@@ -18,78 +22,127 @@ class Rpc:
|
||||
}
|
||||
|
||||
self._kwargs = kwargs
|
||||
self.process: asyncio.subprocess.Process
|
||||
self.process: subprocess.Popen
|
||||
self.id: int
|
||||
self.event_queues: Dict[int, asyncio.Queue]
|
||||
# Map from request ID to `asyncio.Future` returning the response.
|
||||
self.request_events: Dict[int, asyncio.Future]
|
||||
self.event_queues: Dict[int, Queue]
|
||||
# Map from request ID to `threading.Event`.
|
||||
self.request_events: Dict[int, Event]
|
||||
# Map from request ID to the result.
|
||||
self.request_results: Dict[int, Any]
|
||||
self.request_queue: Queue[Any]
|
||||
self.closing: bool
|
||||
self.reader_task: asyncio.Task
|
||||
self.events_task: asyncio.Task
|
||||
self.reader_thread: Thread
|
||||
self.writer_thread: Thread
|
||||
self.events_thread: Thread
|
||||
|
||||
async def start(self) -> None:
|
||||
self.process = await asyncio.create_subprocess_exec(
|
||||
"deltachat-rpc-server",
|
||||
stdin=asyncio.subprocess.PIPE,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
**self._kwargs,
|
||||
)
|
||||
def start(self) -> None:
|
||||
if sys.version_info >= (3, 11):
|
||||
self.process = subprocess.Popen(
|
||||
"deltachat-rpc-server",
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
# Prevent subprocess from capturing SIGINT.
|
||||
process_group=0,
|
||||
**self._kwargs,
|
||||
)
|
||||
else:
|
||||
self.process = subprocess.Popen(
|
||||
"deltachat-rpc-server",
|
||||
stdin=subprocess.PIPE,
|
||||
stdout=subprocess.PIPE,
|
||||
# `process_group` is not supported before Python 3.11.
|
||||
preexec_fn=os.setpgrp, # noqa: PLW1509
|
||||
**self._kwargs,
|
||||
)
|
||||
self.id = 0
|
||||
self.event_queues = {}
|
||||
self.request_events = {}
|
||||
self.request_results = {}
|
||||
self.request_queue = Queue()
|
||||
self.closing = False
|
||||
self.reader_task = asyncio.create_task(self.reader_loop())
|
||||
self.events_task = asyncio.create_task(self.events_loop())
|
||||
self.reader_thread = Thread(target=self.reader_loop)
|
||||
self.reader_thread.start()
|
||||
self.writer_thread = Thread(target=self.writer_loop)
|
||||
self.writer_thread.start()
|
||||
self.events_thread = Thread(target=self.events_loop)
|
||||
self.events_thread.start()
|
||||
|
||||
async def close(self) -> None:
|
||||
def close(self) -> None:
|
||||
"""Terminate RPC server process and wait until the reader loop finishes."""
|
||||
self.closing = True
|
||||
await self.stop_io_for_all_accounts()
|
||||
await self.events_task
|
||||
self.process.terminate()
|
||||
await self.reader_task
|
||||
self.stop_io_for_all_accounts()
|
||||
self.events_thread.join()
|
||||
self.process.stdin.close()
|
||||
self.reader_thread.join()
|
||||
self.request_queue.put(None)
|
||||
self.writer_thread.join()
|
||||
|
||||
async def __aenter__(self):
|
||||
await self.start()
|
||||
def __enter__(self):
|
||||
self.start()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, _exc_type, _exc, _tb):
|
||||
await self.close()
|
||||
def __exit__(self, _exc_type, _exc, _tb):
|
||||
self.close()
|
||||
|
||||
async def reader_loop(self) -> None:
|
||||
while True:
|
||||
line = await self.process.stdout.readline() # noqa
|
||||
if not line: # EOF
|
||||
break
|
||||
response = json.loads(line)
|
||||
if "id" in response:
|
||||
fut = self.request_events.pop(response["id"])
|
||||
fut.set_result(response)
|
||||
else:
|
||||
print(response)
|
||||
def reader_loop(self) -> None:
|
||||
try:
|
||||
while True:
|
||||
line = self.process.stdout.readline()
|
||||
if not line: # EOF
|
||||
break
|
||||
response = json.loads(line)
|
||||
if "id" in response:
|
||||
response_id = response["id"]
|
||||
event = self.request_events.pop(response_id)
|
||||
self.request_results[response_id] = response
|
||||
event.set()
|
||||
else:
|
||||
logging.warning("Got a response without ID: %s", response)
|
||||
except Exception:
|
||||
# Log an exception if the reader loop dies.
|
||||
logging.exception("Exception in the reader loop")
|
||||
|
||||
async def get_queue(self, account_id: int) -> asyncio.Queue:
|
||||
def writer_loop(self) -> None:
|
||||
"""Writer loop ensuring only a single thread writes requests."""
|
||||
try:
|
||||
while True:
|
||||
request = self.request_queue.get()
|
||||
if not request:
|
||||
break
|
||||
data = (json.dumps(request) + "\n").encode()
|
||||
self.process.stdin.write(data)
|
||||
self.process.stdin.flush()
|
||||
|
||||
except Exception:
|
||||
# Log an exception if the writer loop dies.
|
||||
logging.exception("Exception in the writer loop")
|
||||
|
||||
def get_queue(self, account_id: int) -> Queue:
|
||||
if account_id not in self.event_queues:
|
||||
self.event_queues[account_id] = asyncio.Queue()
|
||||
self.event_queues[account_id] = Queue()
|
||||
return self.event_queues[account_id]
|
||||
|
||||
async def events_loop(self) -> None:
|
||||
def events_loop(self) -> None:
|
||||
"""Requests new events and distributes them between queues."""
|
||||
while True:
|
||||
if self.closing:
|
||||
return
|
||||
event = await self.get_next_event()
|
||||
account_id = event["contextId"]
|
||||
queue = await self.get_queue(account_id)
|
||||
await queue.put(event["event"])
|
||||
try:
|
||||
while True:
|
||||
if self.closing:
|
||||
return
|
||||
event = self.get_next_event()
|
||||
account_id = event["contextId"]
|
||||
queue = self.get_queue(account_id)
|
||||
queue.put(event["event"])
|
||||
except Exception:
|
||||
# Log an exception if the event loop dies.
|
||||
logging.exception("Exception in the event loop")
|
||||
|
||||
async def wait_for_event(self, account_id: int) -> Optional[dict]:
|
||||
def wait_for_event(self, account_id: int) -> Optional[dict]:
|
||||
"""Waits for the next event from the given account and returns it."""
|
||||
queue = await self.get_queue(account_id)
|
||||
return await queue.get()
|
||||
queue = self.get_queue(account_id)
|
||||
return queue.get()
|
||||
|
||||
def __getattr__(self, attr: str):
|
||||
async def method(*args) -> Any:
|
||||
def method(*args) -> Any:
|
||||
self.id += 1
|
||||
request_id = self.id
|
||||
|
||||
@@ -99,12 +152,12 @@ class Rpc:
|
||||
"params": args,
|
||||
"id": self.id,
|
||||
}
|
||||
data = (json.dumps(request) + "\n").encode()
|
||||
self.process.stdin.write(data) # noqa
|
||||
loop = asyncio.get_running_loop()
|
||||
fut = loop.create_future()
|
||||
self.request_events[request_id] = fut
|
||||
response = await fut
|
||||
event = Event()
|
||||
self.request_events[request_id] = event
|
||||
self.request_queue.put(request)
|
||||
event.wait()
|
||||
|
||||
response = self.request_results.pop(request_id)
|
||||
if "error" in response:
|
||||
raise JsonRpcError(response["error"])
|
||||
if "result" in response:
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import asyncio
|
||||
import concurrent.futures
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
import pytest
|
||||
@@ -6,26 +6,26 @@ from deltachat_rpc_client import EventType, events
|
||||
from deltachat_rpc_client.rpc import JsonRpcError
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_system_info(rpc) -> None:
|
||||
system_info = await rpc.get_system_info()
|
||||
def test_system_info(rpc) -> None:
|
||||
system_info = rpc.get_system_info()
|
||||
assert "arch" in system_info
|
||||
assert "deltachat_core_version" in system_info
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_sleep(rpc) -> None:
|
||||
def test_sleep(rpc) -> None:
|
||||
"""Test that long-running task does not block short-running task from completion."""
|
||||
sleep_5_task = asyncio.create_task(rpc.sleep(5.0))
|
||||
sleep_3_task = asyncio.create_task(rpc.sleep(3.0))
|
||||
done, pending = await asyncio.wait([sleep_5_task, sleep_3_task], return_when=asyncio.FIRST_COMPLETED)
|
||||
assert sleep_3_task in done
|
||||
assert sleep_5_task in pending
|
||||
sleep_5_task.cancel()
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||
sleep_5_future = executor.submit(rpc.sleep, 5.0)
|
||||
sleep_3_future = executor.submit(rpc.sleep, 3.0)
|
||||
done, pending = concurrent.futures.wait(
|
||||
[sleep_5_future, sleep_3_future],
|
||||
return_when=concurrent.futures.FIRST_COMPLETED,
|
||||
)
|
||||
assert sleep_3_future in done
|
||||
assert sleep_5_future in pending
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_email_address_validity(rpc) -> None:
|
||||
def test_email_address_validity(rpc) -> None:
|
||||
valid_addresses = [
|
||||
"email@example.com",
|
||||
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
|
||||
@@ -33,16 +33,15 @@ async def test_email_address_validity(rpc) -> None:
|
||||
invalid_addresses = ["email@", "example.com", "emai221"]
|
||||
|
||||
for addr in valid_addresses:
|
||||
assert await rpc.check_email_validity(addr)
|
||||
assert rpc.check_email_validity(addr)
|
||||
for addr in invalid_addresses:
|
||||
assert not await rpc.check_email_validity(addr)
|
||||
assert not rpc.check_email_validity(addr)
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_acfactory(acfactory) -> None:
|
||||
account = await acfactory.new_configured_account()
|
||||
def test_acfactory(acfactory) -> None:
|
||||
account = acfactory.new_configured_account()
|
||||
while True:
|
||||
event = await account.wait_for_event()
|
||||
event = account.wait_for_event()
|
||||
if event.type == EventType.CONFIGURE_PROGRESS:
|
||||
assert event.progress != 0 # Progress 0 indicates error.
|
||||
if event.progress == 1000: # Success
|
||||
@@ -52,248 +51,241 @@ async def test_acfactory(acfactory) -> None:
|
||||
print("Successful configuration")
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_configure_starttls(acfactory) -> None:
|
||||
account = await acfactory.new_preconfigured_account()
|
||||
def test_configure_starttls(acfactory) -> None:
|
||||
account = acfactory.new_preconfigured_account()
|
||||
|
||||
# Use STARTTLS
|
||||
await account.set_config("mail_security", "2")
|
||||
await account.set_config("send_security", "2")
|
||||
await account.configure()
|
||||
assert await account.is_configured()
|
||||
account.set_config("mail_security", "2")
|
||||
account.set_config("send_security", "2")
|
||||
account.configure()
|
||||
assert account.is_configured()
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_account(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
def test_account(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = await bob.get_config("addr")
|
||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
||||
await alice_chat_bob.send_text("Hello!")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
event = bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
break
|
||||
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = await message.get_snapshot()
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.chat_id == chat_id
|
||||
assert snapshot.text == "Hello!"
|
||||
await bob.mark_seen_messages([message])
|
||||
bob.mark_seen_messages([message])
|
||||
|
||||
assert alice != bob
|
||||
assert repr(alice)
|
||||
assert (await alice.get_info()).level
|
||||
assert await alice.get_size()
|
||||
assert await alice.is_configured()
|
||||
assert not await alice.get_avatar()
|
||||
assert await alice.get_contact_by_addr(bob_addr) == alice_contact_bob
|
||||
assert await alice.get_contacts()
|
||||
assert await alice.get_contacts(snapshot=True)
|
||||
assert alice.get_info().level
|
||||
assert alice.get_size()
|
||||
assert alice.is_configured()
|
||||
assert not alice.get_avatar()
|
||||
assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob
|
||||
assert alice.get_contacts()
|
||||
assert alice.get_contacts(snapshot=True)
|
||||
assert alice.self_contact
|
||||
assert await alice.get_chatlist()
|
||||
assert await alice.get_chatlist(snapshot=True)
|
||||
assert await alice.get_qr_code()
|
||||
assert await alice.get_fresh_messages()
|
||||
assert await alice.get_next_messages()
|
||||
assert alice.get_chatlist()
|
||||
assert alice.get_chatlist(snapshot=True)
|
||||
assert alice.get_qr_code()
|
||||
assert alice.get_fresh_messages()
|
||||
assert alice.get_next_messages()
|
||||
|
||||
# Test sending empty message.
|
||||
assert len(await bob.wait_next_messages()) == 0
|
||||
await alice_chat_bob.send_text("")
|
||||
messages = await bob.wait_next_messages()
|
||||
assert len(bob.wait_next_messages()) == 0
|
||||
alice_chat_bob.send_text("")
|
||||
messages = bob.wait_next_messages()
|
||||
assert len(messages) == 1
|
||||
message = messages[0]
|
||||
snapshot = await message.get_snapshot()
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.text == ""
|
||||
await bob.mark_seen_messages([message])
|
||||
bob.mark_seen_messages([message])
|
||||
|
||||
group = await alice.create_group("test group")
|
||||
await group.add_contact(alice_contact_bob)
|
||||
group_msg = await group.send_message(text="hello")
|
||||
group = alice.create_group("test group")
|
||||
group.add_contact(alice_contact_bob)
|
||||
group_msg = group.send_message(text="hello")
|
||||
assert group_msg == alice.get_message_by_id(group_msg.id)
|
||||
assert group == alice.get_chat_by_id(group.id)
|
||||
await alice.delete_messages([group_msg])
|
||||
alice.delete_messages([group_msg])
|
||||
|
||||
await alice.set_config("selfstatus", "test")
|
||||
assert await alice.get_config("selfstatus") == "test"
|
||||
await alice.update_config(selfstatus="test2")
|
||||
assert await alice.get_config("selfstatus") == "test2"
|
||||
alice.set_config("selfstatus", "test")
|
||||
assert alice.get_config("selfstatus") == "test"
|
||||
alice.update_config(selfstatus="test2")
|
||||
assert alice.get_config("selfstatus") == "test2"
|
||||
|
||||
assert not await alice.get_blocked_contacts()
|
||||
await alice_contact_bob.block()
|
||||
blocked_contacts = await alice.get_blocked_contacts()
|
||||
assert not alice.get_blocked_contacts()
|
||||
alice_contact_bob.block()
|
||||
blocked_contacts = alice.get_blocked_contacts()
|
||||
assert blocked_contacts
|
||||
assert blocked_contacts[0].contact == alice_contact_bob
|
||||
|
||||
await bob.remove()
|
||||
await alice.stop_io()
|
||||
bob.remove()
|
||||
alice.stop_io()
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_chat(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
def test_chat(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = await bob.get_config("addr")
|
||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
||||
await alice_chat_bob.send_text("Hello!")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
event = bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
break
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = await message.get_snapshot()
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.chat_id == chat_id
|
||||
assert snapshot.text == "Hello!"
|
||||
bob_chat_alice = bob.get_chat_by_id(chat_id)
|
||||
|
||||
assert alice_chat_bob != bob_chat_alice
|
||||
assert repr(alice_chat_bob)
|
||||
await alice_chat_bob.delete()
|
||||
assert not await bob_chat_alice.can_send()
|
||||
await bob_chat_alice.accept()
|
||||
assert await bob_chat_alice.can_send()
|
||||
await bob_chat_alice.block()
|
||||
bob_chat_alice = await snapshot.sender.create_chat()
|
||||
await bob_chat_alice.mute()
|
||||
await bob_chat_alice.unmute()
|
||||
await bob_chat_alice.pin()
|
||||
await bob_chat_alice.unpin()
|
||||
await bob_chat_alice.archive()
|
||||
await bob_chat_alice.unarchive()
|
||||
alice_chat_bob.delete()
|
||||
assert not bob_chat_alice.can_send()
|
||||
bob_chat_alice.accept()
|
||||
assert bob_chat_alice.can_send()
|
||||
bob_chat_alice.block()
|
||||
bob_chat_alice = snapshot.sender.create_chat()
|
||||
bob_chat_alice.mute()
|
||||
bob_chat_alice.unmute()
|
||||
bob_chat_alice.pin()
|
||||
bob_chat_alice.unpin()
|
||||
bob_chat_alice.archive()
|
||||
bob_chat_alice.unarchive()
|
||||
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
|
||||
await bob_chat_alice.set_name("test")
|
||||
await bob_chat_alice.set_ephemeral_timer(300)
|
||||
await bob_chat_alice.get_encryption_info()
|
||||
bob_chat_alice.set_name("test")
|
||||
bob_chat_alice.set_ephemeral_timer(300)
|
||||
bob_chat_alice.get_encryption_info()
|
||||
|
||||
group = await alice.create_group("test group")
|
||||
await group.add_contact(alice_contact_bob)
|
||||
await group.get_qr_code()
|
||||
group = alice.create_group("test group")
|
||||
group.add_contact(alice_contact_bob)
|
||||
group.get_qr_code()
|
||||
|
||||
snapshot = await group.get_basic_snapshot()
|
||||
snapshot = group.get_basic_snapshot()
|
||||
assert snapshot.name == "test group"
|
||||
await group.set_name("new name")
|
||||
snapshot = await group.get_full_snapshot()
|
||||
group.set_name("new name")
|
||||
snapshot = group.get_full_snapshot()
|
||||
assert snapshot.name == "new name"
|
||||
|
||||
msg = await group.send_message(text="hi")
|
||||
assert (await msg.get_snapshot()).text == "hi"
|
||||
await group.forward_messages([msg])
|
||||
msg = group.send_message(text="hi")
|
||||
assert (msg.get_snapshot()).text == "hi"
|
||||
group.forward_messages([msg])
|
||||
|
||||
await group.set_draft(text="test draft")
|
||||
draft = await group.get_draft()
|
||||
group.set_draft(text="test draft")
|
||||
draft = group.get_draft()
|
||||
assert draft.text == "test draft"
|
||||
await group.remove_draft()
|
||||
assert not await group.get_draft()
|
||||
group.remove_draft()
|
||||
assert not group.get_draft()
|
||||
|
||||
assert await group.get_messages()
|
||||
await group.get_fresh_message_count()
|
||||
await group.mark_noticed()
|
||||
assert await group.get_contacts()
|
||||
await group.remove_contact(alice_chat_bob)
|
||||
await group.get_locations()
|
||||
assert group.get_messages()
|
||||
group.get_fresh_message_count()
|
||||
group.mark_noticed()
|
||||
assert group.get_contacts()
|
||||
group.remove_contact(alice_chat_bob)
|
||||
group.get_locations()
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_contact(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
def test_contact(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = await bob.get_config("addr")
|
||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
|
||||
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
|
||||
assert repr(alice_contact_bob)
|
||||
await alice_contact_bob.block()
|
||||
await alice_contact_bob.unblock()
|
||||
await alice_contact_bob.set_name("new name")
|
||||
await alice_contact_bob.get_encryption_info()
|
||||
snapshot = await alice_contact_bob.get_snapshot()
|
||||
alice_contact_bob.block()
|
||||
alice_contact_bob.unblock()
|
||||
alice_contact_bob.set_name("new name")
|
||||
alice_contact_bob.get_encryption_info()
|
||||
snapshot = alice_contact_bob.get_snapshot()
|
||||
assert snapshot.address == bob_addr
|
||||
await alice_contact_bob.create_chat()
|
||||
alice_contact_bob.create_chat()
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_message(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
def test_message(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = await bob.get_config("addr")
|
||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
||||
await alice_chat_bob.send_text("Hello!")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
event = bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
chat_id = event.chat_id
|
||||
msg_id = event.msg_id
|
||||
break
|
||||
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = await message.get_snapshot()
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.chat_id == chat_id
|
||||
assert snapshot.text == "Hello!"
|
||||
assert not snapshot.is_bot
|
||||
assert repr(message)
|
||||
|
||||
with pytest.raises(JsonRpcError): # chat is not accepted
|
||||
await snapshot.chat.send_text("hi")
|
||||
await snapshot.chat.accept()
|
||||
await snapshot.chat.send_text("hi")
|
||||
snapshot.chat.send_text("hi")
|
||||
snapshot.chat.accept()
|
||||
snapshot.chat.send_text("hi")
|
||||
|
||||
await message.mark_seen()
|
||||
await message.send_reaction("😎")
|
||||
reactions = await message.get_reactions()
|
||||
message.mark_seen()
|
||||
message.send_reaction("😎")
|
||||
reactions = message.get_reactions()
|
||||
assert reactions
|
||||
snapshot = await message.get_snapshot()
|
||||
snapshot = message.get_snapshot()
|
||||
assert reactions == snapshot.reactions
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_is_bot(acfactory) -> None:
|
||||
def test_is_bot(acfactory) -> None:
|
||||
"""Test that we can recognize messages submitted by bots."""
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = await bob.get_config("addr")
|
||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
|
||||
# Alice becomes a bot.
|
||||
await alice.set_config("bot", "1")
|
||||
await alice_chat_bob.send_text("Hello!")
|
||||
alice.set_config("bot", "1")
|
||||
alice_chat_bob.send_text("Hello!")
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
event = bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
msg_id = event.msg_id
|
||||
message = bob.get_message_by_id(msg_id)
|
||||
snapshot = await message.get_snapshot()
|
||||
snapshot = message.get_snapshot()
|
||||
assert snapshot.chat_id == event.chat_id
|
||||
assert snapshot.text == "Hello!"
|
||||
assert snapshot.is_bot
|
||||
break
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_bot(acfactory) -> None:
|
||||
def test_bot(acfactory) -> None:
|
||||
mock = MagicMock()
|
||||
user = (await acfactory.get_online_accounts(1))[0]
|
||||
bot = await acfactory.new_configured_bot()
|
||||
bot2 = await acfactory.new_configured_bot()
|
||||
user = (acfactory.get_online_accounts(1))[0]
|
||||
bot = acfactory.new_configured_bot()
|
||||
bot2 = acfactory.new_configured_bot()
|
||||
|
||||
assert await bot.is_configured()
|
||||
assert await bot.account.get_config("bot") == "1"
|
||||
assert bot.is_configured()
|
||||
assert bot.account.get_config("bot") == "1"
|
||||
|
||||
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
|
||||
bot.add_hook(*hook)
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
|
||||
snapshot = await bot.account.get_message_by_id(event.msg_id).get_snapshot()
|
||||
event = acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
|
||||
snapshot = bot.account.get_message_by_id(event.msg_id).get_snapshot()
|
||||
assert not snapshot.is_bot
|
||||
mock.hook.assert_called_once_with(event.msg_id)
|
||||
bot.remove_hook(*hook)
|
||||
@@ -305,53 +297,52 @@ async def test_bot(acfactory) -> None:
|
||||
hook = track, events.NewMessage(r"hello")
|
||||
bot.add_hook(*hook)
|
||||
bot.add_hook(track, events.NewMessage(command="/help"))
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
event = acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
mock.hook.assert_called_with(event.msg_id)
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!")
|
||||
event = acfactory.process_message(from_account=user, to_client=bot, text="hello!")
|
||||
mock.hook.assert_called_with(event.msg_id)
|
||||
await acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
|
||||
acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
|
||||
assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots
|
||||
await acfactory.process_message(from_account=user, to_client=bot, text="hey!")
|
||||
acfactory.process_message(from_account=user, to_client=bot, text="hey!")
|
||||
assert len(mock.hook.mock_calls) == 2
|
||||
bot.remove_hook(*hook)
|
||||
|
||||
mock.hook.reset_mock()
|
||||
await acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="/help")
|
||||
acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||
event = acfactory.process_message(from_account=user, to_client=bot, text="/help")
|
||||
mock.hook.assert_called_once_with(event.msg_id)
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_wait_next_messages(acfactory) -> None:
|
||||
alice = await acfactory.new_configured_account()
|
||||
def test_wait_next_messages(acfactory) -> None:
|
||||
alice = acfactory.new_configured_account()
|
||||
|
||||
# Create a bot account so it does not receive device messages in the beginning.
|
||||
bot = await acfactory.new_preconfigured_account()
|
||||
await bot.set_config("bot", "1")
|
||||
await bot.configure()
|
||||
bot = acfactory.new_preconfigured_account()
|
||||
bot.set_config("bot", "1")
|
||||
bot.configure()
|
||||
|
||||
# There are no old messages and the call returns immediately.
|
||||
assert not await bot.wait_next_messages()
|
||||
assert not bot.wait_next_messages()
|
||||
|
||||
# Bot starts waiting for messages.
|
||||
next_messages_task = asyncio.create_task(bot.wait_next_messages())
|
||||
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||
# Bot starts waiting for messages.
|
||||
next_messages_task = executor.submit(bot.wait_next_messages)
|
||||
|
||||
bot_addr = await bot.get_config("addr")
|
||||
alice_contact_bot = await alice.create_contact(bot_addr, "Bob")
|
||||
alice_chat_bot = await alice_contact_bot.create_chat()
|
||||
await alice_chat_bot.send_text("Hello!")
|
||||
bot_addr = bot.get_config("addr")
|
||||
alice_contact_bot = alice.create_contact(bot_addr, "Bob")
|
||||
alice_chat_bot = alice_contact_bot.create_chat()
|
||||
alice_chat_bot.send_text("Hello!")
|
||||
|
||||
next_messages = await next_messages_task
|
||||
assert len(next_messages) == 1
|
||||
snapshot = await next_messages[0].get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
next_messages = next_messages_task.result()
|
||||
assert len(next_messages) == 1
|
||||
snapshot = next_messages[0].get_snapshot()
|
||||
assert snapshot.text == "Hello!"
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_import_export(acfactory, tmp_path) -> None:
|
||||
alice = await acfactory.new_configured_account()
|
||||
await alice.export_backup(tmp_path)
|
||||
def test_import_export(acfactory, tmp_path) -> None:
|
||||
alice = acfactory.new_configured_account()
|
||||
alice.export_backup(tmp_path)
|
||||
|
||||
files = list(tmp_path.glob("*.tar"))
|
||||
alice2 = await acfactory.get_unconfigured_account()
|
||||
await alice2.import_backup(files[0])
|
||||
alice2 = acfactory.get_unconfigured_account()
|
||||
alice2.import_backup(files[0])
|
||||
|
||||
@@ -1,24 +1,22 @@
|
||||
import pytest
|
||||
from deltachat_rpc_client import EventType
|
||||
|
||||
|
||||
@pytest.mark.asyncio()
|
||||
async def test_webxdc(acfactory) -> None:
|
||||
alice, bob = await acfactory.get_online_accounts(2)
|
||||
def test_webxdc(acfactory) -> None:
|
||||
alice, bob = acfactory.get_online_accounts(2)
|
||||
|
||||
bob_addr = await bob.get_config("addr")
|
||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
||||
await alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||
bob_addr = bob.get_config("addr")
|
||||
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||
alice_chat_bob = alice_contact_bob.create_chat()
|
||||
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||
|
||||
while True:
|
||||
event = await bob.wait_for_event()
|
||||
event = bob.wait_for_event()
|
||||
if event.type == EventType.INCOMING_MSG:
|
||||
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
||||
message = bob.get_message_by_id(event.msg_id)
|
||||
break
|
||||
|
||||
webxdc_info = await message.get_webxdc_info()
|
||||
webxdc_info = message.get_webxdc_info()
|
||||
assert webxdc_info == {
|
||||
"document": None,
|
||||
"icon": "icon.png",
|
||||
@@ -28,20 +26,20 @@ async def test_webxdc(acfactory) -> None:
|
||||
"summary": None,
|
||||
}
|
||||
|
||||
status_updates = await message.get_webxdc_status_updates()
|
||||
status_updates = message.get_webxdc_status_updates()
|
||||
assert status_updates == []
|
||||
|
||||
await bob_chat_alice.accept()
|
||||
await message.send_webxdc_status_update({"payload": 42}, "")
|
||||
await message.send_webxdc_status_update({"payload": "Second update"}, "description")
|
||||
bob_chat_alice.accept()
|
||||
message.send_webxdc_status_update({"payload": 42}, "")
|
||||
message.send_webxdc_status_update({"payload": "Second update"}, "description")
|
||||
|
||||
status_updates = await message.get_webxdc_status_updates()
|
||||
status_updates = message.get_webxdc_status_updates()
|
||||
assert status_updates == [
|
||||
{"payload": 42, "serial": 1, "max_serial": 2},
|
||||
{"payload": "Second update", "serial": 2, "max_serial": 2},
|
||||
]
|
||||
|
||||
status_updates = await message.get_webxdc_status_updates(1)
|
||||
status_updates = message.get_webxdc_status_updates(1)
|
||||
assert status_updates == [
|
||||
{"payload": "Second update", "serial": 2, "max_serial": 2},
|
||||
]
|
||||
|
||||
@@ -6,7 +6,7 @@ envlist =
|
||||
|
||||
[testenv]
|
||||
commands =
|
||||
pytest {posargs}
|
||||
pytest -n6 {posargs}
|
||||
setenv =
|
||||
# Avoid stack overflow when Rust core is built without optimizations.
|
||||
RUST_MIN_STACK=8388608
|
||||
@@ -14,10 +14,8 @@ passenv =
|
||||
DCC_NEW_TMP_EMAIL
|
||||
deps =
|
||||
pytest
|
||||
pytest-asyncio
|
||||
pytest-timeout
|
||||
aiohttp
|
||||
aiodns
|
||||
pytest-xdist
|
||||
|
||||
[testenv:lint]
|
||||
skipsdist = True
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.122.0"
|
||||
version = "1.124.1"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
|
||||
@@ -60,5 +60,5 @@
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.122.0"
|
||||
"version": "1.124.1"
|
||||
}
|
||||
|
||||
@@ -24,3 +24,5 @@ ignore_missing_imports = True
|
||||
[mypy-imap_tools.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-distutils.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
@@ -195,7 +195,7 @@ class Account:
|
||||
assert res != ffi.NULL, f"config value not found for: {name!r}"
|
||||
return from_dc_charpointer(res)
|
||||
|
||||
def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None:
|
||||
def _preconfigure_keypair(self, addr: str, secret: str) -> None:
|
||||
"""See dc_preconfigure_keypair() in deltachat.h.
|
||||
|
||||
In other words, you don't need this.
|
||||
@@ -203,7 +203,7 @@ class Account:
|
||||
res = lib.dc_preconfigure_keypair(
|
||||
self._dc_context,
|
||||
as_dc_charpointer(addr),
|
||||
as_dc_charpointer(public),
|
||||
ffi.NULL,
|
||||
as_dc_charpointer(secret),
|
||||
)
|
||||
if res == 0:
|
||||
@@ -617,18 +617,18 @@ class Account:
|
||||
# meta API for start/stop and event based processing
|
||||
#
|
||||
|
||||
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False):
|
||||
from .events import FFIEventLogger
|
||||
|
||||
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False, displayname=None):
|
||||
"""get the account running, configure it if necessary. add plugins if provided.
|
||||
|
||||
:param addr: the email address of the account
|
||||
:param password: the password of the account
|
||||
:param account_plugins: a list of plugins to add
|
||||
:param show_ffi: show low level ffi events
|
||||
:param displayname: the display name of the account
|
||||
"""
|
||||
from .events import FFIEventLogger
|
||||
|
||||
if show_ffi:
|
||||
self.set_config("displayname", "bot")
|
||||
log = FFIEventLogger(self)
|
||||
self.add_account_plugin(log)
|
||||
|
||||
@@ -644,6 +644,8 @@ class Account:
|
||||
configtracker = self.configure()
|
||||
configtracker.wait_finish()
|
||||
|
||||
if displayname:
|
||||
self.set_config("displayname", displayname)
|
||||
# start IO threads and configure if necessary
|
||||
self.start_io()
|
||||
|
||||
|
||||
@@ -75,9 +75,12 @@ class Contact:
|
||||
"""Return True if the contact is verified."""
|
||||
return lib.dc_contact_is_verified(self._dc_contact) == 2
|
||||
|
||||
def get_verifier(self, contact):
|
||||
def get_verifier(self, contact) -> Optional["Contact"]:
|
||||
"""Return the address of the contact that verified the contact."""
|
||||
return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact))
|
||||
verifier_id = lib.dc_contact_get_verifier_id(contact._dc_contact)
|
||||
if verifier_id == 0:
|
||||
return None
|
||||
return Contact(self.account, verifier_id)
|
||||
|
||||
def get_profile_image(self) -> Optional[str]:
|
||||
"""Get contact profile image.
|
||||
|
||||
@@ -478,10 +478,9 @@ class ACFactory:
|
||||
except IndexError:
|
||||
pass
|
||||
else:
|
||||
fname_pub = self.data.read_path(f"key/{keyname}-public.asc")
|
||||
fname_sec = self.data.read_path(f"key/{keyname}-secret.asc")
|
||||
if fname_pub and fname_sec:
|
||||
account._preconfigure_keypair(addr, fname_pub, fname_sec)
|
||||
if fname_sec:
|
||||
account._preconfigure_keypair(addr, fname_sec)
|
||||
return True
|
||||
print(f"WARN: could not use preconfigured keys for {addr!r}")
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import sys
|
||||
|
||||
import pytest
|
||||
import deltachat as dc
|
||||
|
||||
|
||||
class TestGroupStressTests:
|
||||
@@ -149,9 +150,8 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
assert msg.is_encrypted()
|
||||
|
||||
lp.sec("ac2: Check that ac2 verified ac1")
|
||||
# If we verified the contact ourselves then verifier addr == contact addr
|
||||
ac2_ac1_contact = ac2.get_contacts()[0]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
|
||||
|
||||
lp.sec("ac2: send message and let ac1 read it")
|
||||
chat2.send_text("world")
|
||||
@@ -176,9 +176,9 @@ def test_qr_verified_group_and_chatting(acfactory, lp):
|
||||
|
||||
lp.sec("ac2: Check that ac1 verified ac3 for ac2")
|
||||
ac2_ac1_contact = ac2.get_contacts()[0]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact).id == dc.const.DC_CONTACT_ID_SELF
|
||||
ac2_ac3_contact = ac2.get_contacts()[1]
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact) == ac1_addr
|
||||
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr
|
||||
|
||||
lp.sec("ac2: send message and let ac3 read it")
|
||||
chat2.send_text("hi")
|
||||
|
||||
@@ -1926,13 +1926,15 @@ def test_set_get_group_image(acfactory, data, lp):
|
||||
lp.sec("ac1: add ac2 to promoted group chat")
|
||||
chat.add_contact(ac2) # sends one message
|
||||
|
||||
lp.sec("ac2: wait for receiving member added message from ac1")
|
||||
msg1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg1.is_system_message() # Member added
|
||||
|
||||
lp.sec("ac1: send a first message to ac2")
|
||||
chat.send_text("hi") # sends another message
|
||||
assert chat.is_promoted()
|
||||
|
||||
lp.sec("ac2: wait for receiving message from ac1")
|
||||
msg1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg1.is_system_message() # Member added
|
||||
msg2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg2.text == "hi"
|
||||
assert msg1.chat.id == msg2.chat.id
|
||||
|
||||
@@ -67,10 +67,9 @@ class TestOfflineAccountBasic:
|
||||
|
||||
def test_preconfigure_keypair(self, acfactory, data):
|
||||
ac = acfactory.get_unconfigured_account()
|
||||
alice_public = data.read_path("key/alice-public.asc")
|
||||
alice_secret = data.read_path("key/alice-secret.asc")
|
||||
assert alice_public and alice_secret
|
||||
ac._preconfigure_keypair("alice@example.org", alice_public, alice_secret)
|
||||
assert alice_secret
|
||||
ac._preconfigure_keypair("alice@example.org", alice_secret)
|
||||
|
||||
def test_getinfo(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import json
|
||||
from queue import Queue
|
||||
|
||||
import deltachat as dc
|
||||
@@ -226,10 +227,26 @@ def test_jsonrpc_blocking_call(tmp_path):
|
||||
lib.dc_accounts_unref,
|
||||
)
|
||||
jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref)
|
||||
res = from_optional_dc_charpointer(
|
||||
lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice@example.org"]'),
|
||||
res = json.loads(
|
||||
from_optional_dc_charpointer(
|
||||
lib.dc_jsonrpc_blocking_call(
|
||||
jsonrpc,
|
||||
json.dumps(
|
||||
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice@example.org"], "id": "123"},
|
||||
).encode("utf-8"),
|
||||
),
|
||||
),
|
||||
)
|
||||
assert res == "true"
|
||||
assert res == {"jsonrpc": "2.0", "id": "123", "result": True}
|
||||
|
||||
res = from_optional_dc_charpointer(lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice"]'))
|
||||
assert res == "false"
|
||||
res = json.loads(
|
||||
from_optional_dc_charpointer(
|
||||
lib.dc_jsonrpc_blocking_call(
|
||||
jsonrpc,
|
||||
json.dumps(
|
||||
{"jsonrpc": "2.0", "method": "check_email_validity", "params": ["alice"], "id": "456"},
|
||||
).encode("utf-8"),
|
||||
),
|
||||
),
|
||||
)
|
||||
assert res == {"jsonrpc": "2.0", "id": "456", "result": False}
|
||||
|
||||
@@ -25,6 +25,9 @@ deps =
|
||||
pytest-xdist
|
||||
pdbpp
|
||||
requests
|
||||
# urllib3 2.0 does not work in manylinux2014 containers.
|
||||
# https://github.com/deltachat/deltachat-core-rust/issues/4788
|
||||
urllib3<2
|
||||
|
||||
[testenv:.pkg]
|
||||
passenv =
|
||||
|
||||
@@ -1 +1 @@
|
||||
2023-09-12
|
||||
2023-10-05
|
||||
@@ -18,7 +18,7 @@ and an own build machine.
|
||||
- `remote_tests_rust.sh` rsyncs to the build machine and runs
|
||||
`run-rust-test.sh` remotely on the build machine.
|
||||
|
||||
- `make-python-testenv.sh` creates or updates local python test development environment.
|
||||
- `make-python-testenv.sh` creates local python test development environment.
|
||||
Reusing the same environment is faster than running `run-python-test.sh` which always
|
||||
recreates environment from scratch and runs additional lints.
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@
|
||||
# It rebuilds the core and bindings as needed.
|
||||
#
|
||||
# After running the script, you can either
|
||||
# run `pytest` directly with `env/bin/pytest python/`
|
||||
# or activate the environment with `. env/bin/activacte`
|
||||
# run `pytest` directly with `venv/bin/pytest python/`
|
||||
# or activate the environment with `. venv/bin/activate`
|
||||
# and run `pytest` from there.
|
||||
set -euo pipefail
|
||||
|
||||
@@ -13,9 +13,5 @@ export DCC_RS_TARGET=debug
|
||||
export DCC_RS_DEV="$PWD"
|
||||
cargo build -p deltachat_ffi --features jsonrpc
|
||||
|
||||
if test -d env; then
|
||||
env/bin/pip install -e python --force-reinstall
|
||||
else
|
||||
tox -e py --devenv env
|
||||
env/bin/pip install --upgrade pip
|
||||
fi
|
||||
tox -c python -e py --devenv venv
|
||||
env/bin/pip install --upgrade pip
|
||||
|
||||
@@ -31,7 +31,7 @@ unset DCC_NEW_TMP_EMAIL
|
||||
|
||||
# Try to build wheels for a range of interpreters, but don't fail if they are not available.
|
||||
# E.g. musllinux_1_1 does not have PyPy interpreters as of 2022-07-10
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39 --skip-missing-interpreters true
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
|
||||
|
||||
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"
|
||||
|
||||
|
||||
10
src/chat.rs
10
src/chat.rs
@@ -918,9 +918,10 @@ impl ChatId {
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT chat_id, count(*) AS n
|
||||
FROM chats_contacts where contact_id > 9
|
||||
FROM chats_contacts
|
||||
WHERE contact_id > ? AND chat_id > ?
|
||||
GROUP BY chat_id",
|
||||
(),
|
||||
(ContactId::LAST_SPECIAL, DC_CHAT_ID_LAST_SPECIAL),
|
||||
|row| {
|
||||
let chat_id: ChatId = row.get(0)?;
|
||||
let size: f64 = row.get(1)?;
|
||||
@@ -2897,6 +2898,9 @@ pub enum Direction {
|
||||
}
|
||||
|
||||
/// Searches next/previous message based on the given message and list of types.
|
||||
///
|
||||
/// Deprecated since 2023-10-03.
|
||||
#[deprecated(note = "use `get_chat_media` instead")]
|
||||
pub async fn get_next_media(
|
||||
context: &Context,
|
||||
curr_msg_id: MsgId,
|
||||
@@ -3528,6 +3532,8 @@ pub async fn forward_msgs(context: &Context, msg_ids: &[MsgId], chat_id: ChatId)
|
||||
msg.param.remove(Param::ForcePlaintext);
|
||||
msg.param.remove(Param::Cmd);
|
||||
msg.param.remove(Param::OverrideSenderDisplayname);
|
||||
msg.param.remove(Param::WebxdcDocument);
|
||||
msg.param.remove(Param::WebxdcDocumentTimestamp);
|
||||
msg.param.remove(Param::WebxdcSummary);
|
||||
msg.param.remove(Param::WebxdcSummaryTimestamp);
|
||||
msg.in_reply_to = None;
|
||||
|
||||
@@ -286,6 +286,12 @@ pub enum Config {
|
||||
#[strum(props(default = "60"))]
|
||||
ScanAllFoldersDebounceSecs,
|
||||
|
||||
/// Whether to avoid using IMAP IDLE even if the server supports it.
|
||||
///
|
||||
/// This is a developer option for testing "fake idle".
|
||||
#[strum(props(default = "0"))]
|
||||
DisableIdle,
|
||||
|
||||
/// Defines the max. size (in bytes) of messages downloaded automatically.
|
||||
/// 0 = no limit.
|
||||
#[strum(props(default = "0"))]
|
||||
@@ -465,6 +471,28 @@ impl Context {
|
||||
.set_raw_config(key.as_ref(), value.as_deref())
|
||||
.await?;
|
||||
}
|
||||
Config::Socks5Enabled
|
||||
| Config::BccSelf
|
||||
| Config::E2eeEnabled
|
||||
| Config::MdnsEnabled
|
||||
| Config::SentboxWatch
|
||||
| Config::MvboxMove
|
||||
| Config::OnlyFetchMvbox
|
||||
| Config::FetchExistingMsgs
|
||||
| Config::DeleteToTrash
|
||||
| Config::SaveMimeHeaders
|
||||
| Config::Configured
|
||||
| Config::Bot
|
||||
| Config::NotifyAboutWrongPw
|
||||
| Config::SendSyncMsgs
|
||||
| Config::SignUnencrypted
|
||||
| Config::DisableIdle => {
|
||||
ensure!(
|
||||
matches!(value, None | Some("0") | Some("1")),
|
||||
"Boolean value must be either 0 or 1"
|
||||
);
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
}
|
||||
_ => {
|
||||
self.sql.set_raw_config(key.as_ref(), value).await?;
|
||||
}
|
||||
@@ -614,6 +642,18 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
/// Tests that "bot" config can only be set to "0" or "1".
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_config_bot() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
assert!(t.set_config(Config::Bot, None).await.is_ok());
|
||||
assert!(t.set_config(Config::Bot, Some("0")).await.is_ok());
|
||||
assert!(t.set_config(Config::Bot, Some("1")).await.is_ok());
|
||||
assert!(t.set_config(Config::Bot, Some("2")).await.is_err());
|
||||
assert!(t.set_config(Config::Bot, Some("Foobar")).await.is_err());
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_media_quality_config_option() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
@@ -109,7 +109,7 @@ impl ContactId {
|
||||
|
||||
/// ID of the contact for device messages.
|
||||
pub const DEVICE: ContactId = ContactId::new(5);
|
||||
const LAST_SPECIAL: ContactId = ContactId::new(9);
|
||||
pub(crate) const LAST_SPECIAL: ContactId = ContactId::new(9);
|
||||
|
||||
/// Address to go with [`ContactId::DEVICE`].
|
||||
///
|
||||
@@ -812,7 +812,11 @@ impl Contact {
|
||||
let mut ret = Vec::new();
|
||||
let flag_verified_only = (listflags & DC_GCL_VERIFIED_ONLY) != 0;
|
||||
let flag_add_self = (listflags & DC_GCL_ADD_SELF) != 0;
|
||||
|
||||
let minimal_origin = if context.get_config_bool(Config::Bot).await? {
|
||||
Origin::Unknown
|
||||
} else {
|
||||
Origin::IncomingReplyTo
|
||||
};
|
||||
if flag_verified_only || query.is_some() {
|
||||
let s3str_like_cmd = format!("%{}%", query.unwrap_or(""));
|
||||
context
|
||||
@@ -832,7 +836,7 @@ impl Contact {
|
||||
),
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
|
||||
ContactId::LAST_SPECIAL,
|
||||
Origin::IncomingReplyTo,
|
||||
minimal_origin,
|
||||
s3str_like_cmd,
|
||||
s3str_like_cmd,
|
||||
if flag_verified_only { 0i32 } else { 1i32 }
|
||||
@@ -882,10 +886,10 @@ impl Contact {
|
||||
ORDER BY last_seen DESC, id DESC;",
|
||||
sql::repeat_vars(self_addrs.len())
|
||||
),
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
|
||||
ContactId::LAST_SPECIAL,
|
||||
Origin::IncomingReplyTo
|
||||
])),
|
||||
rusqlite::params_from_iter(
|
||||
params_iter(&self_addrs)
|
||||
.chain(params_slice![ContactId::LAST_SPECIAL, minimal_origin]),
|
||||
),
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| {
|
||||
for id in ids {
|
||||
@@ -1252,11 +1256,22 @@ impl Contact {
|
||||
|
||||
/// Returns the ContactId that verified the contact.
|
||||
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
|
||||
let verifier_addr = self.get_verifier_addr(context).await?;
|
||||
if let Some(addr) = verifier_addr {
|
||||
Ok(Contact::lookup_id_by_addr(context, &addr, Origin::AddressBook).await?)
|
||||
} else {
|
||||
Ok(None)
|
||||
let Some(verifier_addr) = self.get_verifier_addr(context).await? else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if verifier_addr == self.addr {
|
||||
// Contact is directly verified via QR code.
|
||||
return Ok(Some(ContactId::SELF));
|
||||
}
|
||||
|
||||
match Contact::lookup_id_by_addr(context, &verifier_addr, Origin::AddressBook).await? {
|
||||
Some(contact_id) => Ok(Some(contact_id)),
|
||||
None => {
|
||||
let addr = &self.addr;
|
||||
warn!(context, "Could not lookup contact with address {verifier_addr} which introduced {addr}.");
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -382,7 +382,7 @@ impl Context {
|
||||
translated_stockstrings: stockstrings,
|
||||
events,
|
||||
scheduler: SchedulerState::new(),
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds.
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow at least 1 message every 10 seconds + a burst of 6.
|
||||
quota: RwLock::new(None),
|
||||
quota_update_request: AtomicBool::new(false),
|
||||
resync_request: AtomicBool::new(false),
|
||||
@@ -579,6 +579,7 @@ impl Context {
|
||||
let mdns_enabled = self.get_config_int(Config::MdnsEnabled).await?;
|
||||
let bcc_self = self.get_config_int(Config::BccSelf).await?;
|
||||
let send_sync_msgs = self.get_config_int(Config::SendSyncMsgs).await?;
|
||||
let disable_idle = self.get_config_bool(Config::DisableIdle).await?;
|
||||
|
||||
let prv_key_cnt = self.sql.count("SELECT COUNT(*) FROM keypairs;", ()).await?;
|
||||
|
||||
@@ -691,6 +692,7 @@ impl Context {
|
||||
);
|
||||
res.insert("bcc_self", bcc_self.to_string());
|
||||
res.insert("send_sync_msgs", send_sync_msgs.to_string());
|
||||
res.insert("disable_idle", disable_idle.to_string());
|
||||
res.insert("private_key_count", prv_key_cnt.to_string());
|
||||
res.insert("public_key_count", pub_key_cnt.to_string());
|
||||
res.insert("fingerprint", fingerprint_str);
|
||||
@@ -814,7 +816,22 @@ impl Context {
|
||||
pub async fn get_next_msgs(&self) -> Result<Vec<MsgId>> {
|
||||
let last_msg_id = match self.get_config(Config::LastMsgId).await? {
|
||||
Some(s) => MsgId::new(s.parse()?),
|
||||
None => MsgId::new_unset(),
|
||||
None => {
|
||||
// If `last_msg_id` is not set yet,
|
||||
// subtract 1 from the last id,
|
||||
// so a single message is returned and can
|
||||
// be marked as seen.
|
||||
self.sql
|
||||
.query_row(
|
||||
"SELECT IFNULL((SELECT MAX(id) - 1 FROM msgs), 0)",
|
||||
(),
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
Ok(msg_id)
|
||||
},
|
||||
)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
|
||||
let list = self
|
||||
|
||||
@@ -23,7 +23,7 @@ use crate::{job_try, stock_str, EventType};
|
||||
/// eg. to assign them to the correct chat.
|
||||
/// As these messages are typically small,
|
||||
/// they're caught by `MIN_DOWNLOAD_LIMIT`.
|
||||
const MIN_DOWNLOAD_LIMIT: u32 = 32768;
|
||||
pub(crate) const MIN_DOWNLOAD_LIMIT: u32 = 32768;
|
||||
|
||||
/// If a message is downloaded only partially
|
||||
/// and `delete_server_after` is set to small timeouts (eg. "at once"),
|
||||
|
||||
@@ -7,7 +7,9 @@ use futures_lite::FutureExt;
|
||||
|
||||
use super::session::Session;
|
||||
use super::Imap;
|
||||
use crate::config::Config;
|
||||
use crate::imap::{client::IMAP_TIMEOUT, FolderMeaning};
|
||||
use crate::log::LogExt;
|
||||
use crate::{context::Context, scheduler::InterruptInfo};
|
||||
|
||||
const IDLE_TIMEOUT: Duration = Duration::from_secs(23 * 60);
|
||||
@@ -21,6 +23,10 @@ impl Session {
|
||||
) -> Result<(Self, InterruptInfo)> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
if context.get_config_bool(Config::DisableIdle).await? {
|
||||
bail!("IMAP IDLE is disabled");
|
||||
}
|
||||
|
||||
if !self.can_idle() {
|
||||
bail!("IMAP server does not have IDLE capability");
|
||||
}
|
||||
@@ -163,7 +169,14 @@ impl Imap {
|
||||
continue;
|
||||
}
|
||||
if let Some(session) = &self.session {
|
||||
if session.can_idle() {
|
||||
if session.can_idle()
|
||||
&& !context
|
||||
.get_config_bool(Config::DisableIdle)
|
||||
.await
|
||||
.context("Failed to get disable_idle config")
|
||||
.log_err(context)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
// we only fake-idled because network was gone during IDLE, probably
|
||||
break InterruptInfo::new(false);
|
||||
}
|
||||
|
||||
@@ -110,6 +110,8 @@ pub mod tools;
|
||||
pub mod accounts;
|
||||
pub mod reaction;
|
||||
|
||||
pub use deltachat_message_parser as message_parser;
|
||||
|
||||
/// If set IMAP/incoming and SMTP/outgoing MIME messages will be printed.
|
||||
pub const DCC_MIME_DEBUG: &str = "DCC_MIME_DEBUG";
|
||||
|
||||
|
||||
@@ -328,13 +328,13 @@ pub async fn is_sending_locations_to_chat(
|
||||
}
|
||||
|
||||
/// Sets current location of the user device.
|
||||
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> bool {
|
||||
pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64) -> Result<bool> {
|
||||
if latitude == 0.0 && longitude == 0.0 {
|
||||
return true;
|
||||
return Ok(true);
|
||||
}
|
||||
let mut continue_streaming = false;
|
||||
|
||||
if let Ok(chats) = context
|
||||
let chats = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM chats WHERE locations_send_until>?;",
|
||||
@@ -346,33 +346,29 @@ pub async fn set(context: &Context, latitude: f64, longitude: f64, accuracy: f64
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
for chat_id in chats {
|
||||
if let Err(err) = context.sql.execute(
|
||||
"INSERT INTO locations \
|
||||
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
|
||||
(
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
time(),
|
||||
chat_id,
|
||||
ContactId::SELF,
|
||||
)
|
||||
).await {
|
||||
warn!(context, "failed to store location {:#}", err);
|
||||
} else {
|
||||
info!(context, "stored location for chat {}", chat_id);
|
||||
continue_streaming = true;
|
||||
}
|
||||
}
|
||||
if continue_streaming {
|
||||
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
|
||||
};
|
||||
}
|
||||
.await?;
|
||||
|
||||
continue_streaming
|
||||
for chat_id in chats {
|
||||
context.sql.execute(
|
||||
"INSERT INTO locations \
|
||||
(latitude, longitude, accuracy, timestamp, chat_id, from_id) VALUES (?,?,?,?,?,?);",
|
||||
(
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
time(),
|
||||
chat_id,
|
||||
ContactId::SELF,
|
||||
)).await.context("Failed to store location")?;
|
||||
|
||||
info!(context, "Stored location for chat {chat_id}.");
|
||||
continue_streaming = true;
|
||||
}
|
||||
if continue_streaming {
|
||||
context.emit_event(EventType::LocationChanged(Some(ContactId::SELF)));
|
||||
};
|
||||
|
||||
Ok(continue_streaming)
|
||||
}
|
||||
|
||||
/// Searches for locations in the given time range, optionally filtering by chat and contact IDs.
|
||||
@@ -464,7 +460,7 @@ pub async fn delete_all(context: &Context) -> Result<()> {
|
||||
}
|
||||
|
||||
/// Returns `location.kml` contents.
|
||||
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)> {
|
||||
pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<Option<(String, u32)>> {
|
||||
let mut last_added_location_id = 0;
|
||||
|
||||
let self_addr = context.get_primary_self_addr().await?;
|
||||
@@ -534,9 +530,11 @@ pub async fn get_kml(context: &Context, chat_id: ChatId) -> Result<(String, u32)
|
||||
ret += "</Document>\n</kml>";
|
||||
}
|
||||
|
||||
ensure!(location_count > 0, "No locations processed");
|
||||
|
||||
Ok((ret, last_added_location_id))
|
||||
if location_count > 0 {
|
||||
Ok(Some((ret, last_added_location_id)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn get_kml_timestamp(utc: i64) -> String {
|
||||
@@ -928,4 +926,38 @@ Content-Disposition: attachment; filename="location.kml"
|
||||
assert_eq!(locations.len(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_send_locations_to_chat() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
send_locations_to_chat(&alice, alice_chat.id, 1000).await?;
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let msg = bob.recv_msg(&sent).await;
|
||||
assert_eq!(msg.text, "Location streaming enabled by alice@example.org.");
|
||||
let bob_chat_id = msg.chat_id;
|
||||
|
||||
assert_eq!(set(&alice, 10.0, 20.0, 1.0).await?, true);
|
||||
|
||||
// Send image without text.
|
||||
let file_name = "image.png";
|
||||
let bytes = include_bytes!("../test-data/image/logo.png");
|
||||
let file = alice.get_blobdir().join(file_name);
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
let sent = alice.send_msg(alice_chat.id, &mut msg).await;
|
||||
|
||||
let msg = bob.recv_msg_opt(&sent).await.unwrap();
|
||||
assert!(msg.chat_id == bob_chat_id);
|
||||
assert_eq!(msg.msg_ids.len(), 1);
|
||||
|
||||
let bob_msg = Message::load_from_db(&bob, *msg.msg_ids.get(0).unwrap()).await?;
|
||||
assert_eq!(bob_msg.chat_id, bob_chat_id);
|
||||
assert_eq!(bob_msg.viewtype, Viewtype::Image);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -678,6 +678,12 @@ impl<'a> MimeFactory<'a> {
|
||||
})
|
||||
};
|
||||
|
||||
let get_content_type_directives_header = || {
|
||||
(
|
||||
"Content-Type-Deltachat-Directives".to_string(),
|
||||
"protected-headers=\"v1\"".to_string(),
|
||||
)
|
||||
};
|
||||
let outer_message = if is_encrypted {
|
||||
headers.protected.push(from_header);
|
||||
|
||||
@@ -714,10 +720,7 @@ impl<'a> MimeFactory<'a> {
|
||||
if !existing_ct.ends_with(';') {
|
||||
existing_ct += ";";
|
||||
}
|
||||
let message = message.replace_header(Header::new(
|
||||
"Content-Type".to_string(),
|
||||
format!("{existing_ct} protected-headers=\"v1\";"),
|
||||
));
|
||||
let message = message.header(get_content_type_directives_header());
|
||||
|
||||
// Set the appropriate Content-Type for the outer message
|
||||
let outer_message = PartBuilder::new().header((
|
||||
@@ -786,11 +789,12 @@ impl<'a> MimeFactory<'a> {
|
||||
{
|
||||
message
|
||||
} else {
|
||||
let message = message.header(get_content_type_directives_header());
|
||||
let (payload, signature) = encrypt_helper.sign(context, message).await?;
|
||||
PartBuilder::new()
|
||||
.header((
|
||||
"Content-Type".to_string(),
|
||||
"multipart/signed; protocol=\"application/pgp-signature\"".to_string(),
|
||||
"Content-Type",
|
||||
"multipart/signed; protocol=\"application/pgp-signature\"",
|
||||
))
|
||||
.child(payload)
|
||||
.child(
|
||||
@@ -860,9 +864,13 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
|
||||
/// Returns MIME part with a `location.kml` attachment.
|
||||
async fn get_location_kml_part(&mut self, context: &Context) -> Result<PartBuilder> {
|
||||
let (kml_content, last_added_location_id) =
|
||||
location::get_kml(context, self.msg.chat_id).await?;
|
||||
async fn get_location_kml_part(&mut self, context: &Context) -> Result<Option<PartBuilder>> {
|
||||
let Some((kml_content, last_added_location_id)) =
|
||||
location::get_kml(context, self.msg.chat_id).await?
|
||||
else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let part = PartBuilder::new()
|
||||
.content_type(
|
||||
&"application/vnd.google-earth.kml+xml"
|
||||
@@ -878,7 +886,7 @@ impl<'a> MimeFactory<'a> {
|
||||
// otherwise, the independent location is already filed
|
||||
self.last_added_location_id = last_added_location_id;
|
||||
}
|
||||
Ok(part)
|
||||
Ok(Some(part))
|
||||
}
|
||||
|
||||
#[allow(clippy::cognitive_complexity)]
|
||||
@@ -1168,7 +1176,10 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
let flowed_text = format_flowed(final_text);
|
||||
|
||||
let footer = &self.selfstatus;
|
||||
let is_reaction = self.msg.param.get_int(Param::Reaction).unwrap_or_default() != 0;
|
||||
|
||||
let footer = if is_reaction { "" } else { &self.selfstatus };
|
||||
|
||||
let message_text = format!(
|
||||
"{}{}{}{}{}{}",
|
||||
fwdhint.unwrap_or_default(),
|
||||
@@ -1191,7 +1202,7 @@ impl<'a> MimeFactory<'a> {
|
||||
))
|
||||
.body(message_text);
|
||||
|
||||
if self.msg.param.get_int(Param::Reaction).unwrap_or_default() != 0 {
|
||||
if is_reaction {
|
||||
main_part = main_part.header(("Content-Disposition", "reaction"));
|
||||
}
|
||||
|
||||
@@ -1230,11 +1241,8 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
|
||||
if location::is_sending_locations_to_chat(context, Some(self.msg.chat_id)).await? {
|
||||
match self.get_location_kml_part(context).await {
|
||||
Ok(part) => parts.push(part),
|
||||
Err(err) => {
|
||||
warn!(context, "mimefactory: could not send location: {}", err);
|
||||
}
|
||||
if let Some(part) = self.get_location_kml_part(context).await? {
|
||||
parts.push(part);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1363,15 +1371,16 @@ impl<'a> MimeFactory<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns base64-encoded buffer `buf` split into 78-bytes long
|
||||
/// Returns base64-encoded buffer `buf` split into 76-bytes long
|
||||
/// chunks separated by CRLF.
|
||||
///
|
||||
/// This line length limit is an
|
||||
/// [RFC5322 requirement](https://tools.ietf.org/html/rfc5322#section-2.1.1).
|
||||
/// [RFC2045 specification of base64 Content-Transfer-Encoding](https://datatracker.ietf.org/doc/html/rfc2045#section-6.8)
|
||||
/// says that "The encoded output stream must be represented in lines of no more than 76 characters each."
|
||||
/// Longer lines trigger `BASE64_LENGTH_78_79` rule of SpamAssassin.
|
||||
pub(crate) fn wrapped_base64_encode(buf: &[u8]) -> String {
|
||||
let base64 = base64::engine::general_purpose::STANDARD.encode(buf);
|
||||
let mut chars = base64.chars();
|
||||
std::iter::repeat_with(|| chars.by_ref().take(78).collect::<String>())
|
||||
std::iter::repeat_with(|| chars.by_ref().take(76).collect::<String>())
|
||||
.take_while(|s| !s.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\r\n")
|
||||
@@ -1530,6 +1539,7 @@ fn maybe_encode_words(words: &str) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use mailparse::{addrparse_header, MailHeaderMap};
|
||||
use std::str;
|
||||
|
||||
use super::*;
|
||||
use crate::chat::ChatId;
|
||||
@@ -1538,10 +1548,11 @@ mod tests {
|
||||
ProtectionStatus,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants;
|
||||
use crate::contact::{ContactAddress, Origin};
|
||||
use crate::mimeparser::MimeMessage;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{get_chat_msg, TestContext};
|
||||
use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};
|
||||
#[test]
|
||||
fn test_render_email_address() {
|
||||
let display_name = "ä space";
|
||||
@@ -1611,8 +1622,8 @@ mod tests {
|
||||
fn test_wrapped_base64_encode() {
|
||||
let input = b"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA";
|
||||
let output =
|
||||
"QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU\r\n\
|
||||
FBQUFBQUFBQQ==";
|
||||
"QUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFB\r\n\
|
||||
QUFBQUFBQUFBQQ==";
|
||||
assert_eq!(wrapped_base64_encode(input), output);
|
||||
}
|
||||
|
||||
@@ -2190,7 +2201,11 @@ mod tests {
|
||||
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
|
||||
let part = payload.next().unwrap();
|
||||
assert_eq!(part.match_indices("multipart/mixed").count(), 1);
|
||||
assert_eq!(
|
||||
part.match_indices("multipart/mixed; protected-headers=\"v1\"")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 1);
|
||||
assert_eq!(part.match_indices("Autocrypt:").count(), 0);
|
||||
assert_eq!(part.match_indices("Chat-User-Avatar:").count(), 0);
|
||||
@@ -2302,4 +2317,37 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_protected_headers_directive() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let chat = tcm
|
||||
.send_recv_accept(&alice, &bob, "alice->bob")
|
||||
.await
|
||||
.chat_id;
|
||||
|
||||
// Now Bob can send an encrypted message to Alice.
|
||||
let mut msg = Message::new(Viewtype::File);
|
||||
// Long messages are truncated and MimeMessage::decoded_data is set for them. We need
|
||||
// decoded_data to check presense of the necessary headers.
|
||||
msg.set_text("a".repeat(constants::DC_DESIRED_TEXT_LEN + 1));
|
||||
msg.set_file_from_bytes(&bob, "foo.bar", "content".as_bytes(), None)
|
||||
.await?;
|
||||
let sent = bob.send_msg(chat, &mut msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
let mime = MimeMessage::from_bytes(&alice, sent.payload.as_bytes(), None).await?;
|
||||
let mut payload = str::from_utf8(&mime.decoded_data)?.splitn(2, "\r\n\r\n");
|
||||
let part = payload.next().unwrap();
|
||||
assert_eq!(
|
||||
part.match_indices("multipart/mixed; protected-headers=\"v1\"")
|
||||
.count(),
|
||||
1
|
||||
);
|
||||
assert_eq!(part.match_indices("Subject:").count(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,7 +110,7 @@ pub(crate) struct MimeMessage {
|
||||
|
||||
/// The decrypted, raw mime structure.
|
||||
///
|
||||
/// This is non-empty only if the message was actually encrypted. It is used
|
||||
/// This is non-empty iff `is_mime_modified` and the message was actually encrypted. It is used
|
||||
/// for e.g. late-parsing HTML.
|
||||
pub decoded_data: Vec<u8>,
|
||||
|
||||
@@ -804,18 +804,6 @@ impl MimeMessage {
|
||||
|
||||
// Boxed future to deal with recursion
|
||||
async move {
|
||||
if mail.ctype.params.get("protected-headers").is_some() {
|
||||
if mail.ctype.mimetype == "text/rfc822-headers" {
|
||||
warn!(
|
||||
context,
|
||||
"Protected headers found in text/rfc822-headers attachment: Will be ignored.",
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
warn!(context, "Ignoring nested protected headers");
|
||||
}
|
||||
|
||||
enum MimeS {
|
||||
Multiple,
|
||||
Single,
|
||||
@@ -852,7 +840,10 @@ impl MimeMessage {
|
||||
|
||||
self.parse_mime_recursive(context, &mail, is_related).await
|
||||
}
|
||||
MimeS::Single => self.add_single_part_if_known(context, mail, is_related).await,
|
||||
MimeS::Single => {
|
||||
self.add_single_part_if_known(context, mail, is_related)
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
.boxed()
|
||||
@@ -1442,33 +1433,36 @@ impl MimeMessage {
|
||||
let (report_fields, _) = mailparse::parse_headers(&report_body)?;
|
||||
|
||||
// must be present
|
||||
if let Some(_disposition) = report_fields.get_header_value(HeaderDef::Disposition) {
|
||||
let original_message_id = report_fields
|
||||
.get_header_value(HeaderDef::OriginalMessageId)
|
||||
// MS Exchange doesn't add an Original-Message-Id header. Instead, they put
|
||||
// the original message id into the In-Reply-To header:
|
||||
.or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
|
||||
.and_then(|v| parse_message_id(&v).ok());
|
||||
let additional_message_ids = report_fields
|
||||
.get_header_value(HeaderDef::AdditionalMessageIds)
|
||||
.map_or_else(Vec::new, |v| {
|
||||
v.split(' ')
|
||||
.filter_map(|s| parse_message_id(s).ok())
|
||||
.collect()
|
||||
});
|
||||
if report_fields
|
||||
.get_header_value(HeaderDef::Disposition)
|
||||
.is_none()
|
||||
{
|
||||
warn!(
|
||||
context,
|
||||
"Ignoring unknown disposition-notification, Message-Id: {:?}.",
|
||||
report_fields.get_header_value(HeaderDef::MessageId)
|
||||
);
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
return Ok(Some(Report {
|
||||
original_message_id,
|
||||
additional_message_ids,
|
||||
}));
|
||||
}
|
||||
warn!(
|
||||
context,
|
||||
"ignoring unknown disposition-notification, Message-Id: {:?}",
|
||||
report_fields.get_header_value(HeaderDef::MessageId)
|
||||
);
|
||||
let original_message_id = report_fields
|
||||
.get_header_value(HeaderDef::OriginalMessageId)
|
||||
// MS Exchange doesn't add an Original-Message-Id header. Instead, they put
|
||||
// the original message id into the In-Reply-To header:
|
||||
.or_else(|| report.headers.get_header_value(HeaderDef::InReplyTo))
|
||||
.and_then(|v| parse_message_id(&v).ok());
|
||||
let additional_message_ids = report_fields
|
||||
.get_header_value(HeaderDef::AdditionalMessageIds)
|
||||
.map_or_else(Vec::new, |v| {
|
||||
v.split(' ')
|
||||
.filter_map(|s| parse_message_id(s).ok())
|
||||
.collect()
|
||||
});
|
||||
|
||||
Ok(None)
|
||||
Ok(Some(Report {
|
||||
original_message_id,
|
||||
additional_message_ids,
|
||||
}))
|
||||
}
|
||||
|
||||
fn process_delivery_status(
|
||||
|
||||
@@ -4,12 +4,20 @@ use std::time::Duration;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use mime::Mime;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::socks::Socks5Config;
|
||||
|
||||
const HTTP_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
|
||||
static LETSENCRYPT_ROOT: Lazy<reqwest::tls::Certificate> = Lazy::new(|| {
|
||||
reqwest::tls::Certificate::from_der(include_bytes!(
|
||||
"../../assets/root-certificates/letsencrypt/isrgrootx1.der"
|
||||
))
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
/// HTTP(S) GET response.
|
||||
#[derive(Debug)]
|
||||
pub struct Response {
|
||||
@@ -79,7 +87,10 @@ async fn read_url_inner(context: &Context, url: &str) -> Result<reqwest::Respons
|
||||
}
|
||||
|
||||
pub(crate) fn get_client(socks5_config: Option<Socks5Config>) -> Result<reqwest::Client> {
|
||||
let builder = reqwest::ClientBuilder::new().timeout(HTTP_TIMEOUT);
|
||||
let builder = reqwest::ClientBuilder::new()
|
||||
.timeout(HTTP_TIMEOUT)
|
||||
.add_root_certificate(LETSENCRYPT_ROOT.clone());
|
||||
|
||||
let builder = if let Some(socks5_config) = socks5_config {
|
||||
let proxy = reqwest::Proxy::all(socks5_config.to_url())?;
|
||||
builder.proxy(proxy)
|
||||
|
||||
@@ -8,6 +8,7 @@ use trust_dns_resolver::{config, AsyncResolver, TokioAsyncResolver};
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::provider::data::{PROVIDER_DATA, PROVIDER_IDS};
|
||||
use crate::tools::EmailAddress;
|
||||
|
||||
/// Provider status according to manual testing.
|
||||
#[derive(Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive)]
|
||||
@@ -175,21 +176,30 @@ fn get_resolver() -> Result<TokioAsyncResolver> {
|
||||
Ok(resolver)
|
||||
}
|
||||
|
||||
/// Returns provider for the given an e-mail address.
|
||||
///
|
||||
/// Returns an error if provided address is not valid.
|
||||
pub async fn get_provider_info_by_addr(
|
||||
context: &Context,
|
||||
addr: &str,
|
||||
skip_mx: bool,
|
||||
) -> Result<Option<&'static Provider>> {
|
||||
let addr = EmailAddress::new(addr)?;
|
||||
|
||||
let provider = get_provider_info(context, &addr.domain, skip_mx).await;
|
||||
Ok(provider)
|
||||
}
|
||||
|
||||
/// Returns provider for the given domain.
|
||||
///
|
||||
/// This function looks up domain in offline database first. If not
|
||||
/// found, it queries MX record for the domain and looks up offline
|
||||
/// database for MX domains.
|
||||
///
|
||||
/// For compatibility, email address can be passed to this function
|
||||
/// instead of the domain.
|
||||
pub async fn get_provider_info(
|
||||
context: &Context,
|
||||
domain: &str,
|
||||
skip_mx: bool,
|
||||
) -> Option<&'static Provider> {
|
||||
let domain = domain.rsplit('@').next()?;
|
||||
|
||||
if let Some(provider) = get_provider_by_domain(domain) {
|
||||
return Some(provider);
|
||||
}
|
||||
@@ -314,15 +324,25 @@ mod tests {
|
||||
let t = TestContext::new().await;
|
||||
assert!(get_provider_info(&t, "", false).await.is_none());
|
||||
assert!(get_provider_info(&t, "google.com", false).await.unwrap().id == "gmail");
|
||||
assert!(get_provider_info(&t, "example@google.com", false)
|
||||
.await
|
||||
.is_none());
|
||||
}
|
||||
|
||||
// get_provider_info() accepts email addresses for backwards compatibility
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_provider_info_by_addr() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
assert!(get_provider_info_by_addr(&t, "google.com", false)
|
||||
.await
|
||||
.is_err());
|
||||
assert!(
|
||||
get_provider_info(&t, "example@google.com", false)
|
||||
.await
|
||||
get_provider_info_by_addr(&t, "example@google.com", false)
|
||||
.await?
|
||||
.unwrap()
|
||||
.id
|
||||
== "gmail"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -431,6 +431,32 @@ Content-Disposition: reaction\n\
|
||||
assert_eq!(bob_reaction.emojis(), vec!["👍"]);
|
||||
assert_eq!(bob_reaction.as_str(), "👍");
|
||||
|
||||
// Alice receives reaction to her message from Bob with a footer.
|
||||
receive_imf(
|
||||
&alice,
|
||||
"To: alice@example.org\n\
|
||||
From: bob@example.net\n\
|
||||
Date: Today, 29 February 2021 00:00:10 -800\n\
|
||||
Message-ID: 56790@example.net\n\
|
||||
In-Reply-To: 12345@example.org\n\
|
||||
Subject: Meeting\n\
|
||||
Mime-Version: 1.0 (1.0)\n\
|
||||
Content-Type: text/plain; charset=utf-8\n\
|
||||
Content-Disposition: reaction\n\
|
||||
\n\
|
||||
😀\n\
|
||||
\n\
|
||||
--\n\
|
||||
_______________________________________________\n\
|
||||
Here's my footer -- bob@example.net"
|
||||
.as_bytes(),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let reactions = get_msg_reactions(&alice, msg.id).await?;
|
||||
assert_eq!(reactions.to_string(), "😀1");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -464,6 +490,16 @@ Content-Disposition: reaction\n\
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// Test that the status does not get mixed up into reactions.
|
||||
alice
|
||||
.set_config(
|
||||
Config::Selfstatus,
|
||||
Some("Buy Delta Chat today and make this banner go away!"),
|
||||
)
|
||||
.await?;
|
||||
bob.set_config(Config::Selfstatus, Some("Sent from my Delta Chat Pro. 👍"))
|
||||
.await?;
|
||||
|
||||
let chat_alice = alice.create_chat(&bob).await;
|
||||
let alice_msg = alice.send_text(chat_alice.id, "Hi!").await;
|
||||
let bob_msg = bob.recv_msg(&alice_msg).await;
|
||||
|
||||
@@ -35,6 +35,7 @@ use crate::param::{Param, Params};
|
||||
use crate::peerstate::{Peerstate, PeerstateKeyType, PeerstateVerifiedStatus};
|
||||
use crate::reaction::{set_msg_reaction, Reaction};
|
||||
use crate::securejoin::{self, handle_securejoin_handshake, observe_securejoin_on_other_device};
|
||||
use crate::simplify;
|
||||
use crate::sql;
|
||||
use crate::stock_str;
|
||||
use crate::tools::{
|
||||
@@ -76,6 +77,24 @@ pub async fn receive_imf(
|
||||
let mail = parse_mail(imf_raw).context("can't parse mail")?;
|
||||
let rfc724_mid =
|
||||
imap::prefetch_get_message_id(&mail.headers).unwrap_or_else(imap::create_message_id);
|
||||
if let Some(download_limit) = context.download_limit().await? {
|
||||
let download_limit: usize = download_limit.try_into()?;
|
||||
if imf_raw.len() > download_limit {
|
||||
let head = std::str::from_utf8(imf_raw)?
|
||||
.split("\r\n\r\n")
|
||||
.next()
|
||||
.context("No empty line in the message")?;
|
||||
return receive_imf_inner(
|
||||
context,
|
||||
&rfc724_mid,
|
||||
head.as_bytes(),
|
||||
seen,
|
||||
Some(imf_raw.len().try_into()?),
|
||||
false,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
}
|
||||
receive_imf_inner(context, &rfc724_mid, imf_raw, seen, None, false).await
|
||||
}
|
||||
|
||||
@@ -113,21 +132,20 @@ pub(crate) async fn receive_imf_inner(
|
||||
{
|
||||
Err(err) => {
|
||||
warn!(context, "receive_imf: can't parse MIME: {err:#}.");
|
||||
let msg_ids;
|
||||
if !rfc724_mid.starts_with(GENERATED_PREFIX) {
|
||||
let row_id = context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
|
||||
(rfc724_mid, DC_CHAT_ID_TRASH),
|
||||
)
|
||||
.await?;
|
||||
msg_ids = vec![MsgId::new(u32::try_from(row_id)?)];
|
||||
} else {
|
||||
return Ok(None);
|
||||
if rfc724_mid.starts_with(GENERATED_PREFIX) {
|
||||
// We don't have an rfc724_mid, there's no point in adding a trash entry
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let row_id = context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO msgs(rfc724_mid, chat_id) VALUES (?,?)",
|
||||
(rfc724_mid, DC_CHAT_ID_TRASH),
|
||||
)
|
||||
.await?;
|
||||
let msg_ids = vec![MsgId::new(u32::try_from(row_id)?)];
|
||||
|
||||
return Ok(Some(ReceivedMsg {
|
||||
chat_id: DC_CHAT_ID_TRASH,
|
||||
state: MessageState::Undefined,
|
||||
@@ -578,6 +596,7 @@ async fn add_parts(
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
|
||||
context,
|
||||
mime_parser,
|
||||
is_partial_download.is_some(),
|
||||
if test_normal_chat.is_none() {
|
||||
allow_creation
|
||||
} else {
|
||||
@@ -804,6 +823,7 @@ async fn add_parts(
|
||||
if let Some((new_chat_id, new_chat_id_blocked)) = create_or_lookup_group(
|
||||
context,
|
||||
mime_parser,
|
||||
is_partial_download.is_some(),
|
||||
allow_creation,
|
||||
Blocked::Not,
|
||||
from_id,
|
||||
@@ -1093,12 +1113,13 @@ async fn add_parts(
|
||||
|
||||
for part in &mut mime_parser.parts {
|
||||
if part.is_reaction {
|
||||
let reaction_str = simplify::remove_footers(part.msg.as_str());
|
||||
set_msg_reaction(
|
||||
context,
|
||||
&mime_in_reply_to,
|
||||
orig_chat_id.unwrap_or_default(),
|
||||
from_id,
|
||||
Reaction::from(part.msg.as_str()),
|
||||
Reaction::from(reaction_str.as_str()),
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
@@ -1129,7 +1150,8 @@ async fn add_parts(
|
||||
(&part.msg, part.typ)
|
||||
};
|
||||
|
||||
let part_is_empty = part.msg.is_empty() && part.param.get(Param::Quote).is_none();
|
||||
let part_is_empty =
|
||||
typ == Viewtype::Text && msg.is_empty() && part.param.get(Param::Quote).is_none();
|
||||
let mime_modified = save_mime_modified && !part_is_empty;
|
||||
if mime_modified {
|
||||
// Avoid setting mime_modified for more than one part.
|
||||
@@ -1154,7 +1176,8 @@ async fn add_parts(
|
||||
|
||||
// If you change which information is skipped if the message is trashed,
|
||||
// also change `MsgId::trash()` and `delete_expired_messages()`
|
||||
let trash = chat_id.is_trash() || (is_location_kml && msg.is_empty());
|
||||
let trash =
|
||||
chat_id.is_trash() || (is_location_kml && msg.is_empty() && typ == Viewtype::Text);
|
||||
|
||||
let row_id = context
|
||||
.sql
|
||||
@@ -1183,8 +1206,8 @@ INSERT INTO msgs
|
||||
)
|
||||
ON CONFLICT (id) DO UPDATE
|
||||
SET rfc724_mid=excluded.rfc724_mid, chat_id=excluded.chat_id,
|
||||
from_id=excluded.from_id, to_id=excluded.to_id, timestamp=excluded.timestamp, timestamp_sent=excluded.timestamp_sent,
|
||||
timestamp_rcvd=excluded.timestamp_rcvd, type=excluded.type, state=excluded.state, msgrmsg=excluded.msgrmsg,
|
||||
from_id=excluded.from_id, to_id=excluded.to_id, timestamp_sent=excluded.timestamp_sent,
|
||||
type=excluded.type, msgrmsg=excluded.msgrmsg,
|
||||
txt=excluded.txt, subject=excluded.subject, txt_raw=excluded.txt_raw, param=excluded.param,
|
||||
bytes=excluded.bytes, mime_headers=excluded.mime_headers,
|
||||
mime_compressed=excluded.mime_compressed, mime_in_reply_to=excluded.mime_in_reply_to,
|
||||
@@ -1408,56 +1431,53 @@ async fn lookup_chat_by_reply(
|
||||
) -> Result<Option<(ChatId, Blocked)>> {
|
||||
// Try to assign message to the same chat as the parent message.
|
||||
|
||||
if let Some(parent) = parent {
|
||||
let parent_chat = Chat::load_from_db(context, parent.chat_id).await?;
|
||||
let Some(parent) = parent else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
if parent.download_state != DownloadState::Done
|
||||
// TODO (2023-09-12): Added for backward compatibility with versions that did not have
|
||||
// `DownloadState::Undecipherable`. Remove eventually with the comment in
|
||||
// `MimeMessage::from_bytes()`.
|
||||
|| parent
|
||||
.error
|
||||
.as_ref()
|
||||
.filter(|e| e.starts_with("Decrypting failed:"))
|
||||
.is_some()
|
||||
{
|
||||
// If the parent msg is not fully downloaded or undecipherable, it may have been
|
||||
// assigned to the wrong chat (they often get assigned to the 1:1 chat with the sender).
|
||||
return Ok(None);
|
||||
}
|
||||
let parent_chat = Chat::load_from_db(context, parent.chat_id).await?;
|
||||
|
||||
if parent_chat.id == DC_CHAT_ID_TRASH {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// If this was a private message just to self, it was probably a private reply.
|
||||
// It should not go into the group then, but into the private chat.
|
||||
if is_probably_private_reply(context, to_ids, from_id, mime_parser, parent_chat.id).await? {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// If the parent chat is a 1:1 chat, and the sender is a classical MUA and added
|
||||
// a new person to TO/CC, then the message should not go to the 1:1 chat, but to a
|
||||
// newly created ad-hoc group.
|
||||
if parent_chat.typ == Chattype::Single
|
||||
&& !mime_parser.has_chat_version()
|
||||
&& to_ids.len() > 1
|
||||
{
|
||||
let mut chat_contacts = chat::get_chat_contacts(context, parent_chat.id).await?;
|
||||
chat_contacts.push(ContactId::SELF);
|
||||
if to_ids.iter().any(|id| !chat_contacts.contains(id)) {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Assigning message to {} as it's a reply to {}.", parent_chat.id, parent.rfc724_mid
|
||||
);
|
||||
return Ok(Some((parent_chat.id, parent_chat.blocked)));
|
||||
if parent.download_state != DownloadState::Done
|
||||
// TODO (2023-09-12): Added for backward compatibility with versions that did not have
|
||||
// `DownloadState::Undecipherable`. Remove eventually with the comment in
|
||||
// `MimeMessage::from_bytes()`.
|
||||
|| parent
|
||||
.error
|
||||
.as_ref()
|
||||
.filter(|e| e.starts_with("Decrypting failed:"))
|
||||
.is_some()
|
||||
{
|
||||
// If the parent msg is not fully downloaded or undecipherable, it may have been
|
||||
// assigned to the wrong chat (they often get assigned to the 1:1 chat with the sender).
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
if parent_chat.id == DC_CHAT_ID_TRASH {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// If this was a private message just to self, it was probably a private reply.
|
||||
// It should not go into the group then, but into the private chat.
|
||||
if is_probably_private_reply(context, to_ids, from_id, mime_parser, parent_chat.id).await? {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// If the parent chat is a 1:1 chat, and the sender is a classical MUA and added
|
||||
// a new person to TO/CC, then the message should not go to the 1:1 chat, but to a
|
||||
// newly created ad-hoc group.
|
||||
if parent_chat.typ == Chattype::Single && !mime_parser.has_chat_version() && to_ids.len() > 1 {
|
||||
let mut chat_contacts = chat::get_chat_contacts(context, parent_chat.id).await?;
|
||||
chat_contacts.push(ContactId::SELF);
|
||||
if to_ids.iter().any(|id| !chat_contacts.contains(id)) {
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Assigning message to {} as it's a reply to {}.", parent_chat.id, parent.rfc724_mid
|
||||
);
|
||||
Ok(Some((parent_chat.id, parent_chat.blocked)))
|
||||
}
|
||||
|
||||
/// If this method returns true, the message shall be assigned to the 1:1 chat with the sender.
|
||||
@@ -1500,6 +1520,7 @@ async fn is_probably_private_reply(
|
||||
async fn create_or_lookup_group(
|
||||
context: &Context,
|
||||
mime_parser: &mut MimeMessage,
|
||||
is_partial_download: bool,
|
||||
allow_creation: bool,
|
||||
create_blocked: Blocked,
|
||||
from_id: ContactId,
|
||||
@@ -1630,7 +1651,7 @@ async fn create_or_lookup_group(
|
||||
|
||||
if let Some(chat_id) = chat_id {
|
||||
Ok(Some((chat_id, chat_id_blocked)))
|
||||
} else if mime_parser.decrypting_failed {
|
||||
} else if is_partial_download || mime_parser.decrypting_failed {
|
||||
// It is possible that the message was sent to a valid,
|
||||
// yet unknown group, which was rejected because
|
||||
// Chat-Group-Name, which is in the encrypted part, was
|
||||
@@ -1663,7 +1684,7 @@ async fn apply_group_changes(
|
||||
}
|
||||
|
||||
let mut send_event_chat_modified = false;
|
||||
let mut removed_id = None;
|
||||
let (mut removed_id, mut added_id) = (None, None);
|
||||
let mut better_msg = None;
|
||||
|
||||
// True if a Delta Chat client has explicitly added our current primary address.
|
||||
@@ -1674,8 +1695,9 @@ async fn apply_group_changes(
|
||||
false
|
||||
};
|
||||
|
||||
let is_from_in_chat = !chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await?
|
||||
|| chat::is_contact_in_chat(context, chat_id, from_id).await?;
|
||||
let mut chat_contacts = HashSet::from_iter(chat::get_chat_contacts(context, chat_id).await?);
|
||||
let is_from_in_chat =
|
||||
!chat_contacts.contains(&ContactId::SELF) || chat_contacts.contains(&from_id);
|
||||
|
||||
// Reject group membership changes from non-members and old changes.
|
||||
let allow_member_list_changes = is_from_in_chat
|
||||
@@ -1685,12 +1707,10 @@ async fn apply_group_changes(
|
||||
|
||||
// Whether to rebuild the member list from scratch.
|
||||
let recreate_member_list = {
|
||||
// Recreate member list if the message comes from a MUA as these messages do _not_ set add/remove headers.
|
||||
!mime_parser.has_chat_version()
|
||||
// Always recreate membership list if SELF has been added. The older versions of DC
|
||||
// don't always set "In-Reply-To" to the latest message they sent, but to the latest
|
||||
// delivered message (so it's a race), so we have this heuristic here.
|
||||
|| self_added
|
||||
// Always recreate membership list if SELF has been added. The older versions of DC
|
||||
// don't always set "In-Reply-To" to the latest message they sent, but to the latest
|
||||
// delivered message (so it's a race), so we have this heuristic here.
|
||||
self_added
|
||||
|| match mime_parser.get_header(HeaderDef::InReplyTo) {
|
||||
// If we don't know the referenced message, we missed some messages.
|
||||
// Maybe they added/removed members, so we need to recreate our member list.
|
||||
@@ -1716,14 +1736,8 @@ async fn apply_group_changes(
|
||||
Some(stock_str::msg_del_member_local(context, removed_addr, from_id).await)
|
||||
};
|
||||
|
||||
if let Some(contact_id) = removed_id {
|
||||
if allow_member_list_changes {
|
||||
// Remove a single member from the chat.
|
||||
if !recreate_member_list {
|
||||
chat::remove_from_chat_contacts_table(context, chat_id, contact_id).await?;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
} else {
|
||||
if removed_id.is_some() {
|
||||
if !allow_member_list_changes {
|
||||
info!(
|
||||
context,
|
||||
"Ignoring removal of {removed_addr:?} from {chat_id}."
|
||||
@@ -1736,13 +1750,11 @@ async fn apply_group_changes(
|
||||
better_msg = Some(stock_str::msg_add_member_local(context, added_addr, from_id).await);
|
||||
|
||||
if allow_member_list_changes {
|
||||
// Add a single member to the chat.
|
||||
if !recreate_member_list {
|
||||
if let Some(contact_id) =
|
||||
Contact::lookup_id_by_addr(context, added_addr, Origin::Unknown).await?
|
||||
{
|
||||
chat::add_to_chat_contacts_table(context, chat_id, &[contact_id]).await?;
|
||||
send_event_chat_modified = true;
|
||||
added_id = Some(contact_id);
|
||||
} else {
|
||||
warn!(context, "Added {added_addr:?} has no contact id.")
|
||||
}
|
||||
@@ -1809,46 +1821,76 @@ async fn apply_group_changes(
|
||||
}
|
||||
}
|
||||
|
||||
// Recreate the member list.
|
||||
if recreate_member_list {
|
||||
// Only delete old contacts if the sender is not a classical MUA user:
|
||||
// Classical MUA users usually don't intend to remove users from an email
|
||||
// thread, so if they removed a recipient then it was probably by accident.
|
||||
if mime_parser.has_chat_version() {
|
||||
if allow_member_list_changes {
|
||||
let mut new_members = HashSet::from_iter(to_ids.iter().copied());
|
||||
new_members.insert(ContactId::SELF);
|
||||
if !from_id.is_special() {
|
||||
new_members.insert(from_id);
|
||||
}
|
||||
|
||||
if !recreate_member_list {
|
||||
let diff: HashSet<ContactId> =
|
||||
chat_contacts.difference(&new_members).copied().collect();
|
||||
// Only delete old contacts if the sender is not a classical MUA user:
|
||||
// Classical MUA users usually don't intend to remove users from an email
|
||||
// thread, so if they removed a recipient then it was probably by accident.
|
||||
if mime_parser.has_chat_version() {
|
||||
// This is what provides group membership consistency: we remove group members
|
||||
// locally if we see a discrepancy with the "To" list in the received message as it
|
||||
// is better for privacy than adding absent members locally. But it shouldn't be a
|
||||
// big problem if somebody missed a member addition, because they will likely
|
||||
// recreate the member list from the next received message. The problem occurs only
|
||||
// if that "somebody" managed to reply earlier. Really, it's a problem for big
|
||||
// groups with high message rate, but let it be for now.
|
||||
if !diff.is_empty() {
|
||||
warn!(context, "Implicit removal of {diff:?} from chat {chat_id}.");
|
||||
}
|
||||
new_members = chat_contacts.difference(&diff).copied().collect();
|
||||
} else {
|
||||
new_members.extend(diff);
|
||||
}
|
||||
}
|
||||
if let Some(removed_id) = removed_id {
|
||||
new_members.remove(&removed_id);
|
||||
}
|
||||
if let Some(added_id) = added_id {
|
||||
new_members.insert(added_id);
|
||||
}
|
||||
if recreate_member_list {
|
||||
info!(
|
||||
context,
|
||||
"Recreating chat {chat_id} member list with {new_members:?}.",
|
||||
);
|
||||
}
|
||||
|
||||
if new_members != chat_contacts {
|
||||
let new_members_ref = &new_members;
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM chats_contacts WHERE chat_id=?;", (chat_id,))
|
||||
.transaction(move |transaction| {
|
||||
transaction
|
||||
.execute("DELETE FROM chats_contacts WHERE chat_id=?", (chat_id,))?;
|
||||
for contact_id in new_members_ref {
|
||||
transaction.execute(
|
||||
"INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)",
|
||||
(chat_id, contact_id),
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
chat_contacts = new_members;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
|
||||
let mut members_to_add = HashSet::new();
|
||||
members_to_add.extend(to_ids);
|
||||
members_to_add.insert(ContactId::SELF);
|
||||
|
||||
if !from_id.is_special() {
|
||||
members_to_add.insert(from_id);
|
||||
}
|
||||
|
||||
if let Some(removed_id) = removed_id {
|
||||
members_to_add.remove(&removed_id);
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"Recreating chat {chat_id} with members {members_to_add:?}."
|
||||
);
|
||||
|
||||
chat::add_to_chat_contacts_table(context, chat_id, &Vec::from_iter(members_to_add)).await?;
|
||||
send_event_chat_modified = true;
|
||||
}
|
||||
|
||||
if let Some(avatar_action) = &mime_parser.group_avatar {
|
||||
if !chat::is_contact_in_chat(context, chat_id, ContactId::SELF).await? {
|
||||
if !chat_contacts.contains(&ContactId::SELF) {
|
||||
warn!(
|
||||
context,
|
||||
"Received group avatar update for group chat {chat_id} we are not a member of."
|
||||
);
|
||||
} else if !chat::is_contact_in_chat(context, chat_id, from_id).await? {
|
||||
} else if !chat_contacts.contains(&from_id) {
|
||||
warn!(
|
||||
context,
|
||||
"Contact {from_id} attempts to modify group chat {chat_id} avatar without being a member.",
|
||||
@@ -2009,39 +2051,40 @@ async fn apply_mailinglist_changes(
|
||||
mime_parser: &MimeMessage,
|
||||
chat_id: ChatId,
|
||||
) -> Result<()> {
|
||||
if let Some(list_post) = &mime_parser.list_post {
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat.typ != Chattype::Mailinglist {
|
||||
let Some(list_post) = &mime_parser.list_post else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let mut chat = Chat::load_from_db(context, chat_id).await?;
|
||||
if chat.typ != Chattype::Mailinglist {
|
||||
return Ok(());
|
||||
}
|
||||
let listid = &chat.grpid;
|
||||
|
||||
let list_post = match ContactAddress::new(list_post) {
|
||||
Ok(list_post) => list_post,
|
||||
Err(err) => {
|
||||
warn!(context, "Invalid List-Post: {:#}.", err);
|
||||
return Ok(());
|
||||
}
|
||||
let listid = &chat.grpid;
|
||||
};
|
||||
let (contact_id, _) = Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?;
|
||||
let mut contact = Contact::get_by_id(context, contact_id).await?;
|
||||
if contact.param.get(Param::ListId) != Some(listid) {
|
||||
contact.param.set(Param::ListId, listid);
|
||||
contact.update_param(context).await?;
|
||||
}
|
||||
|
||||
let list_post = match ContactAddress::new(list_post) {
|
||||
Ok(list_post) => list_post,
|
||||
Err(err) => {
|
||||
warn!(context, "Invalid List-Post: {:#}.", err);
|
||||
return Ok(());
|
||||
}
|
||||
};
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(context, "", list_post, Origin::Hidden).await?;
|
||||
let mut contact = Contact::get_by_id(context, contact_id).await?;
|
||||
if contact.param.get(Param::ListId) != Some(listid) {
|
||||
contact.param.set(Param::ListId, listid);
|
||||
contact.update_param(context).await?;
|
||||
}
|
||||
|
||||
if let Some(old_list_post) = chat.param.get(Param::ListPost) {
|
||||
if list_post.as_ref() != old_list_post {
|
||||
// Apparently the mailing list is using a different List-Post header in each message.
|
||||
// Make the mailing list read-only because we wouldn't know which message the user wants to reply to.
|
||||
chat.param.remove(Param::ListPost);
|
||||
chat.update_param(context).await?;
|
||||
}
|
||||
} else {
|
||||
chat.param.set(Param::ListPost, list_post);
|
||||
if let Some(old_list_post) = chat.param.get(Param::ListPost) {
|
||||
if list_post.as_ref() != old_list_post {
|
||||
// Apparently the mailing list is using a different List-Post header in each message.
|
||||
// Make the mailing list read-only because we wouldn't know which message the user wants to reply to.
|
||||
chat.param.remove(Param::ListPost);
|
||||
chat.update_param(context).await?;
|
||||
}
|
||||
} else {
|
||||
chat.param.set(Param::ListPost, list_post);
|
||||
chat.update_param(context).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -10,8 +10,9 @@ use crate::chat::{get_chat_msgs, ChatItem, ChatVisibility};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::constants::{DC_GCL_FOR_FORWARDING, DC_GCL_NO_SPECIALS};
|
||||
use crate::download::{DownloadState, MIN_DOWNLOAD_LIMIT};
|
||||
use crate::imap::prefetch_should_download;
|
||||
use crate::message::Message;
|
||||
use crate::message::{self, Message};
|
||||
use crate::test_utils::{get_chat_msg, TestContext, TestContextManager};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -2973,6 +2974,7 @@ async fn test_auto_accept_for_bots() -> Result<()> {
|
||||
let msg = t.get_last_msg().await;
|
||||
let chat = chat::Chat::load_from_db(&t, msg.chat_id).await?;
|
||||
assert!(!chat.is_contact_request());
|
||||
assert!(Contact::get_all(&t, 0, None).await?.len() == 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3369,20 +3371,15 @@ async fn test_dont_recreate_contacts_on_add_remove() -> Result<()> {
|
||||
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
|
||||
// bob didn't receive the addition of fiona, but alice doesn't overwrite her own
|
||||
// contact list with the one from bob which only has three members instead of four.
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 4);
|
||||
|
||||
// bob removes a member.
|
||||
remove_contact_from_chat(&bob, bob_chat_id, bob_blue).await?;
|
||||
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
|
||||
// Bobs chat only has two members after the removal of blue, because he still
|
||||
// didn't receive the addition of fiona. But that doesn't overwrite alice'
|
||||
// memberlist.
|
||||
// Bob didn't receive the addition of Fiona, so Alice must remove Fiona from the members list
|
||||
// back to make their group members view consistent.
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 3);
|
||||
|
||||
// Just a dumb check for remove_contact_from_chat(). Let's have it in this only place.
|
||||
remove_contact_from_chat(&bob, bob_chat_id, bob_blue).await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3514,6 +3511,29 @@ async fn test_mua_cant_remove() -> Result<()> {
|
||||
chat::get_chat_contacts(&alice, group_chat.id).await?.len(),
|
||||
4
|
||||
);
|
||||
|
||||
// But if the parent message is missing, the message must goto a new ad-hoc group.
|
||||
let bob_removes = receive_imf(
|
||||
&alice,
|
||||
b"Subject: Re: Message from alice\r\n\
|
||||
From: <bob@example.net>\r\n\
|
||||
To: <alice@example.org>, <claire@example.org>\r\n\
|
||||
Date: Mon, 12 Dec 2022 14:32:40 +0000\r\n\
|
||||
Message-ID: <bobs_answer_to_two_recipients_1@example.net>\r\n\
|
||||
In-Reply-To: <Mr.missing@example.org>\r\n\
|
||||
\r\n\
|
||||
Hi back!\r\n",
|
||||
false,
|
||||
)
|
||||
.await?
|
||||
.unwrap();
|
||||
assert_ne!(bob_removes.chat_id, alice_chat.id);
|
||||
let group_chat = Chat::load_from_db(&alice, bob_removes.chat_id).await?;
|
||||
assert_eq!(group_chat.typ, Chattype::Group);
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(&alice, group_chat.id).await?.len(),
|
||||
3,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -3679,3 +3699,114 @@ async fn test_keep_member_list_if_possibly_nomember() -> Result<()> {
|
||||
assert!(is_contact_in_chat(&bob, bob_chat_id, bob_alice_contact).await?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_download_later() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
alice.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
assert_eq!(alice.download_limit().await?, Some(MIN_DOWNLOAD_LIMIT));
|
||||
|
||||
let bob = tcm.bob().await;
|
||||
let bob_chat = bob.create_chat(&alice).await;
|
||||
let text = String::from_utf8(vec![b'a'; MIN_DOWNLOAD_LIMIT as usize])?;
|
||||
let sent_msg = bob.send_text(bob_chat.id, &text).await;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
|
||||
let hi_msg = tcm.send_recv(&bob, &alice, "hi").await;
|
||||
|
||||
alice.set_config(Config::DownloadLimit, None).await?;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
assert_eq!(alice.get_last_msg_in(msg.chat_id).await.id, hi_msg.id);
|
||||
assert!(msg.timestamp_sort <= hi_msg.timestamp_sort);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_create_group_with_big_msg() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
let ba_contact = Contact::create(
|
||||
&bob,
|
||||
"alice",
|
||||
&alice.get_config(Config::Addr).await?.unwrap(),
|
||||
)
|
||||
.await?;
|
||||
let file_bytes = include_bytes!("../../test-data/image/screenshot.png");
|
||||
|
||||
let bob_grp_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "Group").await?;
|
||||
add_contact_to_chat(&bob, bob_grp_id, ba_contact).await?;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)
|
||||
.await?;
|
||||
let sent_msg = bob.send_msg(bob_grp_id, &mut msg).await;
|
||||
assert!(!msg.get_showpadlock());
|
||||
|
||||
alice.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
assert_eq!(alice.download_limit().await?, Some(MIN_DOWNLOAD_LIMIT));
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
let alice_grp = Chat::load_from_db(&alice, msg.chat_id).await?;
|
||||
assert_eq!(alice_grp.typ, Chattype::Group);
|
||||
assert_eq!(alice_grp.name, "Group");
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(&alice, alice_grp.id).await?.len(),
|
||||
2
|
||||
);
|
||||
|
||||
alice.set_config(Config::DownloadLimit, None).await?;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
assert_eq!(msg.viewtype, Viewtype::Image);
|
||||
assert_eq!(msg.chat_id, alice_grp.id);
|
||||
let alice_grp = Chat::load_from_db(&alice, msg.chat_id).await?;
|
||||
assert_eq!(alice_grp.typ, Chattype::Group);
|
||||
assert_eq!(alice_grp.name, "Group");
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(&alice, alice_grp.id).await?.len(),
|
||||
2
|
||||
);
|
||||
|
||||
let ab_chat_id = tcm.send_recv_accept(&alice, &bob, "hi").await.chat_id;
|
||||
// Now Bob can send encrypted messages to Alice.
|
||||
|
||||
let bob_grp_id = create_group_chat(&bob, ProtectionStatus::Unprotected, "Group1").await?;
|
||||
add_contact_to_chat(&bob, bob_grp_id, ba_contact).await?;
|
||||
let mut msg = Message::new(Viewtype::Image);
|
||||
msg.set_file_from_bytes(&bob, "a.jpg", file_bytes, None)
|
||||
.await?;
|
||||
let sent_msg = bob.send_msg(bob_grp_id, &mut msg).await;
|
||||
assert!(msg.get_showpadlock());
|
||||
|
||||
alice.set_config(Config::DownloadLimit, Some("1")).await?;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Available);
|
||||
// Until fully downloaded, an encrypted message must sit in the 1:1 chat.
|
||||
assert_eq!(msg.chat_id, ab_chat_id);
|
||||
|
||||
alice.set_config(Config::DownloadLimit, None).await?;
|
||||
let msg = alice.recv_msg(&sent_msg).await;
|
||||
assert_eq!(msg.download_state, DownloadState::Done);
|
||||
assert_eq!(msg.state, MessageState::InFresh);
|
||||
assert_eq!(msg.viewtype, Viewtype::Image);
|
||||
assert_ne!(msg.chat_id, ab_chat_id);
|
||||
let alice_grp = Chat::load_from_db(&alice, msg.chat_id).await?;
|
||||
assert_eq!(alice_grp.typ, Chattype::Group);
|
||||
assert_eq!(alice_grp.name, "Group1");
|
||||
assert_eq!(
|
||||
chat::get_chat_contacts(&alice, alice_grp.id).await?.len(),
|
||||
2
|
||||
);
|
||||
|
||||
// The big message must go away from the 1:1 chat.
|
||||
assert_eq!(alice.get_last_msg_in(ab_chat_id).await.text, "hi");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -574,6 +574,19 @@ async fn fetch_idle(
|
||||
.await;
|
||||
}
|
||||
|
||||
if ctx
|
||||
.get_config_bool(Config::DisableIdle)
|
||||
.await
|
||||
.context("Failed to get disable_idle config")
|
||||
.log_err(ctx)
|
||||
.unwrap_or_default()
|
||||
{
|
||||
info!(ctx, "IMAP IDLE is disabled, going to fake idle.");
|
||||
return connection
|
||||
.fake_idle(ctx, Some(watch_folder), folder_meaning)
|
||||
.await;
|
||||
}
|
||||
|
||||
info!(ctx, "IMAP session supports IDLE, using it.");
|
||||
match session
|
||||
.idle(
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use core::fmt;
|
||||
use std::cmp::min;
|
||||
use std::{iter::once, ops::Deref, sync::Arc};
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
@@ -457,7 +458,8 @@ impl Context {
|
||||
} else {
|
||||
"green"
|
||||
};
|
||||
ret += &format!("<div class=\"bar\"><div class=\"progress {color}\" style=\"width: {percent}%\">{percent}%</div></div>");
|
||||
let div_width_percent = min(100, percent);
|
||||
ret += &format!("<div class=\"bar\"><div class=\"progress {color}\" style=\"width: {div_width_percent}%\">{percent}%</div></div>");
|
||||
|
||||
ret += "</li>";
|
||||
}
|
||||
|
||||
@@ -67,6 +67,15 @@ fn remove_nonstandard_footer<'a>(lines: &'a [&str]) -> (&'a [&'a str], bool) {
|
||||
(lines, false)
|
||||
}
|
||||
|
||||
/// Remove footers if any.
|
||||
/// This also makes all newlines "\n", but why not.
|
||||
pub(crate) fn remove_footers(msg: &str) -> String {
|
||||
let lines = split_lines(msg);
|
||||
let lines = remove_message_footer(&lines).0;
|
||||
let lines = remove_nonstandard_footer(lines).0;
|
||||
lines.join("\n")
|
||||
}
|
||||
|
||||
pub(crate) fn split_lines(buf: &str) -> Vec<&str> {
|
||||
buf.split('\n').collect()
|
||||
}
|
||||
|
||||
@@ -108,9 +108,15 @@ impl TestContextManager {
|
||||
/// - Let one TestContext send a message
|
||||
/// - Let the other TestContext receive it and accept the chat
|
||||
/// - Assert that the message arrived
|
||||
pub async fn send_recv_accept(&self, from: &TestContext, to: &TestContext, msg: &str) {
|
||||
pub async fn send_recv_accept(
|
||||
&self,
|
||||
from: &TestContext,
|
||||
to: &TestContext,
|
||||
msg: &str,
|
||||
) -> Message {
|
||||
let received_msg = self.send_recv(from, to, msg).await;
|
||||
received_msg.chat_id.accept(to).await.unwrap();
|
||||
received_msg
|
||||
}
|
||||
|
||||
/// - Let one TestContext send a message
|
||||
|
||||
@@ -26,7 +26,6 @@ use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
use crate::chat::Chat;
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::ContactId;
|
||||
@@ -846,35 +845,6 @@ impl Message {
|
||||
}
|
||||
}
|
||||
|
||||
/// Replaces WebXDC blob of existing message.
|
||||
///
|
||||
/// This API is supposed to be called from within a WebXDC to replace itself
|
||||
/// e.g. with an updated or persistently reconfigured version.
|
||||
pub async fn replace_webxdc(context: &Context, msg_id: MsgId, data: &[u8]) -> Result<()> {
|
||||
let mut msg = Message::load_from_db(context, msg_id).await?;
|
||||
|
||||
ensure!(
|
||||
msg.get_viewtype() == Viewtype::Webxdc,
|
||||
"Message {msg_id} is not a WebXDC instance"
|
||||
);
|
||||
|
||||
let blob = BlobObject::create(
|
||||
context,
|
||||
&msg.get_filename()
|
||||
.context("Cannot get filename of exising WebXDC instance")?,
|
||||
data,
|
||||
)
|
||||
.await
|
||||
.context("Failed to create WebXDC replacement blob")?;
|
||||
|
||||
let mut param = msg.param.clone();
|
||||
param.set(Param::File, blob.as_name());
|
||||
msg.param = param;
|
||||
msg.update_param(context).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
@@ -1042,7 +1012,7 @@ mod tests {
|
||||
let instance = send_webxdc_instance(&t, chat_id).await?;
|
||||
t.send_webxdc_status_update(
|
||||
instance.id,
|
||||
r#"{"info": "foo", "summary":"bar", "payload": 42}"#,
|
||||
r#"{"info": "foo", "summary":"bar", "document":"doc", "payload": 42}"#,
|
||||
"descr",
|
||||
)
|
||||
.await?;
|
||||
@@ -1050,7 +1020,7 @@ mod tests {
|
||||
assert_eq!(
|
||||
t.get_webxdc_status_updates(instance.id, StatusUpdateSerial(0))
|
||||
.await?,
|
||||
r#"[{"payload":42,"info":"foo","summary":"bar","serial":1,"max_serial":1}]"#
|
||||
r#"[{"payload":42,"info":"foo","document":"doc","summary":"bar","serial":1,"max_serial":1}]"#
|
||||
);
|
||||
assert_eq!(chat_id.get_msg_cnt(&t).await?, 2); // instance and info
|
||||
let info = Message::load_from_db(&t, instance.id)
|
||||
@@ -1058,6 +1028,7 @@ mod tests {
|
||||
.get_webxdc_info(&t)
|
||||
.await?;
|
||||
assert_eq!(info.summary, "bar".to_string());
|
||||
assert_eq!(info.document, "doc".to_string());
|
||||
|
||||
// forwarding an instance creates a fresh instance; updates etc. are not forwarded
|
||||
forward_msgs(&t, &[instance.get_id()], chat_id).await?;
|
||||
@@ -1074,6 +1045,7 @@ mod tests {
|
||||
.get_webxdc_info(&t)
|
||||
.await?;
|
||||
assert_eq!(info.summary, "".to_string());
|
||||
assert_eq!(info.document, "".to_string());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2653,62 +2625,4 @@ sth_for_the = "future""#
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests replacing WebXDC with a newer version.
|
||||
///
|
||||
/// Updates should be preserved after upgrading.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_replace_webxdc() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// Alice sends WebXDC instance.
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let mut alice_instance = create_webxdc_instance(
|
||||
&alice,
|
||||
"minimal.xdc",
|
||||
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
||||
)
|
||||
.await?;
|
||||
alice_instance.set_text("user added text".to_string());
|
||||
send_msg(&alice, alice_chat.id, &mut alice_instance).await?;
|
||||
let alice_instance = alice.get_last_msg().await;
|
||||
assert_eq!(alice_instance.get_text(), "user added text");
|
||||
|
||||
// Bob receives that instance.
|
||||
let alice_sent_instance = alice.pop_sent_msg().await;
|
||||
let bob_received_instance = bob.recv_msg(&alice_sent_instance).await;
|
||||
assert_eq!(bob_received_instance.get_text(), "user added text");
|
||||
|
||||
// Alice sends WebXDC update.
|
||||
alice
|
||||
.send_webxdc_status_update(alice_instance.id, r#"{"payload": 1}"#, "Alice update")
|
||||
.await?;
|
||||
alice.flush_status_updates().await?;
|
||||
let alice_sent_update = alice.pop_sent_msg().await;
|
||||
bob.recv_msg(&alice_sent_update).await;
|
||||
assert_eq!(
|
||||
bob.get_webxdc_status_updates(bob_received_instance.id, StatusUpdateSerial(0))
|
||||
.await?,
|
||||
r#"[{"payload":1,"serial":1,"max_serial":1}]"#
|
||||
);
|
||||
|
||||
// Bob replaces WebXDC.
|
||||
replace_webxdc(
|
||||
&bob,
|
||||
bob_received_instance.id,
|
||||
include_bytes!("../test-data/webxdc/with-minimal-manifest.xdc"),
|
||||
)
|
||||
.await
|
||||
.context("Failed to replace WebXDC")?;
|
||||
|
||||
// Updates are not modified.
|
||||
assert_eq!(
|
||||
bob.get_webxdc_status_updates(bob_received_instance.id, StatusUpdateSerial(0))
|
||||
.await?,
|
||||
r#"[{"payload":1,"serial":1,"max_serial":1}]"#
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user