Compare commits

..

1 Commits

Author SHA1 Message Date
link2xt
bf9eb35f53 feat: add scripts/zig-ffi.sh
This script can be used to compile the libraries
used in the Android application without the NDK.
2023-06-26 15:36:49 +00:00
136 changed files with 4067 additions and 8426 deletions

View File

@@ -15,7 +15,6 @@ on:
push:
branches:
- master
- stable
env:
RUSTFLAGS: -Dwarnings
@@ -25,7 +24,7 @@ jobs:
name: Lint Rust
runs-on: ubuntu-latest
env:
RUSTUP_TOOLCHAIN: 1.73.0
RUSTUP_TOOLCHAIN: 1.70.0
steps:
- uses: actions/checkout@v3
- name: Install rustfmt and clippy
@@ -81,11 +80,11 @@ jobs:
matrix:
include:
- os: ubuntu-latest
rust: 1.73.0
rust: 1.68.2
- os: windows-latest
rust: 1.73.0
rust: 1.68.2
- os: macos-latest
rust: 1.73.0
rust: 1.68.2
# Minimum Supported Rust Version = 1.65.0
#
@@ -182,15 +181,15 @@ jobs:
include:
# Currently used Rust version.
- os: ubuntu-latest
python: 3.12
python: 3.11
- os: macos-latest
python: 3.12
python: 3.11
# PyPy tests
- os: ubuntu-latest
python: pypy3.10
python: pypy3.9
- os: macos-latest
python: pypy3.10
python: pypy3.9
# Minimum Supported Python Version = 3.7
# This is the minimum version for which manylinux Python wheels are
@@ -232,19 +231,22 @@ jobs:
matrix:
include:
- os: ubuntu-latest
python: 3.12
python: 3.11
- os: macos-latest
python: 3.12
python: 3.11
# PyPy tests
- os: ubuntu-latest
python: pypy3.10
python: pypy3.9
- os: macos-latest
python: pypy3.10
python: pypy3.9
# Minimum Supported Python Version = 3.7
# 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>
- os: ubuntu-latest
python: 3.7
python: 3.8
runs-on: ${{ matrix.os }}
steps:

View File

@@ -67,9 +67,7 @@ jobs:
# Build Linux prebuilds inside a container with old glibc for backwards compatibility.
# Debian 10 contained glibc 2.28 at the time of the writing (2023-06-04): https://packages.debian.org/buster/libc6
# Ubuntu 18.04 is at the End of Standard Support since June 2023, but it contains glibc 2.27,
# so we are using it to support Ubuntu 18.04 setups that are still not upgraded.
container: ubuntu:18.04
container: debian:10
steps:
# Working directory is owned by 1001:1001 by default.
# Change it to our user.

3
.gitignore vendored
View File

@@ -18,9 +18,6 @@ python/.eggs
__pycache__
python/src/deltachat/capi*.so
python/.venv/
python/venv/
venv/
env/
python/liveconfig*

View File

@@ -1,245 +1,5 @@
# 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
- jsonrpc: Return only chat IDs for similar chats.
### Fixes
- Reopen all connections on database passpharse change.
- Do not block new group chats if 1:1 chat is blocked.
- Improve group membership consistency algorithm ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782))([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
- Forbid membership changes from possible non-members ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782)).
- `ChatId::parent_query()`: Don't filter out OutPending and OutFailed messages.
### Build system
- Update to OpenSSL 3.0.
- Bump webpki from 0.22.0 to 0.22.1.
- python: Add link to Mastodon into projects.urls.
### Features / Changes
- Add RSA-4096 key generation support.
### Refactor
- pgp: Add constants for encryption algorithm and hash.
## [1.121.0] - 2023-09-06
### API-Changes
- Add `dc_context_change_passphrase()`.
- Add `Message.set_file_from_bytes()` API.
- Add experimental API to get similar chats.
### Build system
- Build node packages on Ubuntu 18.04 instead of Debian 10.
This reduces the requirement for glibc version from 2.28 to 2.27.
### Fixes
- Allow membership changes by a MUA if we're not in the group ([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
- Save mime headers for messages not signed with a known key ([#4557](https://github.com/deltachat/deltachat-core-rust/pull/4557)).
- Return from `dc_get_chatlist(DC_GCL_FOR_FORWARDING)` only chats where we can send ([#4616](https://github.com/deltachat/deltachat-core-rust/pull/4616)).
- Do not allow dots at the end of email addresses.
- deltachat-rpc-client: Remove `aiodns` optional dependency from required dependencies.
`aiodns` depends on `pycares` which [fails to install in Termux](https://github.com/saghul/aiodns/issues/98).
## [1.120.0] - 2023-08-28
### API-Changes
- jsonrpc: Add `resend_messages`.
### Fixes
- Update async-imap to 0.9.1 to fix memory leak.
- Delete messages from SMTP queue only on user demand ([#4579](https://github.com/deltachat/deltachat-core-rust/pull/4579)).
- Do not send images without transparency as stickers ([#4611](https://github.com/deltachat/deltachat-core-rust/pull/4611)).
- `prepare_msg_blob()`: do not use the image if it has Exif metadata but the image cannot be recoded.
### Refactor
- Hide accounts.rs constants from public API.
- Hide pgp module from public API.
### Build system
- Update to Zig 0.11.0.
- Update to Rust 1.72.0.
### CI
- Run on push to stable branch.
### Miscellaneous Tasks
- python: Fix lint errors.
- python: Fix `ruff` 0.0.286 warnings.
- Fix beta clippy warnings.
## [1.119.1] - 2023-08-06
Bugfix release attempting to fix the [iOS build error](https://github.com/deltachat/deltachat-core-rust/issues/4610).
### Features / Changes
- Guess message viewtype from "application/octet-stream" attachment extension ([#4378](https://github.com/deltachat/deltachat-core-rust/pull/4378)).
### Fixes
- Update `xattr` from 1.0.0 to 1.0.1 to fix UnsupportedPlatformError import.
### Tests
- webxdc: Ensure unknown WebXDC update properties do not result in an error.
## [1.119.0] - 2023-08-03
### Fixes
- imap: Avoid IMAP move loops when DeltaChat folder is aliased.
- imap: Do not resync IMAP after initial configuration.
- webxdc: Accept WebXDC updates in mailing lists.
- webxdc: Base64-encode WebXDC updates to prevent corruption of large unencrypted WebXDC updates.
- webxdc: Delete old webxdc status updates during housekeeping.
- Return valid MsgId from `receive_imf()` when the message is replaced.
- Emit MsgsChanged event with correct chat id for replaced messages.
- deltachat-rpc-server: Update tokio-tar to fix backup import.
### Features / Changes
- deltachat-rpc-client: Add `MSG_DELETED` constant.
- Make `dc_msg_get_filename()` return the original attachment filename ([#4309](https://github.com/deltachat/deltachat-core-rust/pull/4309)).
### API-Changes
- deltachat-rpc-client: Add `Account.{import,export}_backup` methods.
- deltachat-jsonrpc: Make `MessageObject.text` non-optional.
### Documentation
- Update default value for `show_emails` in `dc_set_config()` documentation.
### Refactor
- Improve IMAP logs.
### Tests
- Add basic import/export test for async python.
- Add `test_webxdc_download_on_demand`.
- Add tests for deletion of webxdc status-updates.
## [1.118.0] - 2023-07-07
### API-Changes
- [**breaking**] Remove `Contact::load_from_db()` in favor of `Contact::get_by_id()`.
- Add `Contact::get_by_id_optional()` API.
- [**breaking**] Make `Message.text` non-optional.
- [**breaking**] Replace `message::get_msg_info()` with `MsgId.get_info()`.
- Move `handle_mdn` and `handle_ndn` to mimeparser and make them private.
Previously `handle_mdn` was erroneously exposed in the public API.
- python: flatten the API of `deltachat` module.
### Fixes
- Use different member added/removal messages locally and on the network.
- Update tokio to 1.29.1 to fix core panic after sending 29 offline messages ([#4414](https://github.com/deltachat/deltachat-core-rust/issues/4414)).
- Make SVG avatar image work on more platforms (use `xlink:href`).
- Preserve indentation when converting plaintext to HTML.
- Do not run simplify() on dehtml() output.
- Rewrite member added/removed messages even if the change is not allowed PR ([#4529](https://github.com/deltachat/deltachat-core-rust/pull/4529)).
### Documentation
- Document how to regenerate Node.js constants before the release.
### Build system
- git-cliff: Do not fail if commit.footers is undefined.
### Other
- Dependency updates.
- Update MPL 2.0 license text.
- Add LICENSE file to deltachat-rpc-client.
- deltachat-rpc-client: Add Trove classifiers.
- python: Change bindings status to production/stable.
### Tests
- Add `make-python-testenv.sh` script.
## [1.117.0] - 2023-06-15
### Features
@@ -2870,12 +2630,3 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
[1.115.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.114.0...v1.115.0
[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.0...v1.116.0
[1.117.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.116.0...v1.117.0
[1.118.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.117.0...v1.118.0
[1.119.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.118.0...v1.119.0
[1.119.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.0...v1.119.1
[1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0
[1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0
[1.122.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.121.0...v1.122.0
[1.123.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.122.0...v1.123.0
[1.124.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.123.0...v1.124.0
[1.124.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.0...v1.124.1

1334
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat"
version = "1.124.1"
version = "1.117.0"
edition = "2021"
license = "MPL-2.0"
rust-version = "1.65"
@@ -36,7 +36,7 @@ ratelimit = { path = "./deltachat-ratelimit" }
anyhow = "1"
async-channel = "1.8.0"
async-imap = { version = "0.9.1", default-features = false, features = ["runtime-tokio"] }
async-imap = { version = "0.9.0", default-features = false, features = ["runtime-tokio"] }
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
@@ -59,8 +59,8 @@ lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master"
libc = "0.2"
mailparse = "0.14"
mime = "0.3.17"
num_cpus = "1.16"
num-derive = "0.4"
num_cpus = "1.15"
num-derive = "0.3"
num-traits = "0.2"
once_cell = "1.18.0"
percent-encoding = "2.3"
@@ -68,7 +68,7 @@ parking_lot = "0.12"
pgp = { version = "0.10", default-features = false }
pretty_env_logger = { version = "0.5", optional = true }
qrcodegen = "1.7.0"
quick-xml = "0.29"
quick-xml = "0.28"
rand = "0.8"
regex = "1.8"
reqwest = { version = "0.11.18", features = ["json"] }
@@ -80,8 +80,8 @@ serde = { version = "1.0", features = ["derive"] }
sha-1 = "0.10"
sha2 = "0.10"
smallvec = "1"
strum = "0.25"
strum_macros = "0.25"
strum = "0.24"
strum_macros = "0.24"
tagger = "4.3.4"
textwrap = "0.16.0"
thiserror = "1"
@@ -94,7 +94,6 @@ 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"
@@ -104,7 +103,7 @@ log = "0.4"
pretty_env_logger = "0.5"
proptest = { version = "1", default-features = false, features = ["std"] }
tempfile = "3"
testdir = "0.8.0"
testdir = "0.7.3"
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
pretty_assertions = "1.3.0"
@@ -155,4 +154,8 @@ 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"
]

View File

@@ -361,7 +361,7 @@ Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE

View File

@@ -67,11 +67,9 @@ body = """
- {% if commit.breaking %}[**breaking**] {% endif %}\
{% if commit.scope %}{{ commit.scope }}: {% endif %}\
{{ commit.message | upper_first }}.\
{% if commit.footers is defined %}\
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
{% raw %} {% endraw %}- {{ footer.value }}\
{% endif %}{% endfor %}\
{% endif%}\
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
{% raw %} {% endraw %}- {{ footer.value }}\
{% endif %}{% endfor %}\
{% endfor %}
{% endfor %}\n
"""

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat_ffi"
version = "1.124.1"
version = "1.117.0"
description = "Deltachat FFI"
edition = "2018"
readme = "README.md"

View File

@@ -301,19 +301,6 @@ dc_context_t* dc_context_new_closed (const char* dbfile);
int dc_context_open (dc_context_t *context, const char* passphrase);
/**
* Changes the passphrase on the open database.
* Existing database must already be encrypted and the passphrase cannot be NULL or empty.
* It is impossible to encrypt unencrypted database with this method and vice versa.
*
* @memberof dc_context_t
* @param context The context object.
* @param passphrase The new passphrase.
* @return 1 on success, 0 on error.
*/
int dc_context_change_passphrase (dc_context_t* context, const char* passphrase);
/**
* Returns 1 if database is open.
*
@@ -433,19 +420,17 @@ char* dc_get_blobdir (const dc_context_t* context);
* 0=watch all folders normally (default)
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
* show direct replies to chats only,
* show direct replies to chats only (default),
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
* also show all mails of confirmed contacts,
* DC_SHOW_EMAILS_ALL (2)=
* also show mails of unconfirmed contacts (default).
* also show mails of unconfirmed contacts.
* - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)=
* generate recommended key type (default),
* DC_KEY_GEN_RSA2048 (1)=
* generate RSA 2048 keypair
* DC_KEY_GEN_ED25519 (2)=
* generate Curve25519 keypair
* DC_KEY_GEN_RSA4096 (3)=
* generate RSA 4096 keypair
* generate Ed25519 keypair
* - `save_mime_headers` = 1=save mime headers
* and make dc_get_mime_headers() work for subsequent calls,
* 0=do not save mime headers (default)
@@ -492,9 +477,6 @@ 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.
@@ -827,7 +809,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 Ignored, actual public key is extracted from secret_data.
* @param public_data ASCII armored public key.
* @param secret_data ASCII armored secret key.
* @return 1 on success, 0 on failure.
*/
@@ -1339,20 +1321,6 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch
int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t chat_id);
/**
* Returns a list of similar chats.
*
* @warning This is an experimental API which may change or be removed in the future.
*
* @memberof dc_context_t
* @param context The context object as returned from dc_context_new().
* @param chat_id The ID of the chat for which to find similar chats.
* @return The list of similar chats.
* On errors, NULL is returned.
* Must be freed using dc_chatlist_unref() when no longer used.
*/
dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t chat_id);
/**
* Estimate the number of messages that will be deleted
@@ -1484,7 +1452,6 @@ 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.
@@ -2288,7 +2255,6 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
*
* - **DC_IMEX_IMPORT_SELF_KEYS** (2) - Import private keys found in the directory given as `param1`.
* The last imported key is made the default keys unless its name contains the string `legacy`. Public keys are not imported.
* If `param1` is a filename, import the private key from the file and make it the default.
*
* While dc_imex() returns immediately, the started job may take a while,
* you can stop it using dc_stop_ongoing_process(). During execution of the job,
@@ -3993,36 +3959,6 @@ 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.
@@ -4042,17 +3978,16 @@ char* dc_msg_get_parsed_text_as_json (const dc_msg_t* msg, int mode);
*/
char* dc_msg_get_subject (const dc_msg_t* msg);
/**
* Find out full path of the file associated with a message.
* Find out full path, file name and extension of the file associated with a
* message.
*
* Typically files are associated with images, videos, audios, documents.
* Plain text messages do not have a file.
* File name may be mangled. To obtain the original attachment filename use dc_msg_get_filename().
*
* @memberof dc_msg_t
* @param msg The message object.
* @return The full path (with file name and extension) of the file associated with the message.
* @return The full path, the file name, and the extension of the file associated with the message.
* If there is no file associated with the message, an empty string is returned.
* NULL is never returned and the returned value must be released using dc_str_unref().
*/
@@ -4060,13 +3995,14 @@ char* dc_msg_get_file (const dc_msg_t* msg);
/**
* Get an original attachment filename, with extension but without the path. To get the full path,
* use dc_msg_get_file().
* Get a base file name without the path. The base file name includes the extension; the path
* is not returned. To get the full path, use dc_msg_get_file().
*
* @memberof dc_msg_t
* @param msg The message object.
* @return The attachment filename. If there is no file associated with the message, an empty string
* is returned. The returned value must be released using dc_str_unref().
* @return The base file name plus the extension without part. If there is no file
* associated with the message, an empty string is returned. The returned
* value must be released using dc_str_unref().
*/
char* dc_msg_get_filename (const dc_msg_t* msg);
@@ -5081,7 +5017,6 @@ 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);
@@ -5094,7 +5029,7 @@ char* dc_contact_get_verifier_addr (dc_contact_t* contact);
* @memberof dc_contact_t
* @param contact The contact object.
* @return
* The contact ID of the verifier. If it is DC_CONTACT_ID_SELF,
* The `ContactId` of the verifiers address. If it is the same address as the contact itself,
* we verified the contact ourself. If it is 0, we don't have verifier information or
* the contact is not verified.
*/
@@ -5790,11 +5725,12 @@ 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 input JSON-RPC request.
* @param method JSON-RPC method name, e.g. `check_email_validity`.
* @param params JSON-RPC method parameters, e.g. `["alice@example.org"]`.
* @return JSON-RPC response as string, must be freed using dc_str_unref() after usage.
* If there is no response, NULL is returned.
* On error, NULL is returned.
*/
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input);
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *method, const char *params);
/**
* @class dc_event_emitter_t
@@ -6330,7 +6266,6 @@ void dc_event_unref(dc_event_t* event);
#define DC_KEY_GEN_DEFAULT 0
#define DC_KEY_GEN_RSA2048 1
#define DC_KEY_GEN_ED25519 2
#define DC_KEY_GEN_RSA4096 3
/**

View File

@@ -29,9 +29,8 @@ use deltachat::contact::{Contact, ContactId, Origin};
use deltachat::context::Context;
use deltachat::ephemeral::Timer as EphemeralTimer;
use deltachat::imex::BackupProvider;
use deltachat::key::{DcKey, DcSecretKey};
use deltachat::key::DcKey;
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};
@@ -168,24 +167,6 @@ pub unsafe extern "C" fn dc_context_open(
.unwrap_or(0)
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_change_passphrase(
context: *mut dc_context_t,
passphrase: *const libc::c_char,
) -> libc::c_int {
if context.is_null() {
eprintln!("ignoring careless call to dc_context_change_passphrase()");
return 0;
}
let ctx = &*context;
let passphrase = to_string_lossy(passphrase);
block_on(ctx.change_passphrase(passphrase))
.context("dc_context_change_passphrase() failed")
.log_err(ctx)
.is_ok() as libc::c_int
}
#[no_mangle]
pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int {
if context.is_null() {
@@ -806,7 +787,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() {
@@ -816,8 +797,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,
@@ -1261,30 +1242,6 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
})
}
#[no_mangle]
pub unsafe extern "C" fn dc_get_similar_chatlist(
context: *mut dc_context_t,
chat_id: u32,
) -> *mut dc_chatlist_t {
if context.is_null() {
eprintln!("ignoring careless call to dc_get_similar_chatlist()");
return ptr::null_mut();
}
let ctx = &*context;
let chat_id = ChatId::new(chat_id);
match block_on(chat_id.get_similar_chatlist(ctx))
.context("failed to get similar chatlist")
.log_err(ctx)
{
Ok(list) => {
let ffi_list = ChatlistWrapper { context, list };
Box::into_raw(Box::new(ffi_list))
}
Err(_) => ptr::null_mut(),
}
}
#[no_mangle]
pub unsafe extern "C" fn dc_estimate_deletion_cnt(
context: *mut dc_context_t,
@@ -1432,7 +1389,6 @@ 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,
@@ -1921,10 +1877,13 @@ pub unsafe extern "C" fn dc_get_msg_info(
return "".strdup();
}
let ctx = &*context;
let msg_id = MsgId::new(msg_id);
block_on(msg_id.get_info(ctx))
.unwrap_or_log_default(ctx, "failed to get msg id")
.strdup()
block_on(async move {
message::get_msg_info(ctx, MsgId::new(msg_id))
.await
.unwrap_or_log_default(ctx, "failed to get msg id")
.strdup()
})
}
#[no_mangle]
@@ -2569,12 +2528,7 @@ pub unsafe extern "C" fn dc_set_location(
}
let ctx = &*context;
block_on(async move {
location::set(ctx, latitude, longitude, accuracy)
.await
.log_err(ctx)
.unwrap_or_default()
}) as libc::c_int
block_on(location::set(ctx, latitude, longitude, accuracy)) as _
}
#[no_mangle]
@@ -3354,59 +3308,7 @@ pub unsafe extern "C" fn dc_msg_get_text(msg: *mut dc_msg_t) -> *mut libc::c_cha
return "".strdup();
}
let ffi_msg = &*msg;
ffi_msg.message.get_text().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()
}
ffi_msg.message.get_text().unwrap_or_default().strdup()
}
#[no_mangle]
@@ -3791,7 +3693,7 @@ pub unsafe extern "C" fn dc_msg_set_text(msg: *mut dc_msg_t, text: *const libc::
return;
}
let ffi_msg = &mut *msg;
ffi_msg.message.set_text(to_string_lossy(text))
ffi_msg.message.set_text(to_opt_string_lossy(text))
}
#[no_mangle]
@@ -4590,14 +4492,7 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
let ctx = &*context;
match block_on(provider::get_provider_info_by_addr(
ctx,
addr.as_str(),
true,
))
.log_err(ctx)
.unwrap_or_default()
{
match block_on(provider::get_provider_info(ctx, addr.as_str(), true)) {
Some(provider) => provider,
None => ptr::null_mut(),
}
@@ -4624,14 +4519,11 @@ 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_by_addr(
match block_on(provider::get_provider_info(
ctx,
addr.as_str(),
socks5_enabled,
))
.log_err(ctx)
.unwrap_or_default()
{
)) {
Some(provider) => provider,
None => ptr::null_mut(),
}
@@ -5079,7 +4971,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, RpcSession};
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcServer, RpcSession};
use super::*;
@@ -5155,24 +5047,25 @@ mod jsonrpc {
#[no_mangle]
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
input: *const libc::c_char,
method: *const libc::c_char,
params: *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 input = to_string_lossy(input);
let res = block_on(api.handle.process_incoming(&input));
let method = to_string_lossy(method);
let params = to_string_lossy(params);
let params: Option<yerpc::Params> = match serde_json::from_str(&params) {
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));
match res {
Some(message) => {
if let Ok(message) = serde_json::to_string(&message) {
message.strdup()
} else {
ptr::null_mut()
}
}
None => ptr::null_mut(),
Ok(res) => res.to_string().strdup(),
Err(_) => ptr::null_mut(),
}
}
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-jsonrpc"
version = "1.124.1"
version = "1.117.0"
description = "DeltaChat JSON-RPC API"
edition = "2021"
default-run = "deltachat-jsonrpc-server"
@@ -17,14 +17,14 @@ deltachat = { path = ".." }
num-traits = "0.2"
schemars = "0.8.11"
serde = { version = "1.0", features = ["derive"] }
tempfile = "3.6.0"
tempfile = "3.3.0"
log = "0.4"
async-channel = { version = "1.8.0" }
futures = { version = "0.3.28" }
serde_json = "1.0.99"
serde_json = "1.0.96"
yerpc = { version = "0.5.1", features = ["anyhow_expose", "openrpc"] }
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
tokio = { version = "1.29.1" }
tokio = { version = "1.28.2" }
sanitize-filename = "0.4"
walkdir = "2.3.3"
base64 = "0.21"
@@ -34,7 +34,7 @@ axum = { version = "0.6.18", optional = true, features = ["ws"] }
env_logger = { version = "0.10.0", optional = true }
[dev-dependencies]
tokio = { version = "1.29.1", features = ["full", "rt-multi-thread"] }
tokio = { version = "1.28.2", features = ["full", "rt-multi-thread"] }
[features]

View File

@@ -19,7 +19,9 @@ use deltachat::{
context::get_info,
ephemeral::Timer,
imex, location,
message::{self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype},
message::{
self, delete_msgs, get_msg_info, markseen_msgs, Message, MessageState, MsgId, Viewtype,
},
provider::get_provider_info,
qr,
qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg},
@@ -43,13 +45,11 @@ 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,
@@ -568,21 +568,6 @@ impl CommandApi {
Ok(l)
}
/// Returns chats similar to the given one.
///
/// Experimental API, subject to change without notice.
async fn get_similar_chat_ids(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
let ctx = self.get_context(account_id).await?;
let chat_id = ChatId::new(chat_id);
let list = chat_id
.get_similar_chat_ids(&ctx)
.await?
.into_iter()
.map(|(chat_id, _metric)| chat_id.to_u32())
.collect();
Ok(list)
}
async fn get_chatlist_items_by_entries(
&self,
account_id: u32,
@@ -916,7 +901,7 @@ impl CommandApi {
) -> Result<u32> {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(text);
msg.set_text(Some(text));
let message_id =
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?;
Ok(message_id.to_u32())
@@ -1083,31 +1068,6 @@ 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.
///
@@ -1159,7 +1119,7 @@ impl CommandApi {
/// max. text returned by dc_msg_get_text() (about 30000 characters).
async fn get_message_info(&self, account_id: u32, message_id: u32) -> Result<String> {
let ctx = self.get_context(account_id).await?;
MsgId::new(message_id).get_info(&ctx).await
get_msg_info(&ctx, MsgId::new(message_id)).await
}
/// Returns contacts that sent read receipts and the time of reading.
@@ -1380,7 +1340,7 @@ impl CommandApi {
) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let contact_id = ContactId::new(contact_id);
let contact = Contact::get_by_id(&ctx, contact_id).await?;
let contact = Contact::load_from_db(&ctx, contact_id).await?;
let addr = contact.get_addr();
Contact::create(&ctx, &name, addr).await?;
Ok(())
@@ -1454,10 +1414,6 @@ 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,
@@ -1755,20 +1711,6 @@ impl CommandApi {
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
}
/// Resend messages and make information available for newly added chat members.
/// Resending sends out the original message, however, recipients and webxdc-status may differ.
/// Clients that already have the original message can still ignore the resent message as
/// they have tracked the state by dedicated updates.
///
/// Some messages cannot be resent, eg. info-messages, drafts, already pending messages or messages that are not sent by SELF.
///
/// message_ids all message IDs that should be resend. All messages must belong to the same chat.
async fn resend_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
let ctx = self.get_context(account_id).await?;
let message_ids: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
chat::resend_msgs(&ctx, &message_ids).await
}
async fn send_sticker(
&self,
account_id: u32,
@@ -1825,7 +1767,9 @@ impl CommandApi {
} else {
Viewtype::Text
});
message.set_text(data.text.unwrap_or_default());
if data.text.is_some() {
message.set_text(data.text);
}
if data.html.is_some() {
message.set_html(data.html);
}
@@ -2006,7 +1950,7 @@ impl CommandApi {
let ctx = self.get_context(account_id).await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text(text);
msg.set_text(Some(text));
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
Ok(message_id.to_u32())
@@ -2029,7 +1973,9 @@ impl CommandApi {
} else {
Viewtype::Text
});
message.set_text(text.unwrap_or_default());
if text.is_some() {
message.set_text(text);
}
if let Some(file) = file {
message.set_file(file, None);
}
@@ -2073,7 +2019,9 @@ impl CommandApi {
} else {
Viewtype::Text
});
draft.set_text(text.unwrap_or_default());
if text.is_some() {
draft.set_text(text);
}
if let Some(file) = file {
draft.set_file(file, None);
}

View File

@@ -53,7 +53,7 @@ impl FullChat {
contacts.push(
ContactObject::try_from_dc_contact(
context,
Contact::get_by_id(context, *contact_id)
Contact::load_from_db(context, *contact_id)
.await
.context("failed to load contact")?,
)
@@ -74,7 +74,7 @@ impl FullChat {
let was_seen_recently = if chat.get_type() == Chattype::Single {
match contact_ids.get(0) {
Some(contact) => Contact::get_by_id(context, *contact)
Some(contact) => Contact::load_from_db(context, *contact)
.await
.context("failed to load contact for was_seen_recently")?
.was_seen_recently(),

View File

@@ -8,12 +8,15 @@ use deltachat::{
chatlist::Chatlist,
};
use num_traits::cast::ToPrimitive;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use typescript_type_def::TypeDef;
use super::color_int_to_hex_string;
use super::message::MessageViewtype;
#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)]
pub struct ChatListEntry(pub u32, pub u32);
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
#[serde(tag = "type")]
pub enum ChatListItemFetchResult {
@@ -104,7 +107,7 @@ pub(crate) async fn get_chat_list_item_by_id(
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
let contact = chat_contacts.get(0);
let was_seen_recently = match contact {
Some(contact) => Contact::get_by_id(ctx, *contact)
Some(contact) => Contact::load_from_db(ctx, *contact)
.await
.context("contact")?
.was_seen_recently(),

View File

@@ -34,7 +34,7 @@ pub struct MessageObject {
quote: Option<MessageQuote>,
parent_id: Option<u32>,
text: String,
text: Option<String>,
has_location: bool,
has_html: bool,
view_type: MessageViewtype,
@@ -113,7 +113,7 @@ impl MessageObject {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
let sender_contact = Contact::get_by_id(context, message.get_from_id())
let sender_contact = Contact::load_from_db(context, message.get_from_id())
.await
.context("failed to load sender contact")?;
let sender = ContactObject::try_from_dc_contact(context, sender_contact)
@@ -135,7 +135,7 @@ impl MessageObject {
let quote = if let Some(quoted_text) = message.quoted_text() {
match message.quoted_message(context).await? {
Some(quote) => {
let quote_author = Contact::get_by_id(context, quote.get_from_id())
let quote_author = Contact::load_from_db(context, quote.get_from_id())
.await
.context("failed to load quote author contact")?;
Some(MessageQuote::WithMessage {
@@ -318,7 +318,6 @@ pub enum DownloadState {
Done,
Available,
Failure,
Undecipherable,
InProgress,
}
@@ -328,7 +327,6 @@ impl From<download::DownloadState> for DownloadState {
download::DownloadState::Done => DownloadState::Done,
download::DownloadState::Available => DownloadState::Available,
download::DownloadState::Failure => DownloadState::Failure,
download::DownloadState::Undecipherable => DownloadState::Undecipherable,
download::DownloadState::InProgress => DownloadState::InProgress,
}
}
@@ -471,7 +469,7 @@ impl MessageSearchResult {
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
let message = Message::load_from_db(context, msg_id).await?;
let chat = Chat::load_from_db(context, message.get_chat_id()).await?;
let sender = Contact::get_by_id(context, message.get_from_id()).await?;
let sender = Contact::load_from_db(context, message.get_from_id()).await?;
let profile_image = match sender.get_profile_image(context).await? {
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
@@ -502,7 +500,7 @@ impl MessageSearchResult {
is_chat_protected: chat.is_protected(),
is_chat_contact_request: chat.is_contact_request(),
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
message: message.get_text(),
message: message.get_text().unwrap_or_default(),
timestamp: message.get_timestamp(),
})
}

View File

@@ -1,19 +0,0 @@
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),
}
}

View File

@@ -6,7 +6,6 @@ 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;

View File

@@ -55,5 +55,5 @@
},
"type": "module",
"types": "dist/deltachat.d.ts",
"version": "1.124.1"
"version": "1.117.0"
}

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-repl"
version = "1.124.1"
version = "1.117.0"
license = "MPL-2.0"
edition = "2021"
@@ -9,10 +9,10 @@ ansi_term = "0.12.1"
anyhow = "1"
deltachat = { path = "..", features = ["internals"]}
dirs = "5"
log = "0.4.19"
log = "0.4.18"
pretty_env_logger = "0.5"
rusqlite = "0.29"
rustyline = "12"
rustyline = "11"
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
[features]

View File

@@ -138,7 +138,11 @@ 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 let Ok(mut dir) = dir {
if dir.is_err() {
error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
return false;
} else {
let mut dir = dir.unwrap();
while let Ok(Some(entry)) = dir.next_entry().await {
let name_f = entry.file_name();
let name = name_f.to_string_lossy();
@@ -150,9 +154,6 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
}
}
}
} else {
error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
return false;
}
}
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
@@ -186,7 +187,6 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
DownloadState::Available => " [⬇ Download available]",
DownloadState::InProgress => " [⬇ Download in progress...]",
DownloadState::Failure => " [⬇ Download failed]",
DownloadState::Undecipherable => " [⬇ Decryption failed]",
};
let temp2 = timestamp_to_str(msg.get_timestamp());
@@ -199,7 +199,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
if msg.has_location() { "📍" } else { "" },
&contact_name,
contact_id,
msgtext,
msgtext.unwrap_or_default(),
if msg.has_html() { "[HAS-HTML]" } else { "" },
if msg.get_from_id() == ContactId::SELF {
""
@@ -805,30 +805,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
}
"chatinfo" => {
ensure!(sel_chat.is_some(), "No chat selected.");
let sel_chat_id = sel_chat.as_ref().unwrap().get_id();
let contacts = chat::get_chat_contacts(&context, sel_chat_id).await?;
let contacts =
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
println!("Memberlist:");
log_contactlist(&context, &contacts).await?;
println!("{} contacts", contacts.len());
let similar_chats = sel_chat_id.get_similar_chat_ids(&context).await?;
if !similar_chats.is_empty() {
println!("Similar chats: ");
for (similar_chat_id, metric) in similar_chats {
let similar_chat = Chat::load_from_db(&context, similar_chat_id).await?;
println!(
"{} (#{}) {:.1}",
similar_chat.name,
similar_chat_id,
100.0 * metric
);
}
}
println!(
"Location streaming: {}",
"{} contacts\nLocation streaming: {}",
contacts.len(),
location::is_sending_locations_to_chat(
&context,
Some(sel_chat.as_ref().unwrap().get_id())
@@ -893,7 +878,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 {
@@ -927,7 +912,9 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
Viewtype::File
});
msg.set_file(arg1, None);
msg.set_text(arg2.to_string());
if !arg2.is_empty() {
msg.set_text(Some(arg2.to_string()));
}
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
}
"sendhtml" => {
@@ -939,11 +926,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
let mut msg = Message::new(Viewtype::Text);
msg.set_html(Some(html.to_string()));
msg.set_text(if arg2.is_empty() {
msg.set_text(Some(if arg2.is_empty() {
path.file_name().unwrap().to_string_lossy().to_string()
} else {
arg2.to_string()
});
}));
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
}
"sendsyncmsg" => match context.send_sync_msg().await? {
@@ -992,7 +979,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
if !arg1.is_empty() {
let mut draft = Message::new(Viewtype::Text);
draft.set_text(arg1.to_string());
draft.set_text(Some(arg1.to_string()));
sel_chat
.as_ref()
.unwrap()
@@ -1016,7 +1003,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"Please specify text to add as device message."
);
let mut msg = Message::new(Viewtype::Text);
msg.set_text(arg1.to_string());
msg.set_text(Some(arg1.to_string()));
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
}
"listmedia" => {
@@ -1103,7 +1090,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
"msginfo" => {
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
let id = MsgId::new(arg1.parse()?);
let res = id.get_info(&context).await?;
let res = message::get_msg_info(&context, id).await?;
println!("{res}");
}
"download" => {

View File

@@ -1,373 +0,0 @@
Mozilla Public License Version 2.0
==================================
1. Definitions
--------------
1.1. "Contributor"
means each individual or legal entity that creates, contributes to
the creation of, or owns Covered Software.
1.2. "Contributor Version"
means the combination of the Contributions of others (if any) used
by a Contributor and that particular Contributor's Contribution.
1.3. "Contribution"
means Covered Software of a particular Contributor.
1.4. "Covered Software"
means Source Code Form to which the initial Contributor has attached
the notice in Exhibit A, the Executable Form of such Source Code
Form, and Modifications of such Source Code Form, in each case
including portions thereof.
1.5. "Incompatible With Secondary Licenses"
means
(a) that the initial Contributor has attached the notice described
in Exhibit B to the Covered Software; or
(b) that the Covered Software was made available under the terms of
version 1.1 or earlier of the License, but not also under the
terms of a Secondary License.
1.6. "Executable Form"
means any form of the work other than Source Code Form.
1.7. "Larger Work"
means a work that combines Covered Software with other material, in
a separate file or files, that is not Covered Software.
1.8. "License"
means this document.
1.9. "Licensable"
means having the right to grant, to the maximum extent possible,
whether at the time of the initial grant or subsequently, any and
all of the rights conveyed by this License.
1.10. "Modifications"
means any of the following:
(a) any file in Source Code Form that results from an addition to,
deletion from, or modification of the contents of Covered
Software; or
(b) any new file in Source Code Form that contains any Covered
Software.
1.11. "Patent Claims" of a Contributor
means any patent claim(s), including without limitation, method,
process, and apparatus claims, in any patent Licensable by such
Contributor that would be infringed, but for the grant of the
License, by the making, using, selling, offering for sale, having
made, import, or transfer of either its Contributions or its
Contributor Version.
1.12. "Secondary License"
means either the GNU General Public License, Version 2.0, the GNU
Lesser General Public License, Version 2.1, the GNU Affero General
Public License, Version 3.0, or any later versions of those
licenses.
1.13. "Source Code Form"
means the form of the work preferred for making modifications.
1.14. "You" (or "Your")
means an individual or a legal entity exercising rights under this
License. For legal entities, "You" includes any entity that
controls, is controlled by, or is under common control with You. For
purposes of this definition, "control" means (a) the power, direct
or indirect, to cause the direction or management of such entity,
whether by contract or otherwise, or (b) ownership of more than
fifty percent (50%) of the outstanding shares or beneficial
ownership of such entity.
2. License Grants and Conditions
--------------------------------
2.1. Grants
Each Contributor hereby grants You a world-wide, royalty-free,
non-exclusive license:
(a) under intellectual property rights (other than patent or trademark)
Licensable by such Contributor to use, reproduce, make available,
modify, display, perform, distribute, and otherwise exploit its
Contributions, either on an unmodified basis, with Modifications, or
as part of a Larger Work; and
(b) under Patent Claims of such Contributor to make, use, sell, offer
for sale, have made, import, and otherwise transfer either its
Contributions or its Contributor Version.
2.2. Effective Date
The licenses granted in Section 2.1 with respect to any Contribution
become effective for each Contribution on the date the Contributor first
distributes such Contribution.
2.3. Limitations on Grant Scope
The licenses granted in this Section 2 are the only rights granted under
this License. No additional rights or licenses will be implied from the
distribution or licensing of Covered Software under this License.
Notwithstanding Section 2.1(b) above, no patent license is granted by a
Contributor:
(a) for any code that a Contributor has removed from Covered Software;
or
(b) for infringements caused by: (i) Your and any other third party's
modifications of Covered Software, or (ii) the combination of its
Contributions with other software (except as part of its Contributor
Version); or
(c) under Patent Claims infringed by Covered Software in the absence of
its Contributions.
This License does not grant any rights in the trademarks, service marks,
or logos of any Contributor (except as may be necessary to comply with
the notice requirements in Section 3.4).
2.4. Subsequent Licenses
No Contributor makes additional grants as a result of Your choice to
distribute the Covered Software under a subsequent version of this
License (see Section 10.2) or under the terms of a Secondary License (if
permitted under the terms of Section 3.3).
2.5. Representation
Each Contributor represents that the Contributor believes its
Contributions are its original creation(s) or it has sufficient rights
to grant the rights to its Contributions conveyed by this License.
2.6. Fair Use
This License is not intended to limit any rights You have under
applicable copyright doctrines of fair use, fair dealing, or other
equivalents.
2.7. Conditions
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
in Section 2.1.
3. Responsibilities
-------------------
3.1. Distribution of Source Form
All distribution of Covered Software in Source Code Form, including any
Modifications that You create or to which You contribute, must be under
the terms of this License. You must inform recipients that the Source
Code Form of the Covered Software is governed by the terms of this
License, and how they can obtain a copy of this License. You may not
attempt to alter or restrict the recipients' rights in the Source Code
Form.
3.2. Distribution of Executable Form
If You distribute Covered Software in Executable Form then:
(a) such Covered Software must also be made available in Source Code
Form, as described in Section 3.1, and You must inform recipients of
the Executable Form how they can obtain a copy of such Source Code
Form by reasonable means in a timely manner, at a charge no more
than the cost of distribution to the recipient; and
(b) You may distribute such Executable Form under the terms of this
License, or sublicense it under different terms, provided that the
license for the Executable Form does not attempt to limit or alter
the recipients' rights in the Source Code Form under this License.
3.3. Distribution of a Larger Work
You may create and distribute a Larger Work under terms of Your choice,
provided that You also comply with the requirements of this License for
the Covered Software. If the Larger Work is a combination of Covered
Software with a work governed by one or more Secondary Licenses, and the
Covered Software is not Incompatible With Secondary Licenses, this
License permits You to additionally distribute such Covered Software
under the terms of such Secondary License(s), so that the recipient of
the Larger Work may, at their option, further distribute the Covered
Software under the terms of either this License or such Secondary
License(s).
3.4. Notices
You may not remove or alter the substance of any license notices
(including copyright notices, patent notices, disclaimers of warranty,
or limitations of liability) contained within the Source Code Form of
the Covered Software, except that You may alter any license notices to
the extent required to remedy known factual inaccuracies.
3.5. Application of Additional Terms
You may choose to offer, and to charge a fee for, warranty, support,
indemnity or liability obligations to one or more recipients of Covered
Software. However, You may do so only on Your own behalf, and not on
behalf of any Contributor. You must make it absolutely clear that any
such warranty, support, indemnity, or liability obligation is offered by
You alone, and You hereby agree to indemnify every Contributor for any
liability incurred by such Contributor as a result of warranty, support,
indemnity or liability terms You offer. You may include additional
disclaimers of warranty and limitations of liability specific to any
jurisdiction.
4. Inability to Comply Due to Statute or Regulation
---------------------------------------------------
If it is impossible for You to comply with any of the terms of this
License with respect to some or all of the Covered Software due to
statute, judicial order, or regulation then You must: (a) comply with
the terms of this License to the maximum extent possible; and (b)
describe the limitations and the code they affect. Such description must
be placed in a text file included with all distributions of the Covered
Software under this License. Except to the extent prohibited by statute
or regulation, such description must be sufficiently detailed for a
recipient of ordinary skill to be able to understand it.
5. Termination
--------------
5.1. The rights granted under this License will terminate automatically
if You fail to comply with any of its terms. However, if You become
compliant, then the rights granted under this License from a particular
Contributor are reinstated (a) provisionally, unless and until such
Contributor explicitly and finally terminates Your grants, and (b) on an
ongoing basis, if such Contributor fails to notify You of the
non-compliance by some reasonable means prior to 60 days after You have
come back into compliance. Moreover, Your grants from a particular
Contributor are reinstated on an ongoing basis if such Contributor
notifies You of the non-compliance by some reasonable means, this is the
first time You have received notice of non-compliance with this License
from such Contributor, and You become compliant prior to 30 days after
Your receipt of the notice.
5.2. If You initiate litigation against any entity by asserting a patent
infringement claim (excluding declaratory judgment actions,
counter-claims, and cross-claims) alleging that a Contributor Version
directly or indirectly infringes any patent, then the rights granted to
You by any and all Contributors for the Covered Software under Section
2.1 of this License shall terminate.
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
end user license agreements (excluding distributors and resellers) which
have been validly granted by You or Your distributors under this License
prior to termination shall survive termination.
************************************************************************
* *
* 6. Disclaimer of Warranty *
* ------------------------- *
* *
* Covered Software is provided under this License on an "as is" *
* basis, without warranty of any kind, either expressed, implied, or *
* statutory, including, without limitation, warranties that the *
* Covered Software is free of defects, merchantable, fit for a *
* particular purpose or non-infringing. The entire risk as to the *
* quality and performance of the Covered Software is with You. *
* Should any Covered Software prove defective in any respect, You *
* (not any Contributor) assume the cost of any necessary servicing, *
* repair, or correction. This disclaimer of warranty constitutes an *
* essential part of this License. No use of any Covered Software is *
* authorized under this License except under this disclaimer. *
* *
************************************************************************
************************************************************************
* *
* 7. Limitation of Liability *
* -------------------------- *
* *
* Under no circumstances and under no legal theory, whether tort *
* (including negligence), contract, or otherwise, shall any *
* Contributor, or anyone who distributes Covered Software as *
* permitted above, be liable to You for any direct, indirect, *
* special, incidental, or consequential damages of any character *
* including, without limitation, damages for lost profits, loss of *
* goodwill, work stoppage, computer failure or malfunction, or any *
* and all other commercial damages or losses, even if such party *
* shall have been informed of the possibility of such damages. This *
* limitation of liability shall not apply to liability for death or *
* personal injury resulting from such party's negligence to the *
* extent applicable law prohibits such limitation. Some *
* jurisdictions do not allow the exclusion or limitation of *
* incidental or consequential damages, so this exclusion and *
* limitation may not apply to You. *
* *
************************************************************************
8. Litigation
-------------
Any litigation relating to this License may be brought only in the
courts of a jurisdiction where the defendant maintains its principal
place of business and such litigation shall be governed by laws of that
jurisdiction, without reference to its conflict-of-law provisions.
Nothing in this Section shall prevent a party's ability to bring
cross-claims or counter-claims.
9. Miscellaneous
----------------
This License represents the complete agreement concerning the subject
matter hereof. If any provision of this License is held to be
unenforceable, such provision shall be reformed only to the extent
necessary to make it enforceable. Any law or regulation which provides
that the language of a contract shall be construed against the drafter
shall not be used to construe this License against a Contributor.
10. Versions of the License
---------------------------
10.1. New Versions
Mozilla Foundation is the license steward. Except as provided in Section
10.3, no one other than the license steward has the right to modify or
publish new versions of this License. Each version will be given a
distinguishing version number.
10.2. Effect of New Versions
You may distribute the Covered Software under the terms of the version
of the License under which You originally received the Covered Software,
or under the terms of any subsequent version published by the license
steward.
10.3. Modified Versions
If you create software not governed by this License, and you want to
create a new license for such software, you may create and use a
modified version of this License if you rename the license and remove
any references to the name of the license steward (except to note that
such modified license differs from this License).
10.4. Distributing Source Code Form that is Incompatible With Secondary
Licenses
If You choose to distribute Source Code Form that is Incompatible With
Secondary Licenses under the terms of this version of the License, the
notice described in Exhibit B of this License must be attached.
Exhibit A - Source Code Form License Notice
-------------------------------------------
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE
file in a relevant directory) where a recipient would be likely to look
for such a notice.
You may add additional accurate notices of copyright ownership.
Exhibit B - "Incompatible With Secondary Licenses" Notice
---------------------------------------------------------
This Source Code Form is "Incompatible With Secondary Licenses", as
defined by the Mozilla Public License, v. 2.0.

View File

@@ -37,14 +37,19 @@ $ tox --devenv env
$ . env/bin/activate
```
It is recommended to use IPython, because it supports using `await` directly
from the REPL.
```
$ 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()
$ 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()
```

View File

@@ -4,21 +4,23 @@
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)
def log_event(event):
async def log_event(event):
print(event)
@hooks.on(events.NewMessage)
def echo(event):
async def echo(event):
snapshot = event.message_snapshot
snapshot.chat.send_text(snapshot.text)
await snapshot.chat.send_text(snapshot.text)
if __name__ == "__main__":
run_bot_cli(hooks)
asyncio.run(run_bot_cli(hooks))

View File

@@ -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)
def log_event(event):
async def log_event(event):
if event.type == EventType.INFO:
logging.info(event.msg)
elif event.type == EventType.WARNING:
@@ -21,54 +21,54 @@ def log_event(event):
@hooks.on(events.RawEvent(EventType.ERROR))
def log_error(event):
async def log_error(event):
logging.error(event.msg)
@hooks.on(events.MemberListChanged)
def on_memberlist_changed(event):
async def on_memberlist_changed(event):
logging.info("member %s was %s", event.member, "added" if event.member_added else "removed")
@hooks.on(events.GroupImageChanged)
def on_group_image_changed(event):
async def on_group_image_changed(event):
logging.info("group image %s", "deleted" if event.image_deleted else "changed")
@hooks.on(events.GroupNameChanged)
def on_group_name_changed(event):
async def on_group_name_changed(event):
logging.info("group name changed, old name: %s", event.old_name)
@hooks.on(events.NewMessage(func=lambda e: not e.command))
def echo(event):
async def echo(event):
snapshot = event.message_snapshot
if snapshot.text or snapshot.file:
snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
await snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
@hooks.on(events.NewMessage(command="/help"))
def help_command(event):
async def help_command(event):
snapshot = event.message_snapshot
snapshot.chat.send_text("Send me any message and I will echo it back")
await snapshot.chat.send_text("Send me any message and I will echo it back")
def main():
with Rpc() as rpc:
async def main():
async with Rpc() as rpc:
deltachat = DeltaChat(rpc)
system_info = deltachat.get_system_info()
system_info = await deltachat.get_system_info()
logging.info("Running deltachat core %s", system_info.deltachat_core_version)
accounts = deltachat.get_all_accounts()
account = accounts[0] if accounts else deltachat.add_account()
accounts = await deltachat.get_all_accounts()
account = accounts[0] if accounts else await deltachat.add_account()
bot = Bot(account, hooks)
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 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 __name__ == "__main__":
logging.basicConfig(level=logging.INFO)
main()
asyncio.run(main())

View File

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

View File

@@ -5,19 +5,9 @@ build-backend = "setuptools.build_meta"
[project]
name = "deltachat-rpc-client"
description = "Python client for Delta Chat core JSON-RPC interface"
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Operating System :: POSIX :: Linux",
"Operating System :: MacOS :: MacOS X",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.8",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Topic :: Communications :: Chat",
"Topic :: Communications :: Email"
dependencies = [
"aiohttp",
"aiodns"
]
dynamic = [
"version"

View File

@@ -1,4 +1,4 @@
"""Delta Chat JSON-RPC high-level API"""
"""Delta Chat asynchronous high-level API"""
from ._utils import AttrDict, run_bot_cli, run_client_cli
from .account import Account
from .chat import Chat

View File

@@ -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)
def run_client_cli(
async def run_client_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
**kwargs,
@@ -54,10 +54,10 @@ def run_client_cli(
"""
from .client import Client
_run_cli(Client, hooks, argv, **kwargs)
await _run_cli(Client, hooks, argv, **kwargs)
def run_bot_cli(
async def run_bot_cli(
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
**kwargs,
@@ -68,10 +68,10 @@ def run_bot_cli(
"""
from .client import Bot
_run_cli(Bot, hooks, argv, **kwargs)
await _run_cli(Bot, hooks, argv, **kwargs)
def _run_cli(
async def _run_cli(
client_type: Type["Client"],
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
argv: Optional[list] = None,
@@ -93,20 +93,20 @@ def _run_cli(
parser.add_argument("--password", action="store", help="password")
args = parser.parse_args(argv[1:])
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
async with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
deltachat = DeltaChat(rpc)
core_version = (deltachat.get_system_info()).deltachat_core_version
accounts = deltachat.get_all_accounts()
account = accounts[0] if accounts else deltachat.add_account()
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()
client = client_type(account, hooks)
client.logger.debug("Running deltachat core %s", core_version)
if not client.is_configured():
if not await client.is_configured():
assert args.email, "Account is not configured and email must be provided"
assert args.password, "Account is not configured and password must be provided"
configure_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password})
configure_thread.start()
client.run_forever()
# 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()
def extract_addr(text: str) -> str:

View File

@@ -24,63 +24,63 @@ class Account:
def _rpc(self) -> "Rpc":
return self.manager.rpc
def wait_for_event(self) -> AttrDict:
async def wait_for_event(self) -> AttrDict:
"""Wait until the next event and return it."""
return AttrDict(self._rpc.wait_for_event(self.id))
return AttrDict(await self._rpc.wait_for_event(self.id))
def remove(self) -> None:
async def remove(self) -> None:
"""Remove the account."""
self._rpc.remove_account(self.id)
await self._rpc.remove_account(self.id)
def start_io(self) -> None:
async def start_io(self) -> None:
"""Start the account I/O."""
self._rpc.start_io(self.id)
await self._rpc.start_io(self.id)
def stop_io(self) -> None:
async def stop_io(self) -> None:
"""Stop the account I/O."""
self._rpc.stop_io(self.id)
await self._rpc.stop_io(self.id)
def get_info(self) -> AttrDict:
async def get_info(self) -> AttrDict:
"""Return dictionary of this account configuration parameters."""
return AttrDict(self._rpc.get_info(self.id))
return AttrDict(await self._rpc.get_info(self.id))
def get_size(self) -> int:
async def get_size(self) -> int:
"""Get the combined filesize of an account in bytes."""
return self._rpc.get_account_file_size(self.id)
return await self._rpc.get_account_file_size(self.id)
def is_configured(self) -> bool:
async def is_configured(self) -> bool:
"""Return True if this account is configured."""
return self._rpc.is_configured(self.id)
return await self._rpc.is_configured(self.id)
def set_config(self, key: str, value: Optional[str] = None) -> None:
async def set_config(self, key: str, value: Optional[str] = None) -> None:
"""Set configuration value."""
self._rpc.set_config(self.id, key, value)
await self._rpc.set_config(self.id, key, value)
def get_config(self, key: str) -> Optional[str]:
async def get_config(self, key: str) -> Optional[str]:
"""Get configuration value."""
return self._rpc.get_config(self.id, key)
return await self._rpc.get_config(self.id, key)
def update_config(self, **kwargs) -> None:
async def update_config(self, **kwargs) -> None:
"""update config values."""
for key, value in kwargs.items():
self.set_config(key, value)
await self.set_config(key, value)
def set_avatar(self, img_path: Optional[str] = None) -> None:
async def set_avatar(self, img_path: Optional[str] = None) -> None:
"""Set self avatar.
Passing None will discard the currently set avatar.
"""
self.set_config("selfavatar", img_path)
await self.set_config("selfavatar", img_path)
def get_avatar(self) -> Optional[str]:
async def get_avatar(self) -> Optional[str]:
"""Get self avatar."""
return self.get_config("selfavatar")
return await self.get_config("selfavatar")
def configure(self) -> None:
async def configure(self) -> None:
"""Configure an account."""
self._rpc.configure(self.id)
await self._rpc.configure(self.id)
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
async 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 = obj.get_snapshot().address
return Contact(self, self._rpc.create_contact(self.id, obj, name))
obj = (await obj.get_snapshot()).address
return Contact(self, await 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)
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
async def get_contact_by_addr(self, address: str) -> Optional[Contact]:
"""Check if an e-mail address belongs to a known and unblocked contact."""
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
contact_id = await self._rpc.lookup_contact_id_by_addr(self.id, address)
return contact_id and Contact(self, contact_id)
def get_blocked_contacts(self) -> List[AttrDict]:
async def get_blocked_contacts(self) -> List[AttrDict]:
"""Return a list with snapshots of all blocked contacts."""
contacts = self._rpc.get_blocked_contacts(self.id)
contacts = await self._rpc.get_blocked_contacts(self.id)
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
def get_contacts(
async 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 = self._rpc.get_contacts(self.id, flags, query)
contacts = await self._rpc.get_contacts(self.id, flags, query)
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
contacts = self._rpc.get_contact_ids(self.id, flags, query)
contacts = await self._rpc.get_contact_ids(self.id, flags, query)
return [Contact(self, contact_id) for contact_id in contacts]
@property
@@ -143,7 +143,7 @@ class Account:
"""This account's identity as a Contact."""
return Contact(self, SpecialContactId.SELF)
def get_chatlist(
async 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 = self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
if not snapshot:
return [Chat(self, entry) for entry in entries]
items = self._rpc.get_chatlist_items_by_entries(self.id, entries)
items = await self._rpc.get_chatlist_items_by_entries(self.id, entries)
chats = []
for item in items.values():
item["chat"] = Chat(self, item["id"])
chats.append(AttrDict(item))
return chats
def create_group(self, name: str, protect: bool = False) -> Chat:
async def create_group(self, name: str, protect: bool = False) -> Chat:
"""Create a new group chat.
After creation, the group has only self-contact as member and is in unpromoted state.
"""
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
return Chat(self, await self._rpc.create_group_chat(self.id, name, protect))
def get_chat_by_id(self, chat_id: int) -> Chat:
"""Return the Chat instance with the given ID."""
return Chat(self, chat_id)
def secure_join(self, qrdata: str) -> Chat:
async def secure_join(self, qrdata: str) -> Chat:
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on
another device.
@@ -208,62 +208,54 @@ class Account:
:param qrdata: The text of the scanned QR code.
"""
return Chat(self, self._rpc.secure_join(self.id, qrdata))
return Chat(self, await self._rpc.secure_join(self.id, qrdata))
def get_qr_code(self) -> Tuple[str, str]:
async def get_qr_code(self) -> Tuple[str, str]:
"""Get Setup-Contact QR Code text and SVG data.
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 self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
return await self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
def get_message_by_id(self, msg_id: int) -> Message:
"""Return the Message instance with the given ID."""
return Message(self, msg_id)
def mark_seen_messages(self, messages: List[Message]) -> None:
async def mark_seen_messages(self, messages: List[Message]) -> None:
"""Mark the given set of messages as seen."""
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
await self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
def delete_messages(self, messages: List[Message]) -> None:
async def delete_messages(self, messages: List[Message]) -> None:
"""Delete messages (local and remote)."""
self._rpc.delete_messages(self.id, [msg.id for msg in messages])
await self._rpc.delete_messages(self.id, [msg.id for msg in messages])
def get_fresh_messages(self) -> List[Message]:
async def get_fresh_messages(self) -> List[Message]:
"""Return the list of fresh messages, newest messages first.
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 = self._rpc.get_fresh_msgs(self.id)
fresh_msg_ids = await self._rpc.get_fresh_msgs(self.id)
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
def get_next_messages(self) -> List[Message]:
async def get_next_messages(self) -> List[Message]:
"""Return a list of next messages."""
next_msg_ids = self._rpc.get_next_msgs(self.id)
next_msg_ids = await self._rpc.get_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
def wait_next_messages(self) -> List[Message]:
async def wait_next_messages(self) -> List[Message]:
"""Wait for new messages and return a list of them."""
next_msg_ids = self._rpc.wait_next_msgs(self.id)
next_msg_ids = await self._rpc.wait_next_msgs(self.id)
return [Message(self, msg_id) for msg_id in next_msg_ids]
def get_fresh_messages_in_arrival_order(self) -> List[Message]:
async 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(self._rpc.get_fresh_msgs(self.id))
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id))
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
def export_backup(self, path, passphrase: str = "") -> None:
"""Export backup."""
self._rpc.export_backup(self.id, str(path), passphrase)
def import_backup(self, path, passphrase: str = "") -> None:
"""Import backup."""
self._rpc.import_backup(self.id, str(path), passphrase)

View File

@@ -25,7 +25,7 @@ class Chat:
def _rpc(self) -> "Rpc":
return self.account._rpc
def delete(self) -> None:
async 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
"""
self._rpc.delete_chat(self.account.id, self.id)
await self._rpc.delete_chat(self.account.id, self.id)
def block(self) -> None:
async def block(self) -> None:
"""Block this chat."""
self._rpc.block_chat(self.account.id, self.id)
await self._rpc.block_chat(self.account.id, self.id)
def accept(self) -> None:
async def accept(self) -> None:
"""Accept this contact request chat."""
self._rpc.accept_chat(self.account.id, self.id)
await self._rpc.accept_chat(self.account.id, self.id)
def leave(self) -> None:
async def leave(self) -> None:
"""Leave this chat."""
self._rpc.leave_group(self.account.id, self.id)
await self._rpc.leave_group(self.account.id, self.id)
def mute(self, duration: Optional[int] = None) -> None:
async def mute(self, duration: Optional[int] = None) -> None:
"""Mute this chat, if a duration is not provided the chat is muted forever.
: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"
self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
def unmute(self) -> None:
async def unmute(self) -> None:
"""Unmute this chat."""
self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
await self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
def pin(self) -> None:
async def pin(self) -> None:
"""Pin this chat."""
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
def unpin(self) -> None:
async def unpin(self) -> None:
"""Unpin this chat."""
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
def archive(self) -> None:
async def archive(self) -> None:
"""Archive this chat."""
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
def unarchive(self) -> None:
async def unarchive(self) -> None:
"""Unarchive this chat."""
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
def set_name(self, name: str) -> None:
async def set_name(self, name: str) -> None:
"""Set name of this chat."""
self._rpc.set_chat_name(self.account.id, self.id, name)
await self._rpc.set_chat_name(self.account.id, self.id, name)
def set_ephemeral_timer(self, timer: int) -> None:
async def set_ephemeral_timer(self, timer: int) -> None:
"""Set ephemeral timer of this chat."""
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
await self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
def get_encryption_info(self) -> str:
async def get_encryption_info(self) -> str:
"""Return encryption info for this chat."""
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
return await self._rpc.get_chat_encryption_info(self.account.id, self.id)
def get_qr_code(self) -> Tuple[str, str]:
async def get_qr_code(self) -> Tuple[str, str]:
"""Get Join-Group QR code text and SVG data."""
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
return await self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
def get_basic_snapshot(self) -> AttrDict:
async def get_basic_snapshot(self) -> AttrDict:
"""Get a chat snapshot with basic info about this chat."""
info = self._rpc.get_basic_chat_info(self.account.id, self.id)
info = await self._rpc.get_basic_chat_info(self.account.id, self.id)
return AttrDict(chat=self, **info)
def get_full_snapshot(self) -> AttrDict:
async def get_full_snapshot(self) -> AttrDict:
"""Get a full snapshot of this chat."""
info = self._rpc.get_full_chat_by_id(self.account.id, self.id)
info = await self._rpc.get_full_chat_by_id(self.account.id, self.id)
return AttrDict(chat=self, **info)
def can_send(self) -> bool:
async def can_send(self) -> bool:
"""Return true if messages can be sent to the chat."""
return self._rpc.can_send(self.account.id, self.id)
return await self._rpc.can_send(self.account.id, self.id)
def send_message(
async 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 = self._rpc.send_msg(self.account.id, self.id, draft)
msg_id = await self._rpc.send_msg(self.account.id, self.id, draft)
return Message(self.account, msg_id)
def send_text(self, text: str) -> Message:
async def send_text(self, text: str) -> Message:
"""Send a text message and return the resulting Message instance."""
msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text)
msg_id = await self._rpc.misc_send_text_message(self.account.id, self.id, text)
return Message(self.account, msg_id)
def send_videochat_invitation(self) -> Message:
async def send_videochat_invitation(self) -> Message:
"""Send a videochat invitation and return the resulting Message instance."""
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
msg_id = await self._rpc.send_videochat_invitation(self.account.id, self.id)
return Message(self.account, msg_id)
def send_sticker(self, path: str) -> Message:
async def send_sticker(self, path: str) -> Message:
"""Send an sticker and return the resulting Message instance."""
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
msg_id = await self._rpc.send_sticker(self.account.id, self.id, path)
return Message(self.account, msg_id)
def forward_messages(self, messages: List[Message]) -> None:
async def forward_messages(self, messages: List[Message]) -> None:
"""Forward a list of messages to this chat."""
msg_ids = [msg.id for msg in messages]
self._rpc.forward_messages(self.account.id, msg_ids, self.id)
await self._rpc.forward_messages(self.account.id, msg_ids, self.id)
def set_draft(
async def set_draft(
self,
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
self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
await self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
def remove_draft(self) -> None:
async def remove_draft(self) -> None:
"""Remove draft message."""
self._rpc.remove_draft(self.account.id, self.id)
await self._rpc.remove_draft(self.account.id, self.id)
def get_draft(self) -> Optional[AttrDict]:
async def get_draft(self) -> Optional[AttrDict]:
"""Get draft message."""
snapshot = self._rpc.get_draft(self.account.id, self.id)
snapshot = await 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
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
async def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
"""get the list of messages in this chat."""
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
msgs = await self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
return [Message(self.account, msg_id) for msg_id in msgs]
def get_fresh_message_count(self) -> int:
async def get_fresh_message_count(self) -> int:
"""Get number of fresh messages in this chat"""
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
return await self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
def mark_noticed(self) -> None:
async def mark_noticed(self) -> None:
"""Mark all messages in this chat as noticed."""
self._rpc.marknoticed_chat(self.account.id, self.id)
await self._rpc.marknoticed_chat(self.account.id, self.id)
def add_contact(self, *contact: Union[int, str, Contact]) -> None:
async def add_contact(self, *contact: Union[int, str, Contact]) -> None:
"""Add contacts to this group."""
for cnt in contact:
if isinstance(cnt, str):
contact_id = self.account.create_contact(cnt).id
contact_id = (await self.account.create_contact(cnt)).id
elif not isinstance(cnt, int):
contact_id = cnt.id
else:
contact_id = cnt
self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
await self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
async def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
"""Remove members from this group."""
for cnt in contact:
if isinstance(cnt, str):
contact_id = self.account.create_contact(cnt).id
contact_id = (await self.account.create_contact(cnt)).id
elif not isinstance(cnt, int):
contact_id = cnt.id
else:
contact_id = cnt
self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
await self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
def get_contacts(self) -> List[Contact]:
async def get_contacts(self) -> List[Contact]:
"""Get the contacts belonging to this chat.
For single/direct chats self-address is not included.
"""
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
contacts = await self._rpc.get_chat_contacts(self.account.id, self.id)
return [Contact(self.account, contact_id) for contact_id in contacts]
def set_image(self, path: str) -> None:
async 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.
"""
self._rpc.set_chat_profile_image(self.account.id, self.id, path)
await self._rpc.set_chat_profile_image(self.account.id, self.id, path)
def remove_image(self) -> None:
async def remove_image(self) -> None:
"""Remove profile image of this chat."""
self._rpc.set_chat_profile_image(self.account.id, self.id, None)
await self._rpc.set_chat_profile_image(self.account.id, self.id, None)
def get_locations(
async def get_locations(
self,
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 = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
result = await self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
locations = []
contacts: Dict[int, Contact] = {}
for loc in result:

View File

@@ -1,4 +1,5 @@
"""Event loop implementations offering high level event handling/hooking."""
import inspect
import logging
from typing import (
TYPE_CHECKING,
@@ -77,22 +78,22 @@ class Client:
)
self._hooks.get(type(event), set()).remove((hook, event))
def is_configured(self) -> bool:
return self.account.is_configured()
async def is_configured(self) -> bool:
return await self.account.is_configured()
def configure(self, email: str, password: str, **kwargs) -> None:
self.account.set_config("addr", email)
self.account.set_config("mail_pw", password)
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)
for key, value in kwargs.items():
self.account.set_config(key, value)
self.account.configure()
await self.account.set_config(key, value)
await self.account.configure()
self.logger.debug("Account configured")
def run_forever(self) -> None:
async def run_forever(self) -> None:
"""Process events forever."""
self.run_until(lambda _: False)
await self.run_until(lambda _: False)
def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
async 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
@@ -100,37 +101,39 @@ class Client:
evaluates to True.
"""
self.logger.debug("Listening to incoming events...")
if self.is_configured():
self.account.start_io()
self._process_messages() # Process old messages.
if await self.is_configured():
await self.account.start_io()
await self._process_messages() # Process old messages.
while True:
event = self.account.wait_for_event()
event = await self.account.wait_for_event()
event["type"] = EventType(event.type)
event["account"] = self.account
self._on_event(event)
await self._on_event(event)
if event.type == EventType.INCOMING_MSG:
self._process_messages()
await self._process_messages()
stop = func(event)
if inspect.isawaitable(stop):
stop = await stop
if stop:
return event
def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
async def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
for hook, evfilter in self._hooks.get(filter_type, []):
if evfilter.filter(event):
if await evfilter.filter(event):
try:
hook(event)
await hook(event)
except Exception as ex:
self.logger.exception(ex)
def _parse_command(self, event: AttrDict) -> None:
async def _parse_command(self, event: AttrDict) -> None:
cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
parts = event.message_snapshot.text.split(maxsplit=1)
payload = parts[1] if len(parts) > 1 else ""
cmd = parts.pop(0)
if "@" in cmd:
suffix = "@" + self.account.self_contact.get_snapshot().address
suffix = "@" + (await self.account.self_contact.get_snapshot()).address
if cmd.endswith(suffix):
cmd = cmd[: -len(suffix)]
else:
@@ -150,32 +153,32 @@ class Client:
event["command"], event["payload"] = cmd, payload
def _on_new_msg(self, snapshot: AttrDict) -> None:
async def _on_new_msg(self, snapshot: AttrDict) -> None:
event = AttrDict(command="", payload="", message_snapshot=snapshot)
if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX):
self._parse_command(event)
self._on_event(event, NewMessage)
await self._parse_command(event)
await self._on_event(event, NewMessage)
def _handle_info_msg(self, snapshot: AttrDict) -> None:
async def _handle_info_msg(self, snapshot: AttrDict) -> None:
event = AttrDict(message_snapshot=snapshot)
img_changed = parse_system_image_changed(snapshot.text)
if img_changed:
_, event["image_deleted"] = img_changed
self._on_event(event, GroupImageChanged)
await self._on_event(event, GroupImageChanged)
return
title_changed = parse_system_title_changed(snapshot.text)
if title_changed:
_, event["old_name"] = title_changed
self._on_event(event, GroupNameChanged)
await 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"
self._on_event(event, MemberListChanged)
await self._on_event(event, MemberListChanged)
return
self.logger.warning(
@@ -184,20 +187,20 @@ class Client:
snapshot.text,
)
def _process_messages(self) -> None:
async def _process_messages(self) -> None:
if self._should_process_messages:
for message in self.account.get_next_messages():
snapshot = message.get_snapshot()
for message in await self.account.get_next_messages():
snapshot = await message.get_snapshot()
if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]:
self._on_new_msg(snapshot)
await self._on_new_msg(snapshot)
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
self._handle_info_msg(snapshot)
snapshot.message.mark_seen()
await self._handle_info_msg(snapshot)
await snapshot.message.mark_seen()
class Bot(Client):
"""Simple bot implementation that listent to events of a single account."""
def configure(self, email: str, password: str, **kwargs) -> None:
async def configure(self, email: str, password: str, **kwargs) -> None:
kwargs.setdefault("bot", "1")
super().configure(email, password, **kwargs)
await super().configure(email, password, **kwargs)

View File

@@ -45,7 +45,6 @@ class EventType(str, Enum):
MSG_DELIVERED = "MsgDelivered"
MSG_FAILED = "MsgFailed"
MSG_READ = "MsgRead"
MSG_DELETED = "MsgDeleted"
CHAT_MODIFIED = "ChatModified"
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
CONTACTS_CHANGED = "ContactsChanged"

View File

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

View File

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

View File

@@ -1,4 +1,5 @@
"""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
@@ -23,7 +24,7 @@ def _tuple_of(obj, type_: type) -> tuple:
class EventFilter(ABC):
"""The base event filter.
:param func: A Callable function that should accept the event as input
:param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -42,13 +43,16 @@ class EventFilter(ABC):
def __ne__(self, other):
return not self == other
def _call_func(self, event) -> bool:
async def _call_func(self, event) -> bool:
if not self.func:
return True
return self.func(event)
res = self.func(event)
if inspect.isawaitable(res):
return await res
return res
@abstractmethod
def filter(self, event):
async def filter(self, event):
"""Return True-like value if the event passed the filter and should be
used, or False-like value otherwise.
"""
@@ -58,7 +62,7 @@ class RawEvent(EventFilter):
"""Matches raw core events.
:param types: The types of event to match.
:param func: A Callable function that should accept the event as input
:param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -78,10 +82,10 @@ class RawEvent(EventFilter):
return (self.types, self.func) == (other.types, other.func)
return False
def filter(self, event: "AttrDict") -> bool:
async def filter(self, event: "AttrDict") -> bool:
if self.types and event.type not in self.types:
return False
return self._call_func(event)
return await self._call_func(event)
class NewMessage(EventFilter):
@@ -100,7 +104,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 function that should accept the event as input
:param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -155,7 +159,7 @@ class NewMessage(EventFilter):
)
return False
def filter(self, event: "AttrDict") -> bool:
async def filter(self, event: "AttrDict") -> bool:
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
return False
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
@@ -164,9 +168,11 @@ 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 super()._call_func(event)
return await super()._call_func(event)
class MemberListChanged(EventFilter):
@@ -178,7 +184,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 function that should accept the event as input
:param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -195,10 +201,10 @@ class MemberListChanged(EventFilter):
return (self.added, self.func) == (other.added, other.func)
return False
def filter(self, event: "AttrDict") -> bool:
async def filter(self, event: "AttrDict") -> bool:
if self.added is not None and self.added != event.member_added:
return False
return self._call_func(event)
return await self._call_func(event)
class GroupImageChanged(EventFilter):
@@ -210,7 +216,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 function that should accept the event as input
:param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -227,10 +233,10 @@ class GroupImageChanged(EventFilter):
return (self.deleted, self.func) == (other.deleted, other.func)
return False
def filter(self, event: "AttrDict") -> bool:
async def filter(self, event: "AttrDict") -> bool:
if self.deleted is not None and self.deleted != event.image_deleted:
return False
return self._call_func(event)
return await self._call_func(event)
class GroupNameChanged(EventFilter):
@@ -239,7 +245,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 function that should accept the event as input
:param func: A Callable (async or not) function that should accept the event as input
parameter, and return a bool value indicating whether the event
should be dispatched or not.
"""
@@ -252,8 +258,8 @@ class GroupNameChanged(EventFilter):
return self.func == other.func
return False
def filter(self, event: "AttrDict") -> bool:
return self._call_func(event)
async def filter(self, event: "AttrDict") -> bool:
return await self._call_func(event)
class HookCollection:

View File

@@ -21,39 +21,39 @@ class Message:
def _rpc(self) -> "Rpc":
return self.account._rpc
def send_reaction(self, *reaction: str):
async def send_reaction(self, *reaction: str):
"""Send a reaction to this message."""
self._rpc.send_reaction(self.account.id, self.id, reaction)
await self._rpc.send_reaction(self.account.id, self.id, reaction)
def get_snapshot(self) -> AttrDict:
async def get_snapshot(self) -> AttrDict:
"""Get a snapshot with the properties of this message."""
from .chat import Chat
snapshot = AttrDict(self._rpc.get_message(self.account.id, self.id))
snapshot = AttrDict(await self._rpc.get_message(self.account.id, self.id))
snapshot["chat"] = Chat(self.account, snapshot.chat_id)
snapshot["sender"] = Contact(self.account, snapshot.from_id)
snapshot["message"] = self
return snapshot
def get_reactions(self) -> Optional[AttrDict]:
async def get_reactions(self) -> Optional[AttrDict]:
"""Get message reactions."""
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
reactions = await self._rpc.get_message_reactions(self.account.id, self.id)
if reactions:
return AttrDict(reactions)
return None
def mark_seen(self) -> None:
async def mark_seen(self) -> None:
"""Mark the message as seen."""
self._rpc.markseen_msgs(self.account.id, [self.id])
await self._rpc.markseen_msgs(self.account.id, [self.id])
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
async def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
"""Send a webxdc status update. This message must be a webxdc."""
if not isinstance(update, str):
update = json.dumps(update)
self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
await self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
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_info(self) -> dict:
return self._rpc.get_webxdc_info(self.account.id, self.id)
async def get_webxdc_info(self) -> dict:
return await self._rpc.get_webxdc_info(self.account.id, self.id)

View File

@@ -1,67 +1,70 @@
import asyncio
import json
import os
import urllib.request
from typing import AsyncGenerator, List, Optional
import pytest
import aiohttp
import pytest_asyncio
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
from .rpc import Rpc
def get_temp_credentials() -> dict:
async 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"
request = urllib.request.Request(url, method="POST")
with urllib.request.urlopen(request, timeout=60) as f:
return json.load(f)
# 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())
class ACFactory:
def __init__(self, deltachat: DeltaChat) -> None:
self.deltachat = deltachat
def get_unconfigured_account(self) -> Account:
return self.deltachat.add_account()
async def get_unconfigured_account(self) -> Account:
return await self.deltachat.add_account()
def get_unconfigured_bot(self) -> Bot:
return Bot(self.get_unconfigured_account())
async def get_unconfigured_bot(self) -> Bot:
return Bot(await self.get_unconfigured_account())
def new_preconfigured_account(self) -> Account:
async def new_preconfigured_account(self) -> Account:
"""Make a new account with configuration options set, but configuration not started."""
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()
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()
return account
def new_configured_account(self) -> Account:
account = self.new_preconfigured_account()
account.configure()
assert account.is_configured()
async def new_configured_account(self) -> Account:
account = await self.new_preconfigured_account()
await account.configure()
assert await account.is_configured()
return account
def new_configured_bot(self) -> Bot:
credentials = get_temp_credentials()
bot = self.get_unconfigured_bot()
bot.configure(credentials["email"], credentials["password"])
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"])
return bot
def get_online_account(self) -> Account:
account = self.new_configured_account()
account.start_io()
async def get_online_account(self) -> Account:
account = await self.new_configured_account()
await account.start_io()
while True:
event = account.wait_for_event()
event = await account.wait_for_event()
print(event)
if event.type == EventType.IMAP_INBOX_IDLE:
break
return account
def get_online_accounts(self, num: int) -> List[Account]:
return [self.get_online_account() for _ in range(num)]
async def get_online_accounts(self, num: int) -> List[Account]:
return await asyncio.gather(*[self.get_online_account() for _ in range(num)])
def send_message(
async def send_message(
self,
to_account: Account,
from_account: Optional[Account] = None,
@@ -70,16 +73,16 @@ class ACFactory:
group: Optional[str] = None,
) -> Message:
if not from_account:
from_account = (self.get_online_accounts(1))[0]
to_contact = from_account.create_contact(to_account.get_config("addr"))
from_account = (await self.get_online_accounts(1))[0]
to_contact = await from_account.create_contact(await to_account.get_config("addr"))
if group:
to_chat = from_account.create_group(group)
to_chat.add_contact(to_contact)
to_chat = await from_account.create_group(group)
await to_chat.add_contact(to_contact)
else:
to_chat = to_contact.create_chat()
return to_chat.send_message(text=text, file=file)
to_chat = await to_contact.create_chat()
return await to_chat.send_message(text=text, file=file)
def process_message(
async def process_message(
self,
to_client: Client,
from_account: Optional[Account] = None,
@@ -87,7 +90,7 @@ class ACFactory:
file: Optional[str] = None,
group: Optional[str] = None,
) -> AttrDict:
self.send_message(
await self.send_message(
to_account=to_client.account,
from_account=from_account,
text=text,
@@ -95,16 +98,16 @@ class ACFactory:
group=group,
)
return to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
return await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
@pytest.fixture()
def rpc(tmp_path) -> AsyncGenerator:
@pytest_asyncio.fixture
async def rpc(tmp_path) -> AsyncGenerator:
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
with rpc_server:
async with rpc_server:
yield rpc_server
@pytest.fixture()
def acfactory(rpc) -> AsyncGenerator:
return ACFactory(DeltaChat(rpc))
@pytest_asyncio.fixture
async def acfactory(rpc) -> AsyncGenerator:
yield ACFactory(DeltaChat(rpc))

View File

@@ -1,10 +1,6 @@
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
@@ -14,7 +10,7 @@ class JsonRpcError(Exception):
class Rpc:
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
"""The given arguments will be passed to subprocess.Popen()"""
"""The given arguments will be passed to asyncio.create_subprocess_exec()"""
if accounts_dir:
kwargs["env"] = {
**kwargs.get("env", os.environ),
@@ -22,142 +18,95 @@ class Rpc:
}
self._kwargs = kwargs
self.process: subprocess.Popen
self.process: asyncio.subprocess.Process
self.id: int
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.event_queues: Dict[int, asyncio.Queue]
# Map from request ID to `asyncio.Future` returning the response.
self.request_events: Dict[int, asyncio.Future]
self.closing: bool
self.reader_thread: Thread
self.writer_thread: Thread
self.events_thread: Thread
self.reader_task: asyncio.Task
self.events_task: asyncio.Task
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,
)
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,
)
self.id = 0
self.event_queues = {}
self.request_events = {}
self.request_results = {}
self.request_queue = Queue()
self.closing = False
self.reader_thread = Thread(target=self.reader_loop)
self.reader_thread.start()
self.writer_thread = Thread(target=self.writer_loop)
self.writer_thread.start()
self.events_thread = Thread(target=self.events_loop)
self.events_thread.start()
self.reader_task = asyncio.create_task(self.reader_loop())
self.events_task = asyncio.create_task(self.events_loop())
def close(self) -> None:
async def close(self) -> None:
"""Terminate RPC server process and wait until the reader loop finishes."""
self.closing = True
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()
await self.stop_io_for_all_accounts()
await self.events_task
self.process.terminate()
await self.reader_task
def __enter__(self):
self.start()
async def __aenter__(self):
await self.start()
return self
def __exit__(self, _exc_type, _exc, _tb):
self.close()
async def __aexit__(self, _exc_type, _exc, _tb):
await self.close()
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 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 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:
async def get_queue(self, account_id: int) -> asyncio.Queue:
if account_id not in self.event_queues:
self.event_queues[account_id] = Queue()
self.event_queues[account_id] = asyncio.Queue()
return self.event_queues[account_id]
def events_loop(self) -> None:
async def events_loop(self) -> None:
"""Requests new events and distributes them between queues."""
try:
while True:
if self.closing:
return
event = self.get_next_event()
account_id = event["contextId"]
queue = self.get_queue(account_id)
queue.put(event["event"])
except Exception:
# Log an exception if the event loop dies.
logging.exception("Exception in the event loop")
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"])
def wait_for_event(self, account_id: int) -> Optional[dict]:
async def wait_for_event(self, account_id: int) -> Optional[dict]:
"""Waits for the next event from the given account and returns it."""
queue = self.get_queue(account_id)
return queue.get()
queue = await self.get_queue(account_id)
return await queue.get()
def __getattr__(self, attr: str):
def method(*args) -> Any:
async def method(*args, **kwargs) -> Any:
self.id += 1
request_id = self.id
assert not (args and kwargs), "Mixing positional and keyword arguments"
request = {
"jsonrpc": "2.0",
"method": attr,
"params": args,
"params": kwargs or args,
"id": self.id,
}
event = Event()
self.request_events[request_id] = event
self.request_queue.put(request)
event.wait()
response = self.request_results.pop(request_id)
data = (json.dumps(request) + "\n").encode()
self.process.stdin.write(data) # noqa
loop = asyncio.get_running_loop()
fut = loop.create_future()
self.request_events[request_id] = fut
response = await fut
if "error" in response:
raise JsonRpcError(response["error"])
if "result" in response:

View File

@@ -1,4 +1,4 @@
import concurrent.futures
import asyncio
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
def test_system_info(rpc) -> None:
system_info = rpc.get_system_info()
@pytest.mark.asyncio()
async def test_system_info(rpc) -> None:
system_info = await rpc.get_system_info()
assert "arch" in system_info
assert "deltachat_core_version" in system_info
def test_sleep(rpc) -> None:
@pytest.mark.asyncio()
async def test_sleep(rpc) -> None:
"""Test that long-running task does not block short-running task from completion."""
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
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()
def test_email_address_validity(rpc) -> None:
@pytest.mark.asyncio()
async def test_email_address_validity(rpc) -> None:
valid_addresses = [
"email@example.com",
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
@@ -33,15 +33,16 @@ def test_email_address_validity(rpc) -> None:
invalid_addresses = ["email@", "example.com", "emai221"]
for addr in valid_addresses:
assert rpc.check_email_validity(addr)
assert await rpc.check_email_validity(addr)
for addr in invalid_addresses:
assert not rpc.check_email_validity(addr)
assert not await rpc.check_email_validity(addr)
def test_acfactory(acfactory) -> None:
account = acfactory.new_configured_account()
@pytest.mark.asyncio()
async def test_acfactory(acfactory) -> None:
account = await acfactory.new_configured_account()
while True:
event = account.wait_for_event()
event = await account.wait_for_event()
if event.type == EventType.CONFIGURE_PROGRESS:
assert event.progress != 0 # Progress 0 indicates error.
if event.progress == 1000: # Success
@@ -51,241 +52,238 @@ def test_acfactory(acfactory) -> None:
print("Successful configuration")
def test_configure_starttls(acfactory) -> None:
account = acfactory.new_preconfigured_account()
@pytest.mark.asyncio()
async def test_configure_starttls(acfactory) -> None:
account = await acfactory.new_preconfigured_account()
# Use STARTTLS
account.set_config("mail_security", "2")
account.set_config("send_security", "2")
account.configure()
assert account.is_configured()
await account.set_config("mail_security", "2")
await account.set_config("send_security", "2")
await account.configure()
assert await account.is_configured()
def test_account(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
@pytest.mark.asyncio()
async def test_account(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
bob_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!")
while True:
event = bob.wait_for_event()
event = await 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 = message.get_snapshot()
snapshot = await message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
bob.mark_seen_messages([message])
await bob.mark_seen_messages([message])
assert alice != bob
assert repr(alice)
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 (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.self_contact
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()
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()
# Test sending empty message.
assert len(bob.wait_next_messages()) == 0
alice_chat_bob.send_text("")
messages = bob.wait_next_messages()
assert len(messages) == 1
message = messages[0]
snapshot = message.get_snapshot()
assert snapshot.text == ""
bob.mark_seen_messages([message])
group = alice.create_group("test group")
group.add_contact(alice_contact_bob)
group_msg = group.send_message(text="hello")
group = await alice.create_group("test group")
await group.add_contact(alice_contact_bob)
group_msg = await 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)
alice.delete_messages([group_msg])
await alice.delete_messages([group_msg])
alice.set_config("selfstatus", "test")
assert alice.get_config("selfstatus") == "test"
alice.update_config(selfstatus="test2")
assert alice.get_config("selfstatus") == "test2"
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"
assert not alice.get_blocked_contacts()
alice_contact_bob.block()
blocked_contacts = alice.get_blocked_contacts()
assert not await alice.get_blocked_contacts()
await alice_contact_bob.block()
blocked_contacts = await alice.get_blocked_contacts()
assert blocked_contacts
assert blocked_contacts[0].contact == alice_contact_bob
bob.remove()
alice.stop_io()
await bob.remove()
await alice.stop_io()
def test_chat(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
@pytest.mark.asyncio()
async def test_chat(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
bob_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!")
while True:
event = bob.wait_for_event()
event = await 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 = message.get_snapshot()
snapshot = await 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)
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()
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()
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
bob_chat_alice.set_name("test")
bob_chat_alice.set_ephemeral_timer(300)
bob_chat_alice.get_encryption_info()
await bob_chat_alice.set_name("test")
await bob_chat_alice.set_ephemeral_timer(300)
await bob_chat_alice.get_encryption_info()
group = alice.create_group("test group")
group.add_contact(alice_contact_bob)
group.get_qr_code()
group = await alice.create_group("test group")
await group.add_contact(alice_contact_bob)
await group.get_qr_code()
snapshot = group.get_basic_snapshot()
snapshot = await group.get_basic_snapshot()
assert snapshot.name == "test group"
group.set_name("new name")
snapshot = group.get_full_snapshot()
await group.set_name("new name")
snapshot = await group.get_full_snapshot()
assert snapshot.name == "new name"
msg = group.send_message(text="hi")
assert (msg.get_snapshot()).text == "hi"
group.forward_messages([msg])
msg = await group.send_message(text="hi")
assert (await msg.get_snapshot()).text == "hi"
await group.forward_messages([msg])
group.set_draft(text="test draft")
draft = group.get_draft()
await group.set_draft(text="test draft")
draft = await group.get_draft()
assert draft.text == "test draft"
group.remove_draft()
assert not group.get_draft()
await group.remove_draft()
assert not await group.get_draft()
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()
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()
def test_contact(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
@pytest.mark.asyncio()
async def test_contact(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
assert repr(alice_contact_bob)
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()
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()
assert snapshot.address == bob_addr
alice_contact_bob.create_chat()
await alice_contact_bob.create_chat()
def test_message(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
@pytest.mark.asyncio()
async def test_message(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_text("Hello!")
bob_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!")
while True:
event = bob.wait_for_event()
event = await 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 = message.get_snapshot()
snapshot = await message.get_snapshot()
assert snapshot.chat_id == chat_id
assert snapshot.text == "Hello!"
assert not snapshot.is_bot
assert repr(message)
with pytest.raises(JsonRpcError): # chat is not accepted
snapshot.chat.send_text("hi")
snapshot.chat.accept()
snapshot.chat.send_text("hi")
await snapshot.chat.send_text("hi")
await snapshot.chat.accept()
await snapshot.chat.send_text("hi")
message.mark_seen()
message.send_reaction("😎")
reactions = message.get_reactions()
await message.mark_seen()
await message.send_reaction("😎")
reactions = await message.get_reactions()
assert reactions
snapshot = message.get_snapshot()
snapshot = await message.get_snapshot()
assert reactions == snapshot.reactions
def test_is_bot(acfactory) -> None:
@pytest.mark.asyncio()
async def test_is_bot(acfactory) -> None:
"""Test that we can recognize messages submitted by bots."""
alice, bob = acfactory.get_online_accounts(2)
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
bob_addr = await bob.get_config("addr")
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
alice_chat_bob = await alice_contact_bob.create_chat()
# Alice becomes a bot.
alice.set_config("bot", "1")
alice_chat_bob.send_text("Hello!")
await alice.set_config("bot", "1")
await alice_chat_bob.send_text("Hello!")
while True:
event = bob.wait_for_event()
event = await bob.wait_for_event()
if event.type == EventType.INCOMING_MSG:
msg_id = event.msg_id
message = bob.get_message_by_id(msg_id)
snapshot = message.get_snapshot()
snapshot = await message.get_snapshot()
assert snapshot.chat_id == event.chat_id
assert snapshot.text == "Hello!"
assert snapshot.is_bot
break
def test_bot(acfactory) -> None:
@pytest.mark.asyncio()
async def test_bot(acfactory) -> None:
mock = MagicMock()
user = (acfactory.get_online_accounts(1))[0]
bot = acfactory.new_configured_bot()
bot2 = acfactory.new_configured_bot()
user = (await acfactory.get_online_accounts(1))[0]
bot = await acfactory.new_configured_bot()
bot2 = await acfactory.new_configured_bot()
assert bot.is_configured()
assert bot.account.get_config("bot") == "1"
assert await bot.is_configured()
assert await bot.account.get_config("bot") == "1"
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
bot.add_hook(*hook)
event = acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
snapshot = bot.account.get_message_by_id(event.msg_id).get_snapshot()
event = await acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
snapshot = await bot.account.get_message_by_id(event.msg_id).get_snapshot()
assert not snapshot.is_bot
mock.hook.assert_called_once_with(event.msg_id)
bot.remove_hook(*hook)
@@ -297,52 +295,43 @@ def test_bot(acfactory) -> None:
hook = track, events.NewMessage(r"hello")
bot.add_hook(*hook)
bot.add_hook(track, events.NewMessage(command="/help"))
event = acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello")
mock.hook.assert_called_with(event.msg_id)
event = acfactory.process_message(from_account=user, to_client=bot, text="hello!")
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!")
mock.hook.assert_called_with(event.msg_id)
acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
await acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots
acfactory.process_message(from_account=user, to_client=bot, text="hey!")
await acfactory.process_message(from_account=user, to_client=bot, text="hey!")
assert len(mock.hook.mock_calls) == 2
bot.remove_hook(*hook)
mock.hook.reset_mock()
acfactory.process_message(from_account=user, to_client=bot, text="hello")
event = acfactory.process_message(from_account=user, to_client=bot, text="/help")
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")
mock.hook.assert_called_once_with(event.msg_id)
def test_wait_next_messages(acfactory) -> None:
alice = acfactory.new_configured_account()
@pytest.mark.asyncio()
async def test_wait_next_messages(acfactory) -> None:
alice = await acfactory.new_configured_account()
# Create a bot account so it does not receive device messages in the beginning.
bot = acfactory.new_preconfigured_account()
bot.set_config("bot", "1")
bot.configure()
bot = await acfactory.new_preconfigured_account()
await bot.set_config("bot", "1")
await bot.configure()
# There are no old messages and the call returns immediately.
assert not bot.wait_next_messages()
assert not await 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 starts waiting for messages.
next_messages_task = asyncio.create_task(bot.wait_next_messages())
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!")
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!")
next_messages = next_messages_task.result()
assert len(next_messages) == 1
snapshot = next_messages[0].get_snapshot()
assert snapshot.text == "Hello!"
def test_import_export(acfactory, tmp_path) -> None:
alice = acfactory.new_configured_account()
alice.export_backup(tmp_path)
files = list(tmp_path.glob("*.tar"))
alice2 = acfactory.get_unconfigured_account()
alice2.import_backup(files[0])
next_messages = await next_messages_task
assert len(next_messages) == 1
snapshot = await next_messages[0].get_snapshot()
assert snapshot.text == "Hello!"

View File

@@ -1,22 +1,24 @@
import pytest
from deltachat_rpc_client import EventType
def test_webxdc(acfactory) -> None:
alice, bob = acfactory.get_online_accounts(2)
@pytest.mark.asyncio()
async def test_webxdc(acfactory) -> None:
alice, bob = await acfactory.get_online_accounts(2)
bob_addr = bob.get_config("addr")
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
alice_chat_bob = alice_contact_bob.create_chat()
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
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")
while True:
event = bob.wait_for_event()
event = await 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 = message.get_webxdc_info()
webxdc_info = await message.get_webxdc_info()
assert webxdc_info == {
"document": None,
"icon": "icon.png",
@@ -26,20 +28,20 @@ def test_webxdc(acfactory) -> None:
"summary": None,
}
status_updates = message.get_webxdc_status_updates()
status_updates = await message.get_webxdc_status_updates()
assert status_updates == []
bob_chat_alice.accept()
message.send_webxdc_status_update({"payload": 42}, "")
message.send_webxdc_status_update({"payload": "Second update"}, "description")
await bob_chat_alice.accept()
await message.send_webxdc_status_update({"payload": 42}, "")
await message.send_webxdc_status_update({"payload": "Second update"}, "description")
status_updates = message.get_webxdc_status_updates()
status_updates = await message.get_webxdc_status_updates()
assert status_updates == [
{"payload": 42, "serial": 1, "max_serial": 2},
{"payload": "Second update", "serial": 2, "max_serial": 2},
]
status_updates = message.get_webxdc_status_updates(1)
status_updates = await message.get_webxdc_status_updates(1)
assert status_updates == [
{"payload": "Second update", "serial": 2, "max_serial": 2},
]

View File

@@ -6,7 +6,7 @@ envlist =
[testenv]
commands =
pytest -n6 {posargs}
pytest {posargs}
setenv =
# Avoid stack overflow when Rust core is built without optimizations.
RUST_MIN_STACK=8388608
@@ -14,8 +14,10 @@ passenv =
DCC_NEW_TMP_EMAIL
deps =
pytest
pytest-asyncio
pytest-timeout
pytest-xdist
aiohttp
aiodns
[testenv:lint]
skipsdist = True

View File

@@ -1,6 +1,6 @@
[package]
name = "deltachat-rpc-server"
version = "1.124.1"
version = "1.117.0"
description = "DeltaChat JSON-RPC server"
edition = "2021"
readme = "README.md"
@@ -17,9 +17,9 @@ anyhow = "1"
env_logger = { version = "0.10.0" }
futures-lite = "1.13.0"
log = "0.4"
serde_json = "1.0.99"
serde_json = "1.0.96"
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1.29.1", features = ["io-std"] }
tokio = { version = "1.28.2", features = ["io-std"] }
tokio-util = "0.7.8"
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }

View File

@@ -2,7 +2,6 @@
unmaintained = "allow"
ignore = [
"RUSTSEC-2020-0071",
"RUSTSEC-2022-0093",
]
[bans]
@@ -11,7 +10,6 @@ ignore = [
# when upgrading.
# Please keep this list alphabetically sorted.
skip = [
{ name = "ahash", version = "0.7.6" },
{ name = "base16ct", version = "0.1.1" },
{ name = "base64", version = "<0.21" },
{ name = "bitflags", version = "1.3.2" },
@@ -26,11 +24,9 @@ skip = [
{ name = "ed25519-dalek", version = "1.0.1" },
{ name = "ed25519", version = "1.5.3" },
{ name = "getrandom", version = "<0.2" },
{ name = "hashbrown", version = "<0.14.0" },
{ name = "hermit-abi", version = "<0.3" },
{ name = "idna", version = "<0.3" },
{ name = "indexmap", version = "<2.0.0" },
{ name = "linux-raw-sys", version = "0.3.8" },
{ name = "num-derive", version = "0.3.3" },
{ name = "libm", version = "0.1.4" },
{ name = "pem-rfc7468", version = "0.6.0" },
{ name = "pkcs8", version = "0.9.0" },
{ name = "quick-error", version = "<2.0" },
@@ -39,11 +35,9 @@ skip = [
{ name = "rand", version = "<0.8" },
{ name = "redox_syscall", version = "0.2.16" },
{ name = "regex-syntax", version = "0.6.29" },
{ name = "rustix", version = "0.37.21" },
{ name = "sec1", version = "0.3.0" },
{ name = "sha2", version = "<0.10" },
{ name = "signature", version = "1.6.4" },
{ name = "socket2", version = "0.4.9" },
{ name = "spin", version = "<0.9.6" },
{ name = "spki", version = "0.6.0" },
{ name = "syn", version = "1.0.109" },
@@ -56,10 +50,8 @@ skip = [
{ name = "windows-sys", version = "<0.48" },
{ name = "windows-targets", version = "<0.48" },
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
{ name = "windows", version = "0.32.0" },
{ name = "windows_x86_64_gnu", version = "<0.48" },
{ name = "windows_x86_64_msvc", version = "<0.48" },
{ name = "winreg", version = "0.10.1" },
]

View File

@@ -90,7 +90,6 @@ module.exports = {
DC_KEY_GEN_DEFAULT: 0,
DC_KEY_GEN_ED25519: 2,
DC_KEY_GEN_RSA2048: 1,
DC_KEY_GEN_RSA4096: 3,
DC_LP_AUTH_NORMAL: 4,
DC_LP_AUTH_OAUTH2: 2,
DC_MEDIA_QUALITY_BALANCED: 0,

View File

@@ -90,7 +90,6 @@ export enum C {
DC_KEY_GEN_DEFAULT = 0,
DC_KEY_GEN_ED25519 = 2,
DC_KEY_GEN_RSA2048 = 1,
DC_KEY_GEN_RSA4096 = 3,
DC_LP_AUTH_NORMAL = 4,
DC_LP_AUTH_OAUTH2 = 2,
DC_MEDIA_QUALITY_BALANCED = 0,

View File

@@ -446,8 +446,7 @@ describe('Offline Tests with unconfigured account', function () {
context.setChatProfileImage(chatId, imagePath)
const blobPath = context.getChat(chatId).getProfileImage()
expect(blobPath.startsWith(blobs)).to.be.true
expect(blobPath.includes('image')).to.be.true
expect(blobPath.endsWith('.jpeg')).to.be.true
expect(blobPath.endsWith(image)).to.be.true
context.setChatProfileImage(chatId, null)
expect(context.getChat(chatId).getProfileImage()).to.be.equal(

View File

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

View File

@@ -357,7 +357,7 @@ Exhibit A - Source Code Form License Notice
This Source Code Form is subject to the terms of the Mozilla Public
License, v. 2.0. If a copy of the MPL was not distributed with this
file, You can obtain one at https://mozilla.org/MPL/2.0/.
file, You can obtain one at http://mozilla.org/MPL/2.0/.
If it is not possible or desirable to put the notice in a particular
file, then You may include the notice in a location (such as a LICENSE

View File

@@ -2,34 +2,34 @@
high level API reference
========================
- :class:`deltachat.Account` (your main entry point, creates the
- :class:`deltachat.account.Account` (your main entry point, creates the
other classes)
- :class:`deltachat.Contact`
- :class:`deltachat.Chat`
- :class:`deltachat.Message`
- :class:`deltachat.contact.Contact`
- :class:`deltachat.chat.Chat`
- :class:`deltachat.message.Message`
Account
-------
.. autoclass:: deltachat.Account
.. autoclass:: deltachat.account.Account
:members:
Contact
-------
.. autoclass:: deltachat.Contact
.. autoclass:: deltachat.contact.Contact
:members:
Chat
----
.. autoclass:: deltachat.Chat
.. autoclass:: deltachat.chat.Chat
:members:
Message
-------
.. autoclass:: deltachat.Message
.. autoclass:: deltachat.message.Message
:members:

View File

@@ -32,13 +32,25 @@ class GroupTrackingPlugin:
@account_hookimpl
def ac_member_added(self, chat, contact, actor, message):
print(f"ac_member_added {contact.addr} to chat {chat.id} from {actor or message.get_sender_contact().addr}")
print(
"ac_member_added {} to chat {} from {}".format(
contact.addr,
chat.id,
actor or message.get_sender_contact().addr,
),
)
for member in chat.get_contacts():
print(f"chat member: {member.addr}")
@account_hookimpl
def ac_member_removed(self, chat, contact, actor, message):
print(f"ac_member_removed {contact.addr} from chat {chat.id} by {actor or message.get_sender_contact().addr}")
print(
"ac_member_removed {} from chat {} by {}".format(
contact.addr,
chat.id,
actor or message.get_sender_contact().addr,
),
)
def main(argv=None):

View File

@@ -24,6 +24,8 @@ def test_echo_quit_plugin(acfactory, lp):
lp.sec("creating a temp account to contact the bot")
(ac1,) = acfactory.get_online_accounts(1)
botproc.await_resync()
lp.sec("sending a message to the bot")
bot_contact = ac1.create_contact(botproc.addr)
bot_chat = bot_contact.create_chat()
@@ -52,6 +54,8 @@ def test_group_tracking_plugin(acfactory, lp):
ac1.add_account_plugin(FFIEventLogger(ac1))
ac2.add_account_plugin(FFIEventLogger(ac2))
botproc.await_resync()
lp.sec("creating bot test group with bot")
bot_contact = ac1.create_contact(botproc.addr)
ch = ac1.create_group_chat("bot test group")

View File

@@ -24,5 +24,3 @@ ignore_missing_imports = True
[mypy-imap_tools.*]
ignore_missing_imports = True
[mypy-distutils.*]
ignore_missing_imports = True

View File

@@ -11,11 +11,10 @@ authors = [
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
"Programming Language :: Python :: 3",
"Topic :: Communications :: Chat",
"Topic :: Communications :: Email",
"Topic :: Software Development :: Libraries",
]
@@ -34,7 +33,6 @@ dynamic = [
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
"Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues"
"Documentation" = "https://py.delta.chat/"
"Mastodon" = "https://chaos.social/@delta"
[project.entry-points.pytest11]
"deltachat.testplugin" = "deltachat.testplugin"

View File

@@ -6,7 +6,7 @@ from array import array
from contextlib import contextmanager
from email.utils import parseaddr
from threading import Event
from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Union
from typing import Any, Dict, Generator, List, Optional, Union, TYPE_CHECKING
from . import const, hookspec
from .capi import ffi, lib
@@ -195,7 +195,7 @@ class Account:
assert res != ffi.NULL, f"config value not found for: {name!r}"
return from_dc_charpointer(res)
def _preconfigure_keypair(self, addr: str, secret: str) -> None:
def _preconfigure_keypair(self, addr: str, public: 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),
ffi.NULL,
as_dc_charpointer(public),
as_dc_charpointer(secret),
)
if res == 0:
@@ -427,7 +427,7 @@ class Account:
assert dc_chatlist != ffi.NULL
chatlist = []
for i in range(lib.dc_chatlist_get_cnt(dc_chatlist)):
for i in range(0, lib.dc_chatlist_get_cnt(dc_chatlist)):
chat_id = lib.dc_chatlist_get_chat_id(dc_chatlist, i)
chatlist.append(Chat(self, chat_id))
return chatlist
@@ -617,18 +617,18 @@ class Account:
# meta API for start/stop and event based processing
#
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False, displayname=None):
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False):
from .events import FFIEventLogger
"""get the account running, configure it if necessary. add plugins if provided.
:param addr: the email address of the account
: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,8 +644,6 @@ 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()

View File

@@ -75,12 +75,9 @@ class Contact:
"""Return True if the contact is verified."""
return lib.dc_contact_is_verified(self._dc_contact) == 2
def get_verifier(self, contact) -> Optional["Contact"]:
def get_verifier(self, contact):
"""Return the address of the contact that verified the contact."""
verifier_id = lib.dc_contact_get_verifier_id(contact._dc_contact)
if verifier_id == 0:
return None
return Contact(self.account, verifier_id)
return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact))
def get_profile_image(self) -> Optional[str]:
"""Get contact profile image.

View File

@@ -15,7 +15,7 @@ def as_dc_charpointer(obj):
def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]:
for i in range(lib.dc_array_get_cnt(dc_array_t)):
for i in range(0, lib.dc_array_get_cnt(dc_array_t)):
yield constructor(lib.dc_array_get_id(dc_array_t, i))

View File

@@ -9,11 +9,11 @@ from contextlib import contextmanager
from queue import Empty, Queue
from . import const
from .account import Account
from .capi import ffi, lib
from .cutil import from_optional_dc_charpointer
from .hookspec import account_hookimpl
from .message import map_system_message
from .account import Account
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):

View File

@@ -486,9 +486,6 @@ class Message:
dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
return lib.dc_msg_get_download_state(dc_msg)
def download_full(self) -> None:
lib.dc_download_full_msg(self.account._dc_context, self.id)
# some code for handling DC_MSG_* view types
@@ -510,7 +507,8 @@ def get_viewtype_code_from_name(view_type_name):
if code is not None:
return code
raise ValueError(
f"message typecode not found for {view_type_name!r}, available {list(_view_type_mapping.keys())!r}",
"message typecode not found for {!r}, "
"available {!r}".format(view_type_name, list(_view_type_mapping.keys())),
)

View File

@@ -9,7 +9,7 @@ import threading
import time
import weakref
from queue import Queue
from typing import Callable, Dict, List, Optional, Set
from typing import Callable, List, Optional, Dict, Set
import pytest
import requests
@@ -478,9 +478,10 @@ 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_sec:
account._preconfigure_keypair(addr, fname_sec)
if fname_pub and fname_sec:
account._preconfigure_keypair(addr, fname_pub, fname_sec)
return True
print(f"WARN: could not use preconfigured keys for {addr!r}")
@@ -681,6 +682,13 @@ class BotProcess:
print("+++IGN:", line)
ignored.append(line)
def await_resync(self):
self.fnmatch_lines(
"""
*Resync: collected * message IDs in folder INBOX*
""",
)
@pytest.fixture()
def tmp_db_path(tmpdir):

View File

@@ -1,6 +1,6 @@
from queue import Queue
from threading import Event
from typing import TYPE_CHECKING, List
from typing import List, TYPE_CHECKING
from .hookspec import Global, account_hookimpl

View File

@@ -15,6 +15,6 @@ class TestEmpty:
def test_prepare_setup_measurings(self, acfactory):
acfactory.get_online_accounts(BENCH_NUM)
@pytest.mark.parametrize("num", range(BENCH_NUM + 1))
@pytest.mark.parametrize("num", range(0, BENCH_NUM + 1))
def test_setup_online_accounts(self, acfactory, num):
acfactory.get_online_accounts(num)

View File

@@ -1,7 +1,6 @@
import sys
import pytest
import deltachat as dc
class TestGroupStressTests:
@@ -150,8 +149,9 @@ 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).id == dc.const.DC_CONTACT_ID_SELF
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
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).id == dc.const.DC_CONTACT_ID_SELF
assert ac2.get_self_contact().get_verifier(ac2_ac1_contact) == ac1_addr
ac2_ac3_contact = ac2.get_contacts()[1]
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact).addr == ac1_addr
assert ac2.get_self_contact().get_verifier(ac2_ac3_contact) == ac1_addr
lp.sec("ac2: send message and let ac3 read it")
chat2.send_text("hi")

View File

@@ -2,14 +2,14 @@ import os
import queue
import sys
import time
import base64
from datetime import datetime, timezone
import pytest
from imap_tools import AND, U
import deltachat as dc
from deltachat import account_hookimpl, Message
from deltachat import const
from deltachat.hookspec import account_hookimpl
from deltachat.message import Message
from deltachat.tracker import ImexTracker
@@ -36,8 +36,8 @@ def test_basic_imap_api(acfactory, tmp_path):
def test_configure_generate_key(acfactory, lp):
# A slow test which will generate new keys.
acfactory.remove_preconfigured_keys()
ac1 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_RSA2048))
ac2 = acfactory.new_online_configuring_account(key_gen_type=str(dc.const.DC_KEY_GEN_ED25519))
ac1 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_RSA2048))
ac2 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_ED25519))
acfactory.bring_accounts_online()
chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -162,9 +162,8 @@ def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
basename = "somedäüta"
ext = ".html.zip"
p = tmp_path / (basename + ext)
basename = "somedäüta.html.zip"
p = tmp_path / basename
p.write_text("some data")
def send_and_receive_message():
@@ -176,20 +175,18 @@ def test_send_file_twice_unicode_filename_mangling(tmp_path, acfactory, lp):
lp.sec("ac2: receive message")
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
return ac2.get_message_by_id(ev.data2)
msg = send_and_receive_message()
assert msg.text == "withfile"
assert open(msg.filename).read() == "some data"
msg.filename.index(basename)
assert msg.filename.endswith(ext)
assert msg.filename.endswith(basename)
msg2 = send_and_receive_message()
assert msg2.text == "withfile"
assert open(msg2.filename).read() == "some data"
msg2.filename.index(basename)
assert msg2.filename.endswith(ext)
assert msg2.filename.endswith("html.zip")
assert msg.filename != msg2.filename
@@ -197,11 +194,10 @@ def test_send_file_html_attachment(tmp_path, acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = acfactory.get_accepted_chat(ac1, ac2)
basename = "test"
ext = ".html"
basename = "test.html"
content = "<html><body>text</body>data"
p = tmp_path / (basename + ext)
p = tmp_path / basename
# write wrong html to see if core tries to parse it
# (it shouldn't as it's a file attachment)
p.write_text(content)
@@ -211,12 +207,11 @@ def test_send_file_html_attachment(tmp_path, acfactory, lp):
lp.sec("ac2: receive message")
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
msg = ac2.get_message_by_id(ev.data2)
assert open(msg.filename).read() == content
msg.filename.index(basename)
assert msg.filename.endswith(ext)
assert msg.filename.endswith(basename)
def test_html_message(acfactory, lp):
@@ -329,59 +324,6 @@ def test_webxdc_message(acfactory, data, lp):
assert len(list(ac2.direct_imap.conn.fetch(AND(seen=True)))) == 1
def test_webxdc_huge_update(acfactory, data, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
chat = ac1.create_chat(ac2)
msg1 = Message.new_empty(ac1, "webxdc")
msg1.set_text("message1")
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
msg1 = chat.send_msg(msg1)
assert msg1.is_webxdc()
assert msg1.filename
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.is_webxdc()
payload = "A" * 1000
assert msg1.send_status_update({"payload": payload}, "some test data")
ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
update = msg2.get_status_updates()[0]
assert update["payload"] == payload
def test_webxdc_download_on_demand(acfactory, data, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
acfactory.introduce_each_other([ac1, ac2])
chat = acfactory.get_accepted_chat(ac1, ac2)
msg1 = Message.new_empty(ac1, "webxdc")
msg1.set_text("message1")
msg1.set_file(data.get_path("webxdc/minimal.xdc"))
msg1 = chat.send_msg(msg1)
assert msg1.is_webxdc()
assert msg1.filename
msg2 = ac2._evtracker.wait_next_incoming_message()
assert msg2.is_webxdc()
lp.sec("ac2 sets download limit")
ac2.set_config("download_limit", "100")
assert msg1.send_status_update({"payload": base64.b64encode(os.urandom(50000))}, "some test data")
ac2_update = ac2._evtracker.wait_next_incoming_message()
assert ac2_update.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
assert not msg2.get_status_updates()
ac2_update.download_full()
ac2._evtracker.get_matching("DC_EVENT_WEBXDC_STATUS_UPDATE")
assert msg2.get_status_updates()
# Get a event notifying that the message disappeared from the chat.
msgs_changed_event = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
assert msgs_changed_event.data1 == msg2.chat.id
assert msgs_changed_event.data2 == 0
def test_mvbox_sentbox_threads(acfactory, lp):
lp.sec("ac1: start with mvbox thread")
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=True)
@@ -409,42 +351,7 @@ def test_move_works(acfactory):
# Message is downloaded
ev = ac2._evtracker.get_matching("DC_EVENT_INCOMING_MSG")
assert ev.data2 > dc.const.DC_CHAT_ID_LAST_SPECIAL
def test_move_avoids_loop(acfactory):
"""Test that the message is only moved once.
This is to avoid busy loop if moved message reappears in the Inbox
or some scanned folder later.
For example, this happens on servers that alias `INBOX.DeltaChat` to `DeltaChat` folder,
so the message moved to `DeltaChat` appears as a new message in the `INBOX.DeltaChat` folder.
We do not want to move this message from `INBOX.DeltaChat` to `DeltaChat` again.
"""
ac1 = acfactory.new_online_configuring_account()
ac2 = acfactory.new_online_configuring_account(mvbox_move=True)
acfactory.bring_accounts_online()
ac1_chat = acfactory.get_accepted_chat(ac1, ac2)
ac1_chat.send_text("Message 1")
# Message is moved to the DeltaChat folder and downloaded.
ac2_msg1 = ac2._evtracker.wait_next_incoming_message()
assert ac2_msg1.text == "Message 1"
# Move the message to the INBOX again.
ac2.direct_imap.select_folder("DeltaChat")
ac2.direct_imap.conn.move(["*"], "INBOX")
ac1_chat.send_text("Message 2")
ac2_msg2 = ac2._evtracker.wait_next_incoming_message()
assert ac2_msg2.text == "Message 2"
# Check that Message 1 is still in the INBOX folder
# and Message 2 is in the DeltaChat folder.
ac2.direct_imap.select_folder("INBOX")
assert len(ac2.direct_imap.get_all_messages()) == 1
ac2.direct_imap.select_folder("DeltaChat")
assert len(ac2.direct_imap.get_all_messages()) == 1
assert ev.data2 > const.DC_CHAT_ID_LAST_SPECIAL
def test_move_works_on_self_sent(acfactory):
@@ -627,8 +534,8 @@ def test_send_and_receive_message_markseen(acfactory, lp):
lp.step("1")
for _i in range(2):
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
assert ev.data1 > dc.const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > dc.const.DC_MSG_ID_LAST_SPECIAL
assert ev.data1 > const.DC_CHAT_ID_LAST_SPECIAL
assert ev.data2 > const.DC_MSG_ID_LAST_SPECIAL
lp.step("2")
# Check that ac1 marks the read receipt as read.
@@ -1479,7 +1386,7 @@ def test_reaction_to_partially_fetched_msg(acfactory, lp, tmp_path):
lp.sec("wait for ac2 to receive a reaction")
msg2 = ac2._evtracker.wait_next_reactions_changed()
assert msg2.get_sender_contact().addr == ac1_addr
assert msg2.download_state == dc.const.DC_DOWNLOAD_AVAILABLE
assert msg2.download_state == const.DC_DOWNLOAD_AVAILABLE
assert reactions_queue.get() == msg2
reactions = msg2.get_reactions()
contacts = reactions.get_contacts()
@@ -1564,7 +1471,7 @@ def test_import_export_online_all(acfactory, tmp_path, data, lp):
lp.sec(f"export all to {backupdir}")
with ac1.temp_plugin(ImexTracker()) as imex_tracker:
ac1.stop_io()
ac1.imex(str(backupdir), dc.const.DC_IMEX_EXPORT_BACKUP)
ac1.imex(str(backupdir), const.DC_IMEX_EXPORT_BACKUP)
# check progress events for export
assert imex_tracker.wait_progress(1, progress_upper_limit=249)
@@ -1679,36 +1586,6 @@ def test_qr_join_chat(acfactory, lp):
ac1._evtracker.wait_securejoin_inviter_progress(1000)
def test_qr_new_group_unblocked(acfactory, lp):
"""Regression test for a bug intoduced in core v1.113.0.
ac2 scans a verified group QR code created by ac1.
This results in creation of a blocked 1:1 chat with ac1 on ac2,
but ac1 contact is not blocked on ac2.
Then ac1 creates a group, adds ac2 there and promotes it by sending a message.
ac2 should receive a message and create a contact request for the group.
Due to a bug previously ac2 created a blocked group.
"""
ac1, ac2 = acfactory.get_online_accounts(2)
ac1_chat = ac1.create_group_chat("Group for joining", verified=True)
qr = ac1_chat.get_join_qr()
ac2.qr_join_chat(qr)
ac1._evtracker.wait_securejoin_inviter_progress(1000)
ac1_new_chat = ac1.create_group_chat("Another group")
ac1_new_chat.add_contact(ac2)
ac1_new_chat.send_text("Hello!")
# Receive "Member added" message.
ac2._evtracker.wait_next_incoming_message()
# Receive "Hello!" message.
ac2_msg = ac2._evtracker.wait_next_incoming_message()
assert ac2_msg.text == "Hello!"
assert ac2_msg.chat.is_contact_request()
def test_qr_email_capitalization(acfactory, lp):
"""Regression test for a bug
that resulted in failure to propagate verification via gossip in a verified group
@@ -1926,15 +1803,13 @@ 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
@@ -1960,15 +1835,15 @@ def test_connectivity(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2)
ac1.set_config("scan_all_folders_debounce_secs", "0")
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTED)
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTED)
lp.sec("Test stop_io() and start_io()")
ac1.stop_io()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1.start_io()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTING, dc.const.DC_CONNECTIVITY_CONNECTED)
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTING, const.DC_CONNECTIVITY_CONNECTED)
lp.sec(
"Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, "
@@ -1989,8 +1864,8 @@ def test_connectivity(acfactory, lp):
ac2.create_chat(ac1).send_text("Hi 2")
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_CONNECTED, dc.const.DC_CONNECTIVITY_WORKING)
ac1._evtracker.wait_for_connectivity_change(dc.const.DC_CONNECTIVITY_WORKING, dc.const.DC_CONNECTIVITY_CONNECTED)
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTED, const.DC_CONNECTIVITY_WORKING)
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_WORKING, const.DC_CONNECTIVITY_CONNECTED)
msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 2
@@ -2000,7 +1875,7 @@ def test_connectivity(acfactory, lp):
ac1.maybe_network()
while 1:
assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
if ac1.all_work_done():
break
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
@@ -2015,7 +1890,7 @@ def test_connectivity(acfactory, lp):
ac1.maybe_network()
while 1:
assert ac1.get_connectivity() == dc.const.DC_CONNECTIVITY_CONNECTED
assert ac1.get_connectivity() == const.DC_CONNECTIVITY_CONNECTED
if ac1.all_work_done():
break
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
@@ -2024,10 +1899,10 @@ def test_connectivity(acfactory, lp):
ac1.set_config("configured_mail_pw", "abc")
ac1.stop_io()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1.start_io()
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity(dc.const.DC_CONNECTIVITY_NOT_CONNECTED)
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_NOT_CONNECTED)
def test_fetch_deleted_msg(acfactory, lp):
@@ -2510,9 +2385,9 @@ def test_archived_muted_chat(acfactory, lp):
lp.sec("wait for ac2 to receive DC_EVENT_MSGS_CHANGED for DC_CHAT_ID_ARCHIVED_LINK")
while 1:
ev = ac2._evtracker.get_matching("DC_EVENT_MSGS_CHANGED")
if ev.data1 == dc.const.DC_CHAT_ID_ARCHIVED_LINK:
if ev.data1 == const.DC_CHAT_ID_ARCHIVED_LINK:
assert ev.data2 == 0
archive = ac2.get_chat_by_id(dc.const.DC_CHAT_ID_ARCHIVED_LINK)
archive = ac2.get_chat_by_id(const.DC_CHAT_ID_ARCHIVED_LINK)
assert archive.count_fresh_messages() == 1
assert chat2.count_fresh_messages() == 1
break

View File

@@ -49,9 +49,10 @@ class TestOnlineInCreation:
assert str(tmp_path) != ac1.get_blobdir()
src = tmp_path / "file.txt"
src.write_text("hello there\n")
msg = chat.send_file(str(src))
assert msg.filename.startswith(os.path.join(ac1.get_blobdir(), "file"))
assert msg.filename.endswith(".txt")
chat.send_file(str(src))
blob_src = os.path.join(ac1.get_blobdir(), "file.txt")
assert os.path.exists(blob_src), "file.txt not copied to blobdir"
def test_forward_increation(self, acfactory, data, lp):
ac1, ac2 = acfactory.get_online_accounts(2)

View File

@@ -4,11 +4,12 @@ from datetime import datetime, timedelta, timezone
import pytest
import deltachat as dc
from deltachat import Account, const
from deltachat.capi import ffi, lib
from deltachat.cutil import iter_array
from deltachat.hookspec import account_hookimpl
from deltachat.message import Message
from deltachat.tracker import ImexFailed
from deltachat import Account, account_hookimpl, Message
@pytest.mark.parametrize(
@@ -67,9 +68,10 @@ 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_secret
ac._preconfigure_keypair("alice@example.org", alice_secret)
assert alice_public and alice_secret
ac._preconfigure_keypair("alice@example.org", alice_public, alice_secret)
def test_getinfo(self, acfactory):
ac1 = acfactory.get_unconfigured_account()
@@ -297,13 +299,13 @@ class TestOfflineChat:
assert not d["draft"] if chat.get_draft() is None else chat.get_draft()
def test_group_chat_creation_with_translation(self, ac1):
ac1.set_stock_translation(dc.const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s")
ac1.set_stock_translation(const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s")
ac1._evtracker.consume_events()
with pytest.raises(ValueError):
ac1.set_stock_translation(dc.const.DC_STR_FILE, "xyz %1$s")
ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s")
ac1._evtracker.get_matching("DC_EVENT_WARNING")
with pytest.raises(ValueError):
ac1.set_stock_translation(dc.const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
ac1.set_stock_translation(const.DC_STR_CONTACT_NOT_VERIFIED, "xyz %2$s")
ac1._evtracker.get_matching("DC_EVENT_WARNING")
with pytest.raises(ValueError):
ac1.set_stock_translation(500, "xyz %1$s")
@@ -479,19 +481,6 @@ class TestOfflineChat:
contact2 = ac1.create_contact("display1 <x@example.org>", "real")
assert contact2.name == "real"
def test_send_lots_of_offline_msgs(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
ac1.set_config("configured_mail_server", "example.org")
ac1.set_config("configured_mail_user", "example.org")
ac1.set_config("configured_mail_pw", "example.org")
ac1.set_config("configured_send_server", "example.org")
ac1.set_config("configured_send_user", "example.org")
ac1.set_config("configured_send_pw", "example.org")
ac1.start_io()
chat = ac1.create_contact("some1@example.org", name="some1").create_chat()
for i in range(50):
chat.send_text("hello")
def test_create_chat_simple(self, acfactory):
ac1 = acfactory.get_pseudo_configured_account()
contact1 = ac1.create_contact("some1@example.org", name="some1")
@@ -816,7 +805,7 @@ class TestOfflineChat:
lp.sec("check message count of only system messages (without daymarkers)")
dc_array = ffi.gc(
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, dc.const.DC_GCM_INFO_ONLY, 0),
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, const.DC_GCM_INFO_ONLY, 0),
lib.dc_array_unref,
)
assert len(list(iter_array(dc_array, lambda x: x))) == 2

View File

@@ -1,8 +1,6 @@
import json
from queue import Queue
import deltachat as dc
from deltachat import capi, cutil, register_global_plugin
from deltachat import capi, const, cutil, register_global_plugin
from deltachat.capi import ffi, lib
from deltachat.hookspec import global_hookimpl
from deltachat.testplugin import (
@@ -134,20 +132,20 @@ def test_empty_blobdir(tmp_path):
def test_event_defines():
assert dc.const.DC_EVENT_INFO == 100
assert dc.const.DC_CONTACT_ID_SELF
assert const.DC_EVENT_INFO == 100
assert const.DC_CONTACT_ID_SELF
def test_sig():
sig = capi.lib.dc_event_has_string_data
assert not sig(dc.const.DC_EVENT_MSGS_CHANGED)
assert sig(dc.const.DC_EVENT_INFO)
assert sig(dc.const.DC_EVENT_WARNING)
assert sig(dc.const.DC_EVENT_ERROR)
assert sig(dc.const.DC_EVENT_SMTP_CONNECTED)
assert sig(dc.const.DC_EVENT_IMAP_CONNECTED)
assert sig(dc.const.DC_EVENT_SMTP_MESSAGE_SENT)
assert sig(dc.const.DC_EVENT_IMEX_FILE_WRITTEN)
assert not sig(const.DC_EVENT_MSGS_CHANGED)
assert sig(const.DC_EVENT_INFO)
assert sig(const.DC_EVENT_WARNING)
assert sig(const.DC_EVENT_ERROR)
assert sig(const.DC_EVENT_SMTP_CONNECTED)
assert sig(const.DC_EVENT_IMAP_CONNECTED)
assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT)
assert sig(const.DC_EVENT_IMEX_FILE_WRITTEN)
def test_markseen_invalid_message_ids(acfactory):
@@ -227,26 +225,10 @@ def test_jsonrpc_blocking_call(tmp_path):
lib.dc_accounts_unref,
)
jsonrpc = ffi.gc(lib.dc_jsonrpc_init(accounts), lib.dc_jsonrpc_unref)
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"),
),
),
res = from_optional_dc_charpointer(
lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice@example.org"]'),
)
assert res == {"jsonrpc": "2.0", "id": "123", "result": True}
assert res == "true"
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}
res = from_optional_dc_charpointer(lib.dc_jsonrpc_blocking_call(jsonrpc, b"check_email_validity", b'["alice"]'))
assert res == "false"

View File

@@ -25,9 +25,6 @@ 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 =

View File

@@ -1 +1 @@
2023-10-05
2023-06-15

View File

@@ -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 local python test development environment.
- `make-python-testenv.sh` creates or updates 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.
@@ -28,6 +28,8 @@ and an own build machine.
- `zig-rpc-server.sh` compiles binaries of `deltachat-rpc-server` using Zig toolchain statically linked against musl libc.
- `zig-ffi.sh` compiles C libraries into LLVM bitcode using Zig toolchain with musl libc.
- `android-rpc-server.sh` compiles binaries of `deltachat-rpc-server` using Android NDK.
## Triggering runs on the build machine locally (fast!)

View File

@@ -7,7 +7,7 @@ set -euo pipefail
#
# Avoid using rustup here as it depends on reading /proc/self/exe and
# has problems running under QEMU.
RUST_VERSION=1.72.0
RUST_VERSION=1.68.0
ARCH="$(uname -m)"
test -f "/lib/libc.musl-$ARCH.so.1" && LIBC=musl || LIBC=gnu

View File

@@ -44,7 +44,7 @@ def file2url(f):
def process_opt(data):
if not "opt" in data:
return "ProviderOptions::new()"
return "Default::default()"
opt = "ProviderOptions {\n"
opt_data = data.get("opt", "")
for key in opt_data:
@@ -54,7 +54,7 @@ def process_opt(data):
if value in {"True", "False"}:
value = value.lower()
opt += " " + key + ": " + value + ",\n"
opt += " ..ProviderOptions::new()\n"
opt += " ..Default::default()\n"
opt += " }"
return opt
@@ -62,7 +62,7 @@ def process_opt(data):
def process_config_defaults(data):
if not "config_defaults" in data:
return "None"
defaults = "Some(&[\n"
defaults = "Some(vec![\n"
config_defaults = data.get("config_defaults", "")
for key in config_defaults:
value = str(config_defaults[key])
@@ -96,11 +96,11 @@ def process_data(data, file):
raise TypeError("domain used twice: " + domain)
domains_set.add(domain)
domains += ' ("' + domain + '", &' + file2varname(file) + "),\n"
domains += ' ("' + domain + '", &*' + file2varname(file) + "),\n"
comment += domain + ", "
ids = ""
ids += ' ("' + file2id(file) + '", &' + file2varname(file) + "),\n"
ids += ' ("' + file2id(file) + '", &*' + file2varname(file) + "),\n"
server = ""
has_imap = False
@@ -155,18 +155,18 @@ def process_data(data, file):
provider += (
"static "
+ file2varname(file)
+ ": Provider = Provider {\n"
+ ": Lazy<Provider> = Lazy::new(|| Provider {\n"
)
provider += ' id: "' + file2id(file) + '",\n'
provider += " status: Status::" + status.capitalize() + ",\n"
provider += ' before_login_hint: "' + before_login_hint + '",\n'
provider += ' after_login_hint: "' + after_login_hint + '",\n'
provider += ' overview_page: "' + file2url(file) + '",\n'
provider += " server: &[\n" + server + " ],\n"
provider += " server: vec![\n" + server + " ],\n"
provider += " opt: " + opt + ",\n"
provider += " config_defaults: " + config_defaults + ",\n"
provider += " oauth2_authorizer: " + oauth2 + ",\n"
provider += "};\n\n"
provider += "});\n\n"
else:
raise TypeError("SMTP and IMAP must be specified together or left out both")

View File

@@ -4,8 +4,8 @@
# It rebuilds the core and bindings as needed.
#
# After running the script, you can either
# run `pytest` directly with `venv/bin/pytest python/`
# or activate the environment with `. venv/bin/activate`
# run `pytest` directly with `env/bin/pytest python/`
# or activate the environment with `. env/bin/activacte`
# and run `pytest` from there.
set -euo pipefail
@@ -13,5 +13,9 @@ export DCC_RS_TARGET=debug
export DCC_RS_DEV="$PWD"
cargo build -p deltachat_ffi --features jsonrpc
tox -c python -e py --devenv venv
env/bin/pip install --upgrade pip
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

View File

@@ -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,py312,pypy37,pypy38,pypy39,pypy310 --skip-missing-interpreters true
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,py311,pypy37,pypy38,pypy39 --skip-missing-interpreters true
auditwheel repair "$TOXWORKDIR"/wheelhouse/deltachat* -w "$TOXWORKDIR/wheelhouse"

42
scripts/zig-ffi.sh Executable file
View File

@@ -0,0 +1,42 @@
#!/bin/sh
#
# Build FFI library using zig.
set -x
set -e
export RUSTFLAGS="-Clinker-plugin-lto"
export CFLAGS="-fno-unwind-tables -fno-exceptions -fno-asynchronous-unwind-tables -fomit-frame-pointer"
build() {
cargo build --release --target $1 -p deltachat_ffi --features vendored
}
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_I686_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="x86-linux-musl" \
build i686-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="arm-linux-musleabihf" \
ZIG_CPU="generic+v7a+vfp3-d32+thumb2-neon" \
build armv7-unknown-linux-musleabihf
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="x86_64-linux-musl" \
build x86_64-unknown-linux-musl
CC="$PWD/scripts/zig-cc" \
TARGET_CC="$PWD/scripts/zig-cc" \
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_LINKER="$PWD/scripts/zig-cc" \
LD="$PWD/scripts/zig-cc" \
ZIG_TARGET="aarch64-linux-musl" \
build aarch64-unknown-linux-musl

View File

@@ -9,9 +9,9 @@ set -e
unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.72.0
export RUSTUP_TOOLCHAIN=1.70.0
ZIG_VERSION=0.11.0
ZIG_VERSION=0.11.0-dev.2213+515e1c93e
# Download Zig
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"

View File

@@ -8,9 +8,9 @@ set -e
unset RUSTFLAGS
# Pin Rust version to avoid uncontrolled changes in the compiler and linker flags.
export RUSTUP_TOOLCHAIN=1.72.0
export RUSTUP_TOOLCHAIN=1.70.0
ZIG_VERSION=0.11.0
ZIG_VERSION=0.11.0-dev.2213+515e1c93e
# Download Zig
rm -fr "$ZIG_VERSION" "zig-linux-x86_64-$ZIG_VERSION.tar.xz"

View File

@@ -296,10 +296,10 @@ impl Accounts {
}
/// Configuration file name.
const CONFIG_NAME: &str = "accounts.toml";
pub const CONFIG_NAME: &str = "accounts.toml";
/// Database file name.
const DB_NAME: &str = "dc.db";
pub const DB_NAME: &str = "dc.db";
/// Account manager configuration file.
#[derive(Debug, Clone, PartialEq)]

View File

@@ -357,6 +357,7 @@ mod tests {
use super::*;
use crate::aheader::EncryptPreference;
use crate::e2ee;
use crate::message;
use crate::mimeparser;
use crate::peerstate::Peerstate;
use crate::securejoin::get_securejoin_qr;
@@ -704,7 +705,7 @@ Authentication-Results: dkim=";
let received = tcm
.try_send_recv(&alice, &bob2, "My credit card number is 1234")
.await;
assert!(!received.text.contains("1234"));
assert!(!received.text.as_ref().unwrap().contains("1234"));
assert!(received.error.is_some());
tcm.section("Turns out bob2 wasn't an attacker at all, Bob just has a new phone and DKIM just stopped working.");
@@ -785,7 +786,7 @@ Authentication-Results: dkim=";
.insert_str(0, "List-Post: <mailto:deltachat-community.example.net>\n");
let rcvd = alice.recv_msg(&sent).await;
assert!(!rcvd.get_showpadlock());
assert_eq!(&rcvd.text, "hellooo in the mailinglist again");
assert_eq!(&rcvd.text.unwrap(), "hellooo in the mailinglist again");
Ok(())
}
@@ -824,9 +825,7 @@ Authentication-Results: dkim=";
// Disallowing keychanges is disabled for now:
// assert!(rcvd.error.unwrap().contains("DKIM failed"));
// The message info should contain a warning:
assert!(rcvd
.id
.get_info(&bob)
assert!(message::get_msg_info(&bob, rcvd.id)
.await
.unwrap()
.contains("KEYCHANGES NOT ALLOWED"));

View File

@@ -9,7 +9,7 @@ use std::path::{Path, PathBuf};
use anyhow::{format_err, Context as _, Result};
use futures::StreamExt;
use image::{DynamicImage, GenericImageView, ImageFormat, ImageOutputFormat};
use image::{DynamicImage, ImageFormat, ImageOutputFormat};
use num_traits::FromPrimitive;
use tokio::io::AsyncWriteExt;
use tokio::{fs, io};
@@ -323,35 +323,18 @@ impl<'a> BlobObject<'a> {
MediaQuality::Worse => constants::WORSE_AVATAR_SIZE,
};
let maybe_sticker = &mut false;
let strict_limits = true;
// max_bytes is 20_000 bytes: Outlook servers don't allow headers larger than 32k.
// 32 / 4 * 3 = 24k if you account for base64 encoding. To be safe, we reduced this to 20k.
if let Some(new_name) = self.recode_to_size(
context,
blob_abs,
maybe_sticker,
img_wh,
20_000,
strict_limits,
)? {
if let Some(new_name) =
self.recode_to_size(context, blob_abs, img_wh, 20_000, strict_limits)?
{
self.name = new_name;
}
Ok(())
}
/// Recodes an image pointed by a [BlobObject] so that it fits into limits on the image width,
/// height and file size specified by the config.
///
/// On some platforms images are passed to the core as [`crate::message::Viewtype::Sticker`] in
/// which case `maybe_sticker` flag should be set. We recheck if an image is a true sticker
/// assuming that it must have at least one fully transparent corner, otherwise this flag is
/// reset.
pub async fn recode_to_image_size(
&mut self,
context: &Context,
maybe_sticker: &mut bool,
) -> Result<()> {
pub async fn recode_to_image_size(&mut self, context: &Context) -> Result<()> {
let blob_abs = self.to_abs_path();
let (img_wh, max_bytes) =
match MediaQuality::from_i32(context.get_config_int(Config::MediaQuality).await?)
@@ -364,14 +347,9 @@ impl<'a> BlobObject<'a> {
MediaQuality::Worse => (constants::WORSE_IMAGE_SIZE, constants::WORSE_IMAGE_BYTES),
};
let strict_limits = false;
if let Some(new_name) = self.recode_to_size(
context,
blob_abs,
maybe_sticker,
img_wh,
max_bytes,
strict_limits,
)? {
if let Some(new_name) =
self.recode_to_size(context, blob_abs, img_wh, max_bytes, strict_limits)?
{
self.name = new_name;
}
Ok(())
@@ -380,37 +358,20 @@ impl<'a> BlobObject<'a> {
/// If `!strict_limits`, then if `max_bytes` is exceeded, reduce the image to `img_wh` and just
/// proceed with the result.
fn recode_to_size(
&mut self,
&self,
context: &Context,
mut blob_abs: PathBuf,
maybe_sticker: &mut bool,
mut img_wh: u32,
max_bytes: usize,
strict_limits: bool,
) -> Result<Option<String>> {
let mut no_exif = false;
let no_exif_ref = &mut no_exif;
let res = tokio::task::block_in_place(move || {
let (nr_bytes, exif) = self.metadata()?;
*no_exif_ref = exif.is_none();
tokio::task::block_in_place(move || {
let mut img = image::open(&blob_abs).context("image decode failure")?;
let (nr_bytes, exif) = self.metadata()?;
let orientation = exif.as_ref().map(|exif| exif_orientation(exif, context));
let mut encoded = Vec::new();
let mut changed_name = None;
if *maybe_sticker {
let x_max = img.width().saturating_sub(1);
let y_max = img.height().saturating_sub(1);
*maybe_sticker = img.in_bounds(x_max, y_max)
&& (img.get_pixel(0, 0).0[3] == 0
|| img.get_pixel(x_max, 0).0[3] == 0
|| img.get_pixel(0, y_max).0[3] == 0
|| img.get_pixel(x_max, y_max).0[3] == 0);
}
if *maybe_sticker && exif.is_none() {
return Ok(None);
}
img = match orientation {
Some(90) => img.rotate90(),
Some(180) => img.rotate180(),
@@ -508,21 +469,7 @@ impl<'a> BlobObject<'a> {
}
Ok(changed_name)
});
match res {
Ok(_) => res,
Err(err) => {
if !strict_limits && no_exif {
warn!(
context,
"Cannot recode image, using original data: {err:#}.",
);
Ok(None)
} else {
Err(err)
}
}
}
})
}
/// Returns image file size and Exif.
@@ -913,18 +860,10 @@ mod tests {
file.metadata().await.unwrap().len()
}
let mut blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
let maybe_sticker = &mut false;
let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
let strict_limits = true;
blob.recode_to_size(
&t,
blob.to_abs_path(),
maybe_sticker,
1000,
3000,
strict_limits,
)
.unwrap();
blob.recode_to_size(&t, blob.to_abs_path(), 1000, 3000, strict_limits)
.unwrap();
assert!(file_size(&avatar_blob).await <= 3000);
assert!(file_size(&avatar_blob).await > 2000);
tokio::task::block_in_place(move || {
@@ -984,7 +923,6 @@ mod tests {
async fn test_recode_image_1() {
let bytes = include_bytes!("../test-data/image/avatar1000x1000.jpg");
send_image_check_mediaquality(
Viewtype::Image,
Some("0"),
bytes,
"jpg",
@@ -998,7 +936,6 @@ mod tests {
.await
.unwrap();
send_image_check_mediaquality(
Viewtype::Image,
Some("1"),
bytes,
"jpg",
@@ -1018,7 +955,6 @@ mod tests {
// The "-rotated" files are rotated by 270 degrees using the Exif metadata
let bytes = include_bytes!("../test-data/image/rectangle2000x1800-rotated.jpg");
let img_rotated = send_image_check_mediaquality(
Viewtype::Image,
Some("0"),
bytes,
"jpg",
@@ -1038,7 +974,6 @@ mod tests {
let bytes = buf.into_inner();
let img_rotated = send_image_check_mediaquality(
Viewtype::Image,
Some("1"),
&bytes,
"jpg",
@@ -1059,7 +994,6 @@ mod tests {
let bytes = include_bytes!("../test-data/image/screenshot.png");
send_image_check_mediaquality(
Viewtype::Image,
Some("0"),
bytes,
"png",
@@ -1074,7 +1008,6 @@ mod tests {
.unwrap();
send_image_check_mediaquality(
Viewtype::Image,
Some("1"),
bytes,
"png",
@@ -1087,29 +1020,12 @@ mod tests {
)
.await
.unwrap();
// This will be sent as Image, see [`BlobObject::maybe_sticker`] for explanation.
send_image_check_mediaquality(
Viewtype::Sticker,
Some("0"),
bytes,
"png",
false, // no Exif
1920,
1080,
0,
1920,
1080,
)
.await
.unwrap();
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_recode_image_huge_jpg() {
let bytes = include_bytes!("../test-data/image/screenshot.jpg");
send_image_check_mediaquality(
Viewtype::Image,
Some("0"),
bytes,
"jpg",
@@ -1143,7 +1059,6 @@ mod tests {
#[allow(clippy::too_many_arguments)]
async fn send_image_check_mediaquality(
viewtype: Viewtype,
media_quality_config: Option<&str>,
bytes: &[u8],
extension: &str,
@@ -1175,7 +1090,7 @@ mod tests {
assert!(exif.is_none());
}
let mut msg = Message::new(viewtype);
let mut msg = Message::new(Viewtype::Image);
msg.set_file(file.to_str().unwrap(), None);
let chat = alice.create_chat(&bob).await;
let sent = alice.send_msg(chat.id, &mut msg).await;
@@ -1189,7 +1104,6 @@ mod tests {
);
let bob_msg = bob.recv_msg(&sent).await;
assert_eq!(bob_msg.get_viewtype(), Viewtype::Image);
assert_eq!(bob_msg.get_width() as u32, compressed_width);
assert_eq!(bob_msg.get_height() as u32, compressed_height);
let file = bob_msg.get_file(&bob).unwrap();

File diff suppressed because it is too large Load Diff

View File

@@ -10,10 +10,8 @@ use crate::constants::{
use crate::contact::{Contact, ContactId};
use crate::context::Context;
use crate::message::{Message, MessageState, MsgId};
use crate::param::{Param, Params};
use crate::stock_str;
use crate::summary::Summary;
use crate::tools::IsNoneOrEmpty;
/// An object representing a single chatlist in memory.
///
@@ -206,84 +204,34 @@ impl Chatlist {
)
.await?
} else {
let mut ids = if flag_for_forwarding {
let sort_id_up = ChatId::lookup_by_contact(context, ContactId::SELF)
// show normal chatlist
let sort_id_up = if flag_for_forwarding {
ChatId::lookup_by_contact(context, ContactId::SELF)
.await?
.unwrap_or_default();
let process_row = |row: &rusqlite::Row| {
let chat_id: ChatId = row.get(0)?;
let typ: Chattype = row.get(1)?;
let param: Params = row.get::<_, String>(2)?.parse().unwrap_or_default();
let msg_id: Option<MsgId> = row.get(3)?;
Ok((chat_id, typ, param, msg_id))
};
let process_rows = |rows: rusqlite::MappedRows<_>| {
rows.filter_map(|row: std::result::Result<(_, _, Params, _), _>| match row {
Ok((chat_id, typ, param, msg_id)) => {
if typ == Chattype::Mailinglist
&& param.get(Param::ListPost).is_none_or_empty()
{
None
} else {
Some(Ok((chat_id, msg_id)))
}
}
Err(e) => Some(Err(e)),
})
.collect::<std::result::Result<Vec<_>, _>>()
.map_err(Into::into)
};
// Return ProtectionBroken chats also, as that may happen to a verified chat at any
// time. It may be confusing if a chat that is normally in the list disappears
// suddenly. The UI need to deal with that case anyway.
context.sql.query_map(
"SELECT c.id, c.type, c.param, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.id=(
SELECT id
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9 AND c.id!=?
AND c.blocked=0
AND NOT c.archived=?
AND (c.type!=? OR c.id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?))
GROUP BY c.id
ORDER BY c.id=? DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(
MessageState::OutDraft, skip_id, ChatVisibility::Archived,
Chattype::Group, ContactId::SELF,
sort_id_up, ChatVisibility::Pinned,
),
process_row,
process_rows,
).await?
.unwrap_or_default()
} else {
// show normal chatlist
context.sql.query_map(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.id=(
SELECT id
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9 AND c.id!=?
AND (c.blocked=0 OR c.blocked=2)
AND NOT c.archived=?
GROUP BY c.id
ORDER BY c.id=0 DESC, c.archived=? DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, skip_id, ChatVisibility::Archived, ChatVisibility::Pinned),
process_row,
process_rows,
).await?
ChatId::new(0)
};
let mut ids = context.sql.query_map(
"SELECT c.id, m.id
FROM chats c
LEFT JOIN msgs m
ON c.id=m.chat_id
AND m.id=(
SELECT id
FROM msgs
WHERE chat_id=c.id
AND (hidden=0 OR state=?1)
ORDER BY timestamp DESC, id DESC LIMIT 1)
WHERE c.id>9 AND c.id!=?2
AND (c.blocked=0 OR (c.blocked=2 AND NOT ?3))
AND NOT c.archived=?4
GROUP BY c.id
ORDER BY c.id=?5 DESC, c.archived=?6 DESC, IFNULL(m.timestamp,c.created_timestamp) DESC, m.id DESC;",
(MessageState::OutDraft, skip_id, flag_for_forwarding, ChatVisibility::Archived, sort_id_up, ChatVisibility::Pinned),
process_row,
process_rows,
).await?;
if !flag_no_specials && get_archived_cnt(context).await? > 0 {
if ids.is_empty() && flag_add_alldone_hint {
ids.push((DC_CHAT_ID_ALLDONE_HINT, None));
@@ -296,27 +244,6 @@ impl Chatlist {
Ok(Chatlist { ids })
}
/// Converts list of chat IDs to a chatlist.
pub(crate) async fn from_chat_ids(context: &Context, chat_ids: &[ChatId]) -> Result<Self> {
let mut ids = Vec::new();
for &chat_id in chat_ids {
let msg_id: Option<MsgId> = context
.sql
.query_get_value(
"SELECT id
FROM msgs
WHERE chat_id=?1
AND (hidden=0 OR state=?2)
ORDER BY timestamp DESC, id DESC LIMIT 1",
(chat_id, MessageState::OutDraft),
)
.await
.with_context(|| format!("failed to get msg ID for chat {}", chat_id))?;
ids.push((chat_id, msg_id));
}
Ok(Chatlist { ids })
}
/// Find out the number of chats.
pub fn len(&self) -> usize {
self.ids.len()
@@ -392,7 +319,7 @@ impl Chatlist {
} else {
match chat.typ {
Chattype::Group | Chattype::Broadcast | Chattype::Mailinglist => {
let lastcontact = Contact::get_by_id(context, lastmsg.from_id)
let lastcontact = Contact::load_from_db(context, lastmsg.from_id)
.await
.context("loading contact failed")?;
(Some(lastmsg), Some(lastcontact))
@@ -461,9 +388,7 @@ pub async fn get_last_message_for_chat(
#[cfg(test)]
mod tests {
use super::*;
use crate::chat::{
create_group_chat, get_chat_contacts, remove_contact_from_chat, ProtectionStatus,
};
use crate::chat::{create_group_chat, get_chat_contacts, ProtectionStatus};
use crate::message::Viewtype;
use crate::receive_imf::receive_imf;
use crate::stock_str::StockMessage;
@@ -499,7 +424,7 @@ mod tests {
// 2s here.
for chat_id in &[chat_id1, chat_id3, chat_id2] {
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hello".to_string());
msg.set_text(Some("hello".to_string()));
chat_id.set_draft(&t, Some(&mut msg)).await.unwrap();
}
@@ -548,14 +473,6 @@ mod tests {
.await
.unwrap()
.is_self_talk());
remove_contact_from_chat(&t, chats.get_chat_id(1).unwrap(), ContactId::SELF)
.await
.unwrap();
let chats = Chatlist::try_load(&t, DC_GCL_FOR_FORWARDING, None, None)
.await
.unwrap();
assert!(chats.len() == 1);
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
@@ -719,7 +636,7 @@ mod tests {
.unwrap();
let mut msg = Message::new(Viewtype::Text);
msg.set_text("foo:\nbar \r\n test".to_string());
msg.set_text(Some("foo:\nbar \r\n test".to_string()));
chat_id1.set_draft(&t, Some(&mut msg)).await.unwrap();
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();

View File

@@ -1,7 +1,6 @@
//! # Key-value configuration management.
use std::env;
use std::path::Path;
use std::str::FromStr;
use anyhow::{ensure, Context as _, Result};
@@ -286,12 +285,6 @@ 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"))]
@@ -336,11 +329,7 @@ impl Context {
let value = match key {
Config::Selfavatar => {
let rel_path = self.sql.get_raw_config(key.as_ref()).await?;
rel_path.map(|p| {
get_abs_path(self, Path::new(&p))
.to_string_lossy()
.into_owned()
})
rel_path.map(|p| get_abs_path(self, p).to_string_lossy().into_owned())
}
Config::SysVersion => Some((*DC_VERSION_STR).clone()),
Config::SysMsgsizeMaxRecommended => Some(format!("{RECOMMENDED_FILE_SIZE}")),
@@ -471,28 +460,6 @@ 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?;
}
@@ -642,18 +609,6 @@ 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;

View File

@@ -130,8 +130,8 @@ async fn on_configure_completed(
old_addr: Option<String>,
) -> Result<()> {
if let Some(provider) = param.provider {
if let Some(config_defaults) = provider.config_defaults {
for def in config_defaults {
if let Some(config_defaults) = &provider.config_defaults {
for def in config_defaults.iter() {
if !context.config_exists(def.key).await? {
info!(context, "apply config_defaults {}={}", def.key, def.value);
context.set_config(def.key, Some(def.value)).await?;
@@ -146,7 +146,7 @@ async fn on_configure_completed(
if !provider.after_login_hint.is_empty() {
let mut msg = Message::new(Viewtype::Text);
msg.text = provider.after_login_hint.to_string();
msg.text = Some(provider.after_login_hint.to_string());
if chat::add_device_msg(context, Some("core-provider-info"), Some(&mut msg))
.await
.is_err()
@@ -161,7 +161,7 @@ async fn on_configure_completed(
if !addr_cmp(&new_addr, &old_addr) {
let mut msg = Message::new(Viewtype::Text);
msg.text =
stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await;
Some(stock_str::aeap_explanation_and_link(context, &old_addr, &new_addr).await);
chat::add_device_msg(context, None, Some(&mut msg))
.await
.context("Cannot add AEAP explanation")
@@ -318,7 +318,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
}
// respect certificate setting from function parameters
for server in &mut servers {
for mut server in &mut servers {
let certificate_checks = match server.protocol {
Protocol::Imap => param.imap.certificate_checks,
Protocol::Smtp => param.smtp.certificate_checks,
@@ -462,12 +462,9 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
progress!(ctx, 910);
if let Some(configured_addr) = ctx.get_config(Config::ConfiguredAddr).await? {
if configured_addr != param.addr {
// Switched account, all server UIDs we know are invalid
info!(ctx, "Scheduling resync because the address has changed.");
job::schedule_resync(ctx).await?;
}
if ctx.get_config(Config::ConfiguredAddr).await?.as_deref() != Some(&param.addr) {
// Switched account, all server UIDs we know are invalid
job::schedule_resync(ctx).await?;
}
// the trailing underscore is correct
@@ -656,7 +653,7 @@ async fn try_smtp_one_param(
})
} else {
info!(context, "success: {}", inf);
smtp.disconnect();
smtp.disconnect().await;
Ok(())
}
}

View File

@@ -234,7 +234,7 @@ fn parse_serverparams(in_emailaddr: &str, xml_raw: &str) -> Result<Vec<ServerPar
let res = moz_ac
.incoming_servers
.into_iter()
.chain(moz_ac.outgoing_servers)
.chain(moz_ac.outgoing_servers.into_iter())
.filter_map(|server| {
let protocol = match server.typ.as_ref() {
"imap" => Some(Protocol::Imap),

View File

@@ -62,15 +62,8 @@ pub enum MediaQuality {
pub enum KeyGenType {
#[default]
Default = 0,
/// 2048-bit RSA.
Rsa2048 = 1,
/// [Ed25519](https://ed25519.cr.yp.to/) signature and X25519 encryption.
Ed25519 = 2,
/// 4096-bit RSA.
Rsa4096 = 3,
}
/// Video chat URL type.
@@ -238,7 +231,6 @@ mod tests {
assert_eq!(KeyGenType::Default, KeyGenType::from_i32(0).unwrap());
assert_eq!(KeyGenType::Rsa2048, KeyGenType::from_i32(1).unwrap());
assert_eq!(KeyGenType::Ed25519, KeyGenType::from_i32(2).unwrap());
assert_eq!(KeyGenType::Rsa4096, KeyGenType::from_i32(3).unwrap());
}
#[test]

View File

@@ -5,7 +5,7 @@ use std::collections::BinaryHeap;
use std::convert::{TryFrom, TryInto};
use std::fmt;
use std::ops::Deref;
use std::path::{Path, PathBuf};
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{bail, ensure, Context as _, Result};
@@ -109,7 +109,7 @@ impl ContactId {
/// ID of the contact for device messages.
pub const DEVICE: ContactId = ContactId::new(5);
pub(crate) const LAST_SPECIAL: ContactId = ContactId::new(9);
const LAST_SPECIAL: ContactId = ContactId::new(9);
/// Address to go with [`ContactId::DEVICE`].
///
@@ -338,33 +338,11 @@ impl Default for VerifiedStatus {
}
impl Contact {
/// Loads a single contact object from the database.
///
/// Returns an error if the contact does not exist.
///
/// For contact ContactId::SELF (1), the function returns sth.
/// like "Me" in the selected language and the email address
/// defined by set_config().
///
/// For contact ContactId::DEVICE, the function overrides
/// the contact name and status with localized address.
pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result<Self> {
let contact = Self::get_by_id_optional(context, contact_id)
.await?
.with_context(|| format!("contact {contact_id} not found"))?;
Ok(contact)
}
/// Loads a single contact object from the database.
///
/// Similar to [`Contact::get_by_id()`] but returns `None` if the contact does not exist.
pub async fn get_by_id_optional(
context: &Context,
contact_id: ContactId,
) -> Result<Option<Self>> {
if let Some(mut contact) = context
/// Loads a contact snapshot from the database.
pub async fn load_from_db(context: &Context, contact_id: ContactId) -> Result<Self> {
let mut contact = context
.sql
.query_row_optional(
.query_row(
"SELECT c.name, c.addr, c.origin, c.blocked, c.last_seen,
c.authname, c.param, c.status
FROM contacts c
@@ -393,27 +371,23 @@ impl Contact {
Ok(contact)
},
)
.await?
{
if contact_id == ContactId::SELF {
contact.name = stock_str::self_msg(context).await;
contact.addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
contact.status = context
.get_config(Config::Selfstatus)
.await?
.unwrap_or_default();
} else if contact_id == ContactId::DEVICE {
contact.name = stock_str::device_messages(context).await;
contact.addr = ContactId::DEVICE_ADDR.to_string();
contact.status = stock_str::device_messages_hint(context).await;
}
Ok(Some(contact))
} else {
Ok(None)
.await?;
if contact_id == ContactId::SELF {
contact.name = stock_str::self_msg(context).await;
contact.addr = context
.get_config(Config::ConfiguredAddr)
.await?
.unwrap_or_default();
contact.status = context
.get_config(Config::Selfstatus)
.await?
.unwrap_or_default();
} else if contact_id == ContactId::DEVICE {
contact.name = stock_str::device_messages(context).await;
contact.addr = ContactId::DEVICE_ADDR.to_string();
contact.status = stock_str::device_messages_hint(context).await;
}
Ok(contact)
}
/// Returns `true` if this contact is blocked.
@@ -433,13 +407,7 @@ impl Contact {
/// Check if a contact is blocked.
pub async fn is_blocked_load(context: &Context, id: ContactId) -> Result<bool> {
let blocked = context
.sql
.query_row("SELECT blocked FROM contacts WHERE id=?", (id,), |row| {
let blocked: bool = row.get(0)?;
Ok(blocked)
})
.await?;
let blocked = Self::load_from_db(context, id).await?.blocked;
Ok(blocked)
}
@@ -812,11 +780,7 @@ 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
@@ -836,7 +800,7 @@ impl Contact {
),
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
ContactId::LAST_SPECIAL,
minimal_origin,
Origin::IncomingReplyTo,
s3str_like_cmd,
s3str_like_cmd,
if flag_verified_only { 0i32 } else { 1i32 }
@@ -886,10 +850,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, minimal_origin]),
),
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_slice![
ContactId::LAST_SPECIAL,
Origin::IncomingReplyTo
])),
|row| row.get::<_, ContactId>(0),
|ids| {
for id in ids {
@@ -995,7 +959,7 @@ impl Contact {
);
let mut ret = String::new();
if let Ok(contact) = Contact::get_by_id(context, contact_id).await {
if let Ok(contact) = Contact::load_from_db(context, contact_id).await {
let loginparam = LoginParam::load_configured_params(context).await?;
let peerstate = Peerstate::from_addr(context, &contact.addr).await?;
@@ -1082,6 +1046,17 @@ impl Contact {
Ok(())
}
/// Get a single contact object. For a list, see eg. get_contacts().
///
/// For contact ContactId::SELF (1), the function returns sth.
/// like "Me" in the selected language and the email address
/// defined by set_config().
pub async fn get_by_id(context: &Context, contact_id: ContactId) -> Result<Contact> {
let contact = Contact::load_from_db(context, contact_id).await?;
Ok(contact)
}
/// Updates `param` column in the database.
pub async fn update_param(&self, context: &Context) -> Result<()> {
context
@@ -1145,29 +1120,11 @@ impl Contact {
&self.addr
}
/// Get a summary of authorized name and address.
///
/// The returned string is either "Name (email@domain.com)" or just
/// "email@domain.com" if the name is unset.
///
/// This string is suitable for sending over email
/// as it does not leak the locally set name.
pub fn get_authname_n_addr(&self) -> String {
if !self.authname.is_empty() {
format!("{} ({})", self.authname, self.addr)
} else {
(&self.addr).into()
}
}
/// Get a summary of name and address.
///
/// The returned string is either "Name (email@domain.com)" or just
/// "email@domain.com" if the name is unset.
///
/// The result should only be used locally and never sent over the network
/// as it leaks the local contact name.
///
/// The summary is typically used when asking the user something about the contact.
/// The attached email address makes the question unique, eg. "Chat with Alan Miller (am@uniquedomain.com)?"
pub fn get_name_n_addr(&self) -> String {
@@ -1190,7 +1147,7 @@ impl Contact {
}
} else if let Some(image_rel) = self.param.get(Param::ProfileImage) {
if !image_rel.is_empty() {
return Ok(Some(get_abs_path(context, Path::new(image_rel))));
return Ok(Some(get_abs_path(context, image_rel)));
}
}
Ok(None)
@@ -1256,22 +1213,11 @@ impl Contact {
/// Returns the ContactId that verified the contact.
pub async fn get_verifier_id(&self, context: &Context) -> Result<Option<ContactId>> {
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)
}
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)
}
}
@@ -1371,7 +1317,7 @@ async fn set_block_contact(
contact_id
);
let contact = Contact::get_by_id(context, contact_id).await?;
let contact = Contact::load_from_db(context, contact_id).await?;
if contact.blocked != new_blocking {
context
@@ -1433,7 +1379,7 @@ pub(crate) async fn set_profile_image(
profile_image: &AvatarAction,
was_encrypted: bool,
) -> Result<()> {
let mut contact = Contact::get_by_id(context, contact_id).await?;
let mut contact = Contact::load_from_db(context, contact_id).await?;
let changed = match profile_image {
AvatarAction::Change(profile_image) => {
if contact_id == ContactId::SELF {
@@ -1488,7 +1434,7 @@ pub(crate) async fn set_status(
.await?;
}
} else {
let mut contact = Contact::get_by_id(context, contact_id).await?;
let mut contact = Contact::load_from_db(context, contact_id).await?;
if contact.status != status {
contact.status = status;
@@ -1747,7 +1693,7 @@ mod tests {
assert_eq!(may_be_valid_addr("dd.tt"), false);
assert_eq!(may_be_valid_addr("tt.dd@uu"), true);
assert_eq!(may_be_valid_addr("u@d"), true);
assert_eq!(may_be_valid_addr("u@d."), false);
assert_eq!(may_be_valid_addr("u@d."), true);
assert_eq!(may_be_valid_addr("u@d.t"), true);
assert_eq!(may_be_valid_addr("u@d.tt"), true);
assert_eq!(may_be_valid_addr("u@.tt"), true);
@@ -1756,7 +1702,6 @@ mod tests {
assert_eq!(may_be_valid_addr("sk <@d.tt>"), false);
assert_eq!(may_be_valid_addr("as@sd.de>"), false);
assert_eq!(may_be_valid_addr("ask dkl@dd.tt"), false);
assert_eq!(may_be_valid_addr("user@domain.tld."), false);
}
#[test]
@@ -1807,7 +1752,7 @@ mod tests {
.await?;
assert_ne!(id, ContactId::UNDEFINED);
let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
let contact = Contact::load_from_db(&context.ctx, id).await.unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_authname(), "bob");
assert_eq!(contact.get_display_name(), "bob");
@@ -1835,7 +1780,7 @@ mod tests {
.await?;
assert_eq!(contact_bob_id, id);
assert_eq!(modified, Modifier::Modified);
let contact = Contact::get_by_id(&context.ctx, id).await.unwrap();
let contact = Contact::load_from_db(&context.ctx, id).await.unwrap();
assert_eq!(contact.get_name(), "someone");
assert_eq!(contact.get_authname(), "bob");
assert_eq!(contact.get_display_name(), "someone");
@@ -1901,7 +1846,7 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_id(), contact_id);
assert_eq!(contact.get_name(), "Name one");
assert_eq!(contact.get_authname(), "bla foo");
@@ -1920,7 +1865,7 @@ mod tests {
.unwrap();
assert_eq!(contact_id, contact_id_test);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "Real one");
assert_eq!(contact.get_addr(), "one@eins.org");
assert!(!contact.is_blocked());
@@ -1936,7 +1881,7 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::None);
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "three@drei.sam");
assert_eq!(contact.get_addr(), "three@drei.sam");
@@ -1953,7 +1898,7 @@ mod tests {
.unwrap();
assert_eq!(contact_id, contact_id_test);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name_n_addr(), "m. serious (three@drei.sam)");
assert!(!contact.is_blocked());
@@ -1968,7 +1913,7 @@ mod tests {
.unwrap();
assert_eq!(contact_id, contact_id_test);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "m. serious");
assert_eq!(contact.get_name_n_addr(), "schnucki (three@drei.sam)");
assert!(!contact.is_blocked());
@@ -1984,14 +1929,14 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::None);
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "Wonderland, Alice");
assert_eq!(contact.get_display_name(), "Wonderland, Alice");
assert_eq!(contact.get_addr(), "alice@w.de");
assert_eq!(contact.get_name_n_addr(), "Wonderland, Alice (alice@w.de)");
// check SELF
let contact = Contact::get_by_id(&t, ContactId::SELF).await.unwrap();
let contact = Contact::load_from_db(&t, ContactId::SELF).await.unwrap();
assert_eq!(contact.get_name(), stock_str::self_msg(&t).await);
assert_eq!(contact.get_addr(), ""); // we're not configured
assert!(!contact.is_blocked());
@@ -2022,7 +1967,7 @@ mod tests {
assert_eq!(chatlist.len(), 1);
let contacts = get_chat_contacts(&t, chat_id).await?;
let contact_id = contacts.first().unwrap();
let contact = Contact::get_by_id(&t, *contact_id).await?;
let contact = Contact::load_from_db(&t, *contact_id).await?;
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "f@example.org");
@@ -2048,7 +1993,7 @@ mod tests {
assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Flobbyfoo");
let chatlist = Chatlist::try_load(&t, 0, Some("flobbyfoo"), None).await?;
assert_eq!(chatlist.len(), 1);
let contact = Contact::get_by_id(&t, *contact_id).await?;
let contact = Contact::load_from_db(&t, *contact_id).await?;
assert_eq!(contact.get_authname(), "Flobbyfoo");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "Flobbyfoo");
@@ -2078,7 +2023,7 @@ mod tests {
assert_eq!(chatlist.len(), 0);
let chatlist = Chatlist::try_load(&t, 0, Some("Foo Flobby"), None).await?;
assert_eq!(chatlist.len(), 1);
let contact = Contact::get_by_id(&t, *contact_id).await?;
let contact = Contact::load_from_db(&t, *contact_id).await?;
assert_eq!(contact.get_authname(), "Foo Flobby");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "Foo Flobby");
@@ -2096,7 +2041,7 @@ mod tests {
assert_eq!(Chat::load_from_db(&t, chat_id).await?.name, "Falk");
let chatlist = Chatlist::try_load(&t, 0, Some("Falk"), None).await?;
assert_eq!(chatlist.len(), 1);
let contact = Contact::get_by_id(&t, *contact_id).await?;
let contact = Contact::load_from_db(&t, *contact_id).await?;
assert_eq!(contact.get_authname(), "Foo Flobby");
assert_eq!(contact.get_name(), "Falk");
assert_eq!(contact.get_display_name(), "Falk");
@@ -2135,7 +2080,7 @@ mod tests {
// If a contact has ongoing chats, contact is only hidden on deletion
Contact::delete(&alice, contact_id).await?;
let contact = Contact::get_by_id(&alice, contact_id).await?;
let contact = Contact::load_from_db(&alice, contact_id).await?;
assert_eq!(contact.origin, Origin::Hidden);
assert_eq!(
Contact::get_all(&alice, 0, Some("bob@example.net"))
@@ -2149,7 +2094,7 @@ mod tests {
// Can delete contact physically now
Contact::delete(&alice, contact_id).await?;
assert!(Contact::get_by_id(&alice, contact_id).await.is_err());
assert!(Contact::load_from_db(&alice, contact_id).await.is_err());
assert_eq!(
Contact::get_all(&alice, 0, Some("bob@example.net"))
.await?
@@ -2168,7 +2113,7 @@ mod tests {
let contact_id1 = Contact::create(&t, "Foo", "foo@bar.de").await?;
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
Contact::delete(&t, contact_id1).await?;
assert!(Contact::get_by_id(&t, contact_id1).await.is_err());
assert!(Contact::load_from_db(&t, contact_id1).await.is_err());
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
let contact_id2 = Contact::create(&t, "Foo", "foo@bar.de").await?;
assert_ne!(contact_id2, contact_id1);
@@ -2177,12 +2122,12 @@ mod tests {
// test recreation after hiding
t.create_chat_with_contact("Foo", "foo@bar.de").await;
Contact::delete(&t, contact_id2).await?;
let contact = Contact::get_by_id(&t, contact_id2).await?;
let contact = Contact::load_from_db(&t, contact_id2).await?;
assert_eq!(contact.origin, Origin::Hidden);
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 0);
let contact_id3 = Contact::create(&t, "Foo", "foo@bar.de").await?;
let contact = Contact::get_by_id(&t, contact_id3).await?;
let contact = Contact::load_from_db(&t, contact_id3).await?;
assert_eq!(contact.origin, Origin::ManuallyCreated);
assert_eq!(contact_id3, contact_id2);
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
@@ -2205,7 +2150,7 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::Created);
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "bob1");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "bob1");
@@ -2221,7 +2166,7 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "bob2");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "bob2");
@@ -2231,7 +2176,7 @@ mod tests {
.await
.unwrap();
assert!(!contact_id.is_special());
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "bob2");
assert_eq!(contact.get_name(), "bob3");
assert_eq!(contact.get_display_name(), "bob3");
@@ -2247,7 +2192,7 @@ mod tests {
.unwrap();
assert!(!contact_id.is_special());
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "bob4");
assert_eq!(contact.get_name(), "bob3");
assert_eq!(contact.get_display_name(), "bob3");
@@ -2260,7 +2205,7 @@ mod tests {
// manually create "claire@example.org" without a given name
let contact_id = Contact::create(&t, "", "claire@example.org").await.unwrap();
assert!(!contact_id.is_special());
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "claire@example.org");
@@ -2276,7 +2221,7 @@ mod tests {
.unwrap();
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "claire1");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "claire1");
@@ -2292,7 +2237,7 @@ mod tests {
.unwrap();
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "claire2");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "claire2");
@@ -2315,7 +2260,7 @@ mod tests {
)
.await?;
assert_eq!(sth_modified, Modifier::Created);
let contact = Contact::get_by_id(&t, contact_id).await?;
let contact = Contact::load_from_db(&t, contact_id).await?;
assert_eq!(contact.get_display_name(), "Bob");
// Incoming message from someone else with "Not Bob" <bob@example.org> in the "To:" field.
@@ -2328,7 +2273,7 @@ mod tests {
.await?;
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified);
let contact = Contact::get_by_id(&t, contact_id).await?;
let contact = Contact::load_from_db(&t, contact_id).await?;
assert_eq!(contact.get_display_name(), "Not Bob");
// Incoming message from Bob, changing the name back.
@@ -2341,7 +2286,7 @@ mod tests {
.await?;
assert_eq!(contact_id, contact_id_same);
assert_eq!(sth_modified, Modifier::Modified); // This was None until the bugfix
let contact = Contact::get_by_id(&t, contact_id).await?;
let contact = Contact::load_from_db(&t, contact_id).await?;
assert_eq!(contact.get_display_name(), "Bob");
Ok(())
@@ -2355,7 +2300,7 @@ mod tests {
let contact_id = Contact::create(&t, "dave1", "dave@example.org")
.await
.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "");
assert_eq!(contact.get_name(), "dave1");
assert_eq!(contact.get_display_name(), "dave1");
@@ -2369,14 +2314,14 @@ mod tests {
)
.await
.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "dave2");
assert_eq!(contact.get_name(), "dave1");
assert_eq!(contact.get_display_name(), "dave1");
// manually clear the name
Contact::create(&t, "", "dave@example.org").await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_authname(), "dave2");
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_display_name(), "dave2");
@@ -2394,21 +2339,21 @@ mod tests {
let t = TestContext::new().await;
let contact_id = Contact::create(&t, "", "<dave@example.org>").await.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "");
assert_eq!(contact.get_addr(), "dave@example.org");
let contact_id = Contact::create(&t, "", "Mueller, Dave <dave@example.org>")
.await
.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "Mueller, Dave");
assert_eq!(contact.get_addr(), "dave@example.org");
let contact_id = Contact::create(&t, "name1", "name2 <dave@example.org>")
.await
.unwrap();
let contact = Contact::get_by_id(&t, contact_id).await.unwrap();
let contact = Contact::load_from_db(&t, contact_id).await.unwrap();
assert_eq!(contact.get_name(), "name1");
assert_eq!(contact.get_addr(), "dave@example.org");
@@ -2652,7 +2597,7 @@ CCCB 5AA9 F6E1 141C 9431
Origin::ManuallyCreated,
)
.await?;
let contact = Contact::get_by_id(&alice, contact_id).await?;
let contact = Contact::load_from_db(&alice, contact_id).await?;
assert_eq!(contact.last_seen(), 0);
let mime = br#"Subject: Hello
@@ -2669,7 +2614,7 @@ Hi."#;
let timestamp = msg.get_timestamp();
assert!(timestamp > 0);
let contact = Contact::get_by_id(&alice, contact_id).await?;
let contact = Contact::load_from_db(&alice, contact_id).await?;
assert_eq!(contact.last_seen(), timestamp);
Ok(())

View File

@@ -332,12 +332,6 @@ impl Context {
}
}
/// Changes encrypted database passphrase.
pub async fn change_passphrase(&self, passphrase: String) -> Result<()> {
self.sql.change_passphrase(passphrase).await?;
Ok(())
}
/// Returns true if database is open.
pub async fn is_open(&self) -> bool {
self.sql.is_open().await
@@ -382,7 +376,7 @@ impl Context {
translated_stockstrings: stockstrings,
events,
scheduler: SchedulerState::new(),
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow at least 1 message every 10 seconds + a burst of 6.
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds.
quota: RwLock::new(None),
quota_update_request: AtomicBool::new(false),
resync_request: AtomicBool::new(false),
@@ -579,7 +573,6 @@ 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?;
@@ -692,7 +685,6 @@ 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);
@@ -816,22 +808,7 @@ 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 => {
// 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?
}
None => MsgId::new_unset(),
};
let list = self
@@ -1065,7 +1042,7 @@ mod tests {
async fn receive_msg(t: &TestContext, chat: &Chat) {
let members = get_chat_contacts(t, chat.id).await.unwrap();
let contact = Contact::get_by_id(t, *members.first().unwrap())
let contact = Contact::load_from_db(t, *members.first().unwrap())
.await
.unwrap();
let msg = format!(
@@ -1327,11 +1304,11 @@ mod tests {
// Add messages to chat with Bob.
let mut msg1 = Message::new(Viewtype::Text);
msg1.set_text("foobar".to_string());
msg1.set_text(Some("foobar".to_string()));
send_msg(&alice, chat.id, &mut msg1).await?;
let mut msg2 = Message::new(Viewtype::Text);
msg2.set_text("barbaz".to_string());
msg2.set_text(Some("barbaz".to_string()));
send_msg(&alice, chat.id, &mut msg2).await?;
// Global search with a part of text finds the message.
@@ -1427,7 +1404,7 @@ mod tests {
// Add 999 messages
let mut msg = Message::new(Viewtype::Text);
msg.set_text("foobar".to_string());
msg.set_text(Some("foobar".to_string()));
for _ in 0..999 {
send_msg(&alice, chat.id, &mut msg).await?;
}
@@ -1476,35 +1453,6 @@ mod tests {
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_context_change_passphrase() -> Result<()> {
let dir = tempdir()?;
let dbfile = dir.path().join("db.sqlite");
let id = 1;
let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new())
.await
.context("failed to create context")?;
assert_eq!(context.open("foo".to_string()).await?, true);
assert_eq!(context.is_open().await, true);
context
.set_config(Config::Addr, Some("alice@example.org"))
.await?;
context
.change_passphrase("bar".to_string())
.await
.context("Failed to change passphrase")?;
assert_eq!(
context.get_config(Config::Addr).await?.unwrap(),
"alice@example.org"
);
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ongoing() -> Result<()> {
let context = TestContext::new().await;

View File

@@ -29,30 +29,14 @@ pub fn try_decrypt(
private_keyring: &Keyring<SignedSecretKey>,
public_keyring_for_validate: &Keyring<SignedPublicKey>,
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
let encrypted_data_part = match {
let mime = get_autocrypt_mime(mail);
if mime.is_some() {
info!(context, "Detected Autocrypt-mime message.");
}
mime
}
.or_else(|| {
let mime = get_mixed_up_mime(mail);
if mime.is_some() {
info!(context, "Detected mixed-up mime message.");
}
mime
})
.or_else(|| {
let mime = get_attachment_mime(mail);
if mime.is_some() {
info!(context, "Detected attached Autocrypt-mime message.");
}
mime
}) {
let encrypted_data_part = match get_autocrypt_mime(mail)
.or_else(|| get_mixed_up_mime(mail))
.or_else(|| get_attachment_mime(mail))
{
None => return Ok(None),
Some(res) => res,
};
info!(context, "Detected Autocrypt-mime message");
decrypt_part(
encrypted_data_part,
@@ -415,22 +399,8 @@ mod tests {
let bob = TestContext::new_bob().await;
receive_imf(&bob, attachment_mime, false).await?;
let msg = bob.get_last_msg().await;
assert_eq!(msg.text, "Hello from Thunderbird!");
assert_eq!(msg.text.as_deref(), Some("Hello from Thunderbird!"));
Ok(())
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_mixed_up_mime_long() -> Result<()> {
// Long "mixed-up" mail as received when sending an encrypted message using Delta Chat
// Desktop via MS Exchange (actually made with TB though).
let mixed_up_mime = include_bytes!("../test-data/message/mixed-up-long.eml");
let bob = TestContext::new_bob().await;
receive_imf(&bob, mixed_up_mime, false).await?;
let msg = bob.get_last_msg().await;
assert!(!msg.get_text().is_empty());
assert!(msg.has_html());
assert!(msg.id.get_html(&bob).await?.unwrap().len() > 40000);
Ok(())
}
}

View File

@@ -10,11 +10,10 @@ use quick_xml::{
Reader,
};
use crate::simplify::{simplify_quote, SimplifiedText};
static LINE_RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
struct Dehtml {
strbuilder: String,
quote: String,
add_text: AddText,
last_href: Option<String>,
/// GMX wraps a quote in `<div name="quote">`. After a `<div name="quote">`, this count is
@@ -30,22 +29,17 @@ struct Dehtml {
}
impl Dehtml {
/// Returns true if HTML parser is currently inside the quote.
fn is_quote(&self) -> bool {
self.divs_since_quoted_content_div > 0 || self.blockquotes_since_blockquote > 0
}
/// Returns the buffer where the text should be written.
///
/// If the parser is inside the quote, returns the quote buffer.
fn get_buf(&mut self) -> &mut String {
if self.is_quote() {
&mut self.quote
fn line_prefix(&self) -> &str {
if self.divs_since_quoted_content_div > 0 || self.blockquotes_since_blockquote > 0 {
"> "
} else {
&mut self.strbuilder
""
}
}
fn append_prefix(&self, line_end: &str) -> String {
// line_end is e.g. "\n\n". We add "> " if necessary.
line_end.to_string() + self.line_prefix()
}
fn get_add_text(&self) -> AddText {
if self.divs_since_quote_div > 0 && self.divs_since_quoted_content_div == 0 {
AddText::No // Everything between `<div name="quoted">` and `<div name="quoted_content">` is metadata which we don't want
@@ -57,70 +51,30 @@ impl Dehtml {
#[derive(Debug, PartialEq, Clone, Copy)]
enum AddText {
/// Inside `<script>`, `<style>` and similar tags
/// which contents should not be displayed.
No,
YesRemoveLineEnds,
/// Inside `<pre>`.
YesPreserveLineEnds,
}
pub(crate) fn dehtml(buf: &str) -> Option<SimplifiedText> {
let (s, quote) = dehtml_quick_xml(buf);
// dehtml() returns way too many newlines; however, an optimisation on this issue is not needed as
// the newlines are typically removed in further processing by the caller
pub fn dehtml(buf: &str) -> Option<String> {
let s = dehtml_quick_xml(buf);
if !s.trim().is_empty() {
let text = dehtml_cleanup(s);
let top_quote = if !quote.trim().is_empty() {
Some(dehtml_cleanup(simplify_quote(&quote).0))
} else {
None
};
return Some(SimplifiedText {
text,
top_quote,
..Default::default()
});
return Some(s);
}
let s = dehtml_manually(buf);
if !s.trim().is_empty() {
let text = dehtml_cleanup(s);
return Some(SimplifiedText {
text,
..Default::default()
});
return Some(s);
}
None
}
fn dehtml_cleanup(mut text: String) -> String {
text.retain(|c| c != '\r');
let lines = text.trim().split('\n');
let mut text = String::new();
let mut linebreak = false;
for line in lines {
if line.chars().all(char::is_whitespace) {
linebreak = true;
} else {
if !text.is_empty() {
text += "\n";
if linebreak {
text += "\n";
}
}
text += line.trim_end();
linebreak = false;
}
}
text
}
fn dehtml_quick_xml(buf: &str) -> (String, String) {
fn dehtml_quick_xml(buf: &str) -> String {
let buf = buf.trim().trim_start_matches("<!doctype html>");
let mut dehtml = Dehtml {
strbuilder: String::with_capacity(buf.len()),
quote: String::new(),
add_text: AddText::YesRemoveLineEnds,
last_href: None,
divs_since_quote_div: 0,
@@ -172,33 +126,22 @@ fn dehtml_quick_xml(buf: &str) -> (String, String) {
buf.clear();
}
(dehtml.strbuilder, dehtml.quote)
dehtml.strbuilder
}
fn dehtml_text_cb(event: &BytesText, dehtml: &mut Dehtml) {
static LINE_RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"(\r?\n)+").unwrap());
if dehtml.get_add_text() == AddText::YesPreserveLineEnds
|| dehtml.get_add_text() == AddText::YesRemoveLineEnds
{
let last_added = escaper::decode_html_buf_sloppy(event as &[_]).unwrap_or_default();
if dehtml.get_add_text() == AddText::YesRemoveLineEnds {
// Replace all line ends with spaces.
// E.g. `\r\n\r\n` is replaced with one space.
let last_added = LINE_RE.replace_all(&last_added, " ");
// Add a space if `last_added` starts with a space
// and there is no whitespace at the end of the buffer yet.
// Trim the rest of leading whitespace from `last_added`.
let buf = dehtml.get_buf();
if !buf.ends_with(' ') && !buf.ends_with('\n') && last_added.starts_with(' ') {
*buf += " ";
}
*buf += last_added.trim_start();
dehtml.strbuilder += LINE_RE.replace_all(&last_added, "\r").as_ref();
} else if !dehtml.line_prefix().is_empty() {
let l = dehtml.append_prefix("\n");
dehtml.strbuilder += LINE_RE.replace_all(&last_added, l.as_str()).as_ref();
} else {
*dehtml.get_buf() += LINE_RE.replace_all(&last_added, "\n").as_ref();
dehtml.strbuilder += &last_added;
}
}
}
@@ -210,36 +153,35 @@ fn dehtml_endtag_cb(event: &BytesEnd, dehtml: &mut Dehtml) {
match tag.as_str() {
"style" | "script" | "title" | "pre" => {
*dehtml.get_buf() += "\n\n";
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
dehtml.add_text = AddText::YesRemoveLineEnds;
}
"div" => {
pop_tag(&mut dehtml.divs_since_quote_div);
pop_tag(&mut dehtml.divs_since_quoted_content_div);
*dehtml.get_buf() += "\n\n";
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
dehtml.add_text = AddText::YesRemoveLineEnds;
}
"a" => {
if let Some(ref last_href) = dehtml.last_href.take() {
let buf = dehtml.get_buf();
if buf.ends_with('[') {
buf.truncate(buf.len() - 1);
if dehtml.strbuilder.ends_with('[') {
dehtml.strbuilder.truncate(dehtml.strbuilder.len() - 1);
} else {
*buf += "](";
*buf += last_href;
*buf += ")";
dehtml.strbuilder += "](";
dehtml.strbuilder += last_href;
dehtml.strbuilder += ")";
}
}
}
"b" | "strong" => {
if dehtml.get_add_text() != AddText::No {
*dehtml.get_buf() += "*";
dehtml.strbuilder += "*";
}
}
"i" | "em" => {
if dehtml.get_add_text() != AddText::No {
*dehtml.get_buf() += "_";
dehtml.strbuilder += "_";
}
}
"blockquote" => pop_tag(&mut dehtml.blockquotes_since_blockquote),
@@ -259,7 +201,7 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
match tag.as_str() {
"p" | "table" | "td" => {
if !dehtml.strbuilder.is_empty() {
*dehtml.get_buf() += "\n\n";
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
}
dehtml.add_text = AddText::YesRemoveLineEnds;
}
@@ -268,18 +210,18 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
maybe_push_tag(event, reader, "quote", &mut dehtml.divs_since_quote_div);
maybe_push_tag(event, reader, "quoted-content", &mut dehtml.divs_since_quoted_content_div);
*dehtml.get_buf() += "\n\n";
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
dehtml.add_text = AddText::YesRemoveLineEnds;
}
"br" => {
*dehtml.get_buf() += "\n";
dehtml.strbuilder += &dehtml.append_prefix("\n");
dehtml.add_text = AddText::YesRemoveLineEnds;
}
"style" | "script" | "title" => {
dehtml.add_text = AddText::No;
}
"pre" => {
*dehtml.get_buf() += "\n\n";
dehtml.strbuilder += &dehtml.append_prefix("\n\n");
dehtml.add_text = AddText::YesPreserveLineEnds;
}
"a" => {
@@ -300,18 +242,18 @@ fn dehtml_starttag_cb<B: std::io::BufRead>(
if !href.is_empty() {
dehtml.last_href = Some(href);
*dehtml.get_buf() += "[";
dehtml.strbuilder += "[";
}
}
}
"b" | "strong" => {
if dehtml.get_add_text() != AddText::No {
*dehtml.get_buf() += "*";
dehtml.strbuilder += "*";
}
}
"i" | "em" => {
if dehtml.get_add_text() != AddText::No {
*dehtml.get_buf() += "_";
dehtml.strbuilder += "_";
}
}
"blockquote" => dehtml.blockquotes_since_blockquote += 1,
@@ -372,6 +314,7 @@ pub fn dehtml_manually(buf: &str) -> String {
#[cfg(test)]
mod tests {
use super::*;
use crate::simplify::{simplify, SimplifiedText};
#[test]
fn test_dehtml() {
@@ -385,18 +328,18 @@ mod tests {
("<b> bar <i> foo", "* bar _ foo"),
("&amp; bar", "& bar"),
// Despite missing ', this should be shown:
("<a href='/foo.png>Hi</a> ", "Hi"),
("No link: <a href='https://get.delta.chat/'/>", "No link:"),
("<a href='/foo.png>Hi</a> ", "Hi "),
("No link: <a href='https://get.delta.chat/'/>", "No link: "),
(
"No link: <a href='https://get.delta.chat/'></a>",
"No link:",
"No link: ",
),
("<!doctype html>\n<b>fat text</b>", "*fat text*"),
// Invalid html (at least DC should show the text if the html is invalid):
("<!some invalid html code>\n<b>some text</b>", "some text"),
];
for (input, output) in cases {
assert_eq!(dehtml(input).unwrap().text, output);
assert_eq!(simplify(dehtml(input).unwrap(), true).text, output);
}
let none_cases = vec!["<html> </html>", ""];
for input in none_cases {
@@ -406,54 +349,31 @@ mod tests {
#[test]
fn test_dehtml_parse_br() {
let html = "line1<br>line2";
let plain = dehtml(html).unwrap().text;
assert_eq!(plain, "line1\nline2");
let html = "line1<br> line2";
let plain = dehtml(html).unwrap().text;
assert_eq!(plain, "line1\nline2");
let html = "line1 <br><br> line2";
let plain = dehtml(html).unwrap().text;
assert_eq!(plain, "line1\n\nline2");
let html = "\r\r\nline1<br>\r\n\r\n\r\rline2<br/>line3\n\r";
let plain = dehtml(html).unwrap().text;
assert_eq!(plain, "line1\nline2\nline3");
}
let plain = dehtml(html).unwrap();
#[test]
fn test_dehtml_parse_span() {
assert_eq!(dehtml("<span>Foo</span>bar").unwrap().text, "Foobar");
assert_eq!(dehtml("<span>Foo</span> bar").unwrap().text, "Foo bar");
assert_eq!(dehtml("<span>Foo </span>bar").unwrap().text, "Foo bar");
assert_eq!(dehtml("<span>Foo</span>\nbar").unwrap().text, "Foo bar");
assert_eq!(dehtml("\n<span>Foo</span> bar").unwrap().text, "Foo bar");
assert_eq!(dehtml("<span>Foo</span>\n\nbar").unwrap().text, "Foo bar");
assert_eq!(dehtml("Foo\n<span>bar</span>").unwrap().text, "Foo bar");
assert_eq!(dehtml("Foo<span>\nbar</span>").unwrap().text, "Foo bar");
assert_eq!(plain, "line1\n\r\r\rline2\nline3");
}
#[test]
fn test_dehtml_parse_p() {
let html = "<p>Foo</p><p>Bar</p>";
let plain = dehtml(html).unwrap().text;
let plain = dehtml(html).unwrap();
assert_eq!(plain, "Foo\n\nBar");
let html = "<p>Foo<p>Bar";
let plain = dehtml(html).unwrap().text;
let plain = dehtml(html).unwrap();
assert_eq!(plain, "Foo\n\nBar");
let html = "<p>Foo</p><p>Bar<p>Baz";
let plain = dehtml(html).unwrap().text;
let plain = dehtml(html).unwrap();
assert_eq!(plain, "Foo\n\nBar\n\nBaz");
}
#[test]
fn test_dehtml_parse_href() {
let html = "<a href=url>text</a";
let plain = dehtml(html).unwrap().text;
let plain = dehtml(html).unwrap();
assert_eq!(plain, "[text](url)");
}
@@ -461,7 +381,7 @@ mod tests {
#[test]
fn test_dehtml_bold_text() {
let html = "<!DOCTYPE name [<!DOCTYPE ...>]><!-- comment -->text <b><?php echo ... ?>bold</b><![CDATA[<>]]>";
let plain = dehtml(html).unwrap().text;
let plain = dehtml(html).unwrap();
assert_eq!(plain, "text *bold*<>");
}
@@ -471,7 +391,7 @@ mod tests {
let html =
"&lt;&gt;&quot;&apos;&amp; &auml;&Auml;&ouml;&Ouml;&uuml;&Uuml;&szlig; foo&AElig;&ccedil;&Ccedil; &diams;&lrm;&rlm;&zwnj;&noent;&zwj;";
let plain = dehtml(html).unwrap().text;
let plain = dehtml(html).unwrap();
assert_eq!(
plain,
@@ -495,38 +415,32 @@ mod tests {
</html>
"##;
let txt = dehtml(input).unwrap();
assert_eq!(txt.text.trim(), "lots of text");
assert_eq!(txt.trim(), "lots of text");
}
#[test]
fn test_pre_tag() {
let input = "<html><pre>\ntwo\nlines\n</pre></html>";
let txt = dehtml(input).unwrap();
assert_eq!(txt.text.trim(), "two\nlines");
assert_eq!(txt.trim(), "two\nlines");
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_quote_div() {
let input = include_str!("../test-data/message/gmx-quote-body.eml");
let dehtml = dehtml(input).unwrap();
println!("{dehtml}");
let SimplifiedText {
text,
is_forwarded,
is_cut,
top_quote,
footer,
} = dehtml;
} = simplify(dehtml, false);
assert_eq!(text, "Test");
assert_eq!(is_forwarded, false);
assert_eq!(is_cut, false);
assert_eq!(top_quote.as_deref(), Some("test"));
assert_eq!(footer, None);
}
#[test]
fn test_spaces() {
let input = include_str!("../test-data/spaces.html");
let txt = dehtml(input).unwrap();
assert_eq!(txt.text, "Welcome back to Strolling!\n\nHey there,\n\nWelcome back! Use this link to securely sign in to your Strolling account:\n\nSign in to Strolling\n\nFor your security, the link will expire in 24 hours time.\n\nSee you soon!\n\nYou can also copy\n\nhttps://strolling.rosano.ca/members/?token=XXX\n\nIf you did not make this request, you can safely ignore this email.\n\nThis message was sent from [strolling.rosano.ca](https://strolling.rosano.ca/) to [alice@example.org](mailto:alice@example.org)");
}
}

View File

@@ -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`.
pub(crate) const MIN_DOWNLOAD_LIMIT: u32 = 32768;
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"),
@@ -59,9 +59,6 @@ pub enum DownloadState {
/// Failed to fully download the message.
Failure = 20,
/// Undecipherable message.
Undecipherable = 30,
/// Full download of the message is in progress.
InProgress = 1000,
}
@@ -83,9 +80,7 @@ impl MsgId {
pub async fn download_full(self, context: &Context) -> Result<()> {
let msg = Message::load_from_db(context, self).await?;
match msg.download_state() {
DownloadState::Done | DownloadState::Undecipherable => {
return Err(anyhow!("Nothing to download."))
}
DownloadState::Done => return Err(anyhow!("Nothing to download.")),
DownloadState::InProgress => return Err(anyhow!("Download already in progress.")),
DownloadState::Available | DownloadState::Failure => {
self.update_download_state(context, DownloadState::InProgress)
@@ -313,7 +308,7 @@ mod tests {
let chat = t.create_chat_with_contact("Bob", "bob@example.org").await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("Hi Bob".to_owned());
msg.set_text(Some("Hi Bob".to_owned()));
let msg_id = send_msg(&t, chat.id, &mut msg).await?;
let msg = Message::load_from_db(&t, msg_id).await?;
assert_eq!(msg.download_state(), DownloadState::Done);
@@ -360,6 +355,7 @@ mod tests {
assert_eq!(msg.get_subject(), "foo");
assert!(msg
.get_text()
.unwrap()
.contains(&stock_str::partial_download_msg_body(&t, 100000).await));
receive_imf_inner(
@@ -374,7 +370,7 @@ mod tests {
let msg = t.get_last_msg().await;
assert_eq!(msg.download_state(), DownloadState::Done);
assert_eq!(msg.get_subject(), "foo");
assert_eq!(msg.get_text(), "100k text...");
assert_eq!(msg.get_text(), Some("100k text...".to_string()));
Ok(())
}

View File

@@ -219,7 +219,7 @@ impl ChatId {
if self.is_promoted(context).await? {
let mut msg = Message::new(Viewtype::Text);
msg.text = stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await;
msg.text = Some(stock_ephemeral_timer_changed(context, timer, ContactId::SELF).await);
msg.param.set_cmd(SystemMessage::EphemeralTimerChanged);
if let Err(err) = send_msg(context, self, &mut msg).await {
error!(
@@ -1062,14 +1062,14 @@ mod tests {
delete_expired_messages(t, not_deleted_at).await?;
let loaded = Message::load_from_db(t, msg_id).await?;
assert!(!loaded.text.is_empty());
assert_eq!(loaded.text.unwrap(), "Message text");
assert_eq!(loaded.chat_id, chat.id);
assert!(next_expiration < deleted_at);
delete_expired_messages(t, deleted_at).await?;
t.evtracker
.get_matching(|evt| {
if let EventType::MsgDeleted {
if let EventType::MsgsChanged {
msg_id: event_msg_id,
..
} = evt
@@ -1082,6 +1082,7 @@ mod tests {
.await;
let loaded = Message::load_from_db(t, msg_id).await?;
assert_eq!(loaded.text.unwrap(), "");
assert_eq!(loaded.chat_id, DC_CHAT_ID_TRASH);
// Check that the msg was deleted locally.
@@ -1104,7 +1105,7 @@ mod tests {
if let Ok(msg) = Message::load_from_db(t, msg_id).await {
assert_eq!(msg.from_id, ContactId::UNDEFINED);
assert_eq!(msg.to_id, ContactId::UNDEFINED);
assert_eq!(msg.text, "");
assert!(msg.text.is_none_or_empty(), "{:?}", msg.text);
let rawtxt: Option<String> = t
.sql
.query_get_value("SELECT txt_raw FROM msgs WHERE id=?;", (msg_id,))
@@ -1299,32 +1300,4 @@ mod tests {
Ok(())
}
// Tests that if we are offline for a time longer than the ephemeral timer duration, the message
// is deleted from the chat but is still in the "smtp" table, i.e. will be sent upon a
// successful reconnection.
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_ephemeral_msg_offline() -> Result<()> {
let alice = TestContext::new_alice().await;
let chat = alice
.create_chat_with_contact("Bob", "bob@example.org")
.await;
let duration = 60;
chat.id
.set_ephemeral_timer(&alice, Timer::Enabled { duration })
.await?;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hi".to_string());
assert!(chat::send_msg_sync(&alice, chat.id, &mut msg)
.await
.is_err());
let stmt = "SELECT COUNT(*) FROM smtp WHERE msg_id=?";
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
let now = time();
check_msg_will_be_deleted(&alice, msg.id, &chat, now, now + i64::from(duration) + 1)
.await?;
assert!(alice.sql.exists(stmt, (msg.id,)).await?);
Ok(())
}
}

View File

@@ -291,7 +291,7 @@ mod tests {
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r#"<!DOCTYPE html>
r##"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
@@ -299,7 +299,7 @@ mod tests {
This message does not have Content-Type nor Subject.<br/>
<br/>
</body></html>
"#
"##
);
}
@@ -310,7 +310,7 @@ This message does not have Content-Type nor Subject.<br/>
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r#"<!DOCTYPE html>
r##"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
@@ -318,7 +318,7 @@ This message does not have Content-Type nor Subject.<br/>
message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
<br/>
</body></html>
"#
"##
);
}
@@ -330,7 +330,7 @@ message with a non-UTF-8 encoding: äöüßÄÖÜ<br/>
assert!(parser.plain.unwrap().flowed);
assert_eq!(
parser.html,
r#"<!DOCTYPE html>
r##"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
@@ -341,7 +341,7 @@ This line does not end with a space<br/>
and will be wrapped as usual.<br/>
<br/>
</body></html>
"#
"##
);
}
@@ -352,7 +352,7 @@ and will be wrapped as usual.<br/>
let parser = HtmlMsgParser::from_bytes(&t.ctx, raw).await.unwrap();
assert_eq!(
parser.html,
r#"<!DOCTYPE html>
r##"<!DOCTYPE html>
<html><head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta name="color-scheme" content="light dark" />
@@ -363,7 +363,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
<br/>
<br/>
</body></html>
"#
"##
);
}
@@ -456,7 +456,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_ne!(msg.get_from_id(), ContactId::SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::No);
assert!(!msg.is_forwarded());
assert!(msg.get_text().contains("this is plain"));
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
@@ -470,7 +470,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_eq!(msg.get_from_id(), ContactId::SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.is_forwarded());
assert!(msg.get_text().contains("this is plain"));
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
@@ -483,7 +483,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_ne!(msg.get_from_id(), ContactId::SELF);
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.is_forwarded());
assert!(msg.get_text().contains("this is plain"));
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
@@ -526,7 +526,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
assert_eq!(msg.is_dc_message, MessengerMessage::Yes);
assert!(msg.get_showpadlock());
assert!(msg.is_forwarded());
assert!(msg.get_text().contains("this is plain"));
assert!(msg.get_text().unwrap().contains("this is plain"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
assert!(html.contains("this is <b>html</b>"));
@@ -540,14 +540,14 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
// alice sends a message with html-part to bob
let chat_id = alice.create_chat(&bob).await.id;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("plain text".to_string());
msg.set_text(Some("plain text".to_string()));
msg.set_html(Some("<b>html</b> text".to_string()));
assert!(msg.mime_modified);
chat::send_msg(&alice, chat_id, &mut msg).await.unwrap();
// check the message is written correctly to alice's db
let msg = alice.get_last_msg_in(chat_id).await;
assert_eq!(msg.get_text(), "plain text");
assert_eq!(msg.get_text(), Some("plain text".to_string()));
assert!(!msg.is_forwarded());
assert!(msg.mime_modified);
let html = msg.get_id().get_html(&alice).await.unwrap().unwrap();
@@ -557,7 +557,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
let chat_id = bob.create_chat(&alice).await.id;
let msg = bob.recv_msg(&alice.pop_sent_msg().await).await;
assert_eq!(msg.chat_id, chat_id);
assert_eq!(msg.get_text(), "plain text");
assert_eq!(msg.get_text(), Some("plain text".to_string()));
assert!(!msg.is_forwarded());
assert!(msg.mime_modified);
let html = msg.get_id().get_html(&bob).await.unwrap().unwrap();
@@ -575,7 +575,7 @@ test some special html-characters as &lt; &gt; and &amp; but also &quot; and &#x
.await?;
let msg = t.get_last_msg().await;
assert_eq!(msg.viewtype, Viewtype::Text);
assert!(msg.text.contains("foo bar ä ö ü ß"));
assert!(msg.text.as_ref().unwrap().contains("foo bar ä ö ü ß"));
assert!(msg.has_html());
let html = msg.get_id().get_html(&t).await?.unwrap();
println!("{html}");

View File

@@ -412,7 +412,7 @@ impl Imap {
drop(lock);
let mut msg = Message::new(Viewtype::Text);
msg.text = message.clone();
msg.text = Some(message.clone());
if let Err(e) =
chat::add_device_msg_with_importance(context, None, Some(&mut msg), true)
.await
@@ -611,7 +611,8 @@ impl Imap {
if uid_next < old_uid_next {
warn!(
context,
"The server illegally decreased the uid_next of folder {folder:?} from {old_uid_next} to {uid_next} without changing validity ({new_uid_validity}), resyncing UIDs...",
"The server illegally decreased the uid_next of folder {} from {} to {} without changing validity ({}), resyncing UIDs...",
folder, old_uid_next, uid_next, new_uid_validity,
);
set_uid_next(context, folder, uid_next).await?;
job::schedule_resync(context).await?;
@@ -627,7 +628,7 @@ impl Imap {
set_modseq(context, folder, 0).await?;
if mailbox.exists == 0 {
info!(context, "Folder {folder:?} is empty.");
info!(context, "Folder \"{}\" is empty.", folder);
// set uid_next=1 for empty folders.
// If we do not do this here, we'll miss the first message
@@ -645,7 +646,7 @@ impl Imap {
None => {
warn!(
context,
"IMAP folder {folder:?} has no uid_next, fall back to fetching."
"IMAP folder has no uid_next, fall back to fetching"
);
// note that we use fetch by sequence number
// and thus we only need to get exactly the
@@ -684,7 +685,7 @@ impl Imap {
}
info!(
context,
"uid/validity change folder {}: new {}/{} previous {}/{}.",
"uid/validity change folder {}: new {}/{} previous {}/{}",
folder,
new_uid_next,
new_uid_validity,
@@ -705,17 +706,17 @@ impl Imap {
fetch_existing_msgs: bool,
) -> Result<bool> {
if should_ignore_folder(context, folder, folder_meaning).await? {
info!(context, "Not fetching from {folder:?}.");
info!(context, "Not fetching from {}", folder);
return Ok(false);
}
let new_emails = self
.select_with_uidvalidity(context, folder)
.await
.with_context(|| format!("Failed to select folder {folder:?}"))?;
.with_context(|| format!("failed to select folder {folder}"))?;
if !new_emails && !fetch_existing_msgs {
info!(context, "No new emails in folder {folder:?}.");
info!(context, "No new emails in folder {}", folder);
return Ok(false);
}
@@ -741,56 +742,14 @@ impl Imap {
let headers = match get_fetch_headers(fetch_response) {
Ok(headers) => headers,
Err(err) => {
warn!(context, "Failed to parse FETCH headers: {err:#}.");
warn!(context, "Failed to parse FETCH headers: {}", err);
continue;
}
};
let message_id = prefetch_get_message_id(&headers);
// Determine the target folder where the message should be moved to.
//
// If we have seen the message on the IMAP server before, do not move it.
// This is required to avoid infinite MOVE loop on IMAP servers
// that alias `DeltaChat` folder to other names.
// For example, some Dovecot servers alias `DeltaChat` folder to `INBOX.DeltaChat`.
// In this case Delta Chat configured with `DeltaChat` as the destination folder
// would detect messages in the `INBOX.DeltaChat` folder
// and try to move them to the `DeltaChat` folder.
// Such move to the same folder results in the messages
// getting a new UID, so the messages will be detected as new
// in the `INBOX.DeltaChat` folder again.
let target = if let Some(message_id) = &message_id {
if context
.sql
.exists(
"SELECT COUNT (*) FROM imap WHERE rfc724_mid=?",
(message_id,),
)
.await?
{
info!(
context,
"Not moving the message {} that we have seen before.", &message_id
);
folder.to_string()
} else {
target_folder(context, folder, folder_meaning, &headers).await?
}
} else {
// Do not move the messages without Message-ID.
// We cannot reliably determine if we have seen them before,
// so it is safer not to move them.
warn!(
context,
"Not moving the message that does not have a Message-ID."
);
folder.to_string()
};
// Generate a fake Message-ID to identify the message in the database
// if the message has no real Message-ID.
let message_id = message_id.unwrap_or_else(create_message_id);
// Get the Message-ID or generate a fake one to identify the message in the database.
let message_id = prefetch_get_or_create_message_id(&headers);
let target = target_folder(context, folder, folder_meaning, &headers).await?;
context
.sql
@@ -932,7 +891,7 @@ impl Imap {
if let Some(folder) = context.get_config(config).await? {
info!(
context,
"Fetching existing messages from folder {folder:?}."
"Fetching existing messages from folder \"{}\"", folder
);
self.fetch_new_messages(context, &folder, meaning, true)
.await
@@ -2101,15 +2060,16 @@ fn get_fetch_headers(prefetch_msg: &Fetch) -> Result<Vec<mailparse::MailHeader>>
}
}
pub(crate) fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
fn prefetch_get_message_id(headers: &[mailparse::MailHeader]) -> Option<String> {
headers
.get_header_value(HeaderDef::XMicrosoftOriginalMessageId)
.or_else(|| headers.get_header_value(HeaderDef::MessageId))
.and_then(|msgid| mimeparser::parse_message_id(&msgid).ok())
}
pub(crate) fn create_message_id() -> String {
format!("{}{}", GENERATED_PREFIX, create_id())
pub(crate) fn prefetch_get_or_create_message_id(headers: &[mailparse::MailHeader]) -> String {
prefetch_get_message_id(headers)
.unwrap_or_else(|| format!("{}{}", GENERATED_PREFIX, create_id()))
}
/// Returns chat by prefetched headers.

View File

@@ -7,9 +7,7 @@ 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);
@@ -23,10 +21,6 @@ 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");
}
@@ -169,14 +163,7 @@ impl Imap {
continue;
}
if let Some(session) = &self.session {
if session.can_idle()
&& !context
.get_config_bool(Config::DisableIdle)
.await
.context("Failed to get disable_idle config")
.log_err(context)
.unwrap_or_default()
{
if session.can_idle() {
// we only fake-idled because network was gone during IDLE, probably
break InterruptInfo::new(false);
}

View File

@@ -253,10 +253,12 @@ async fn maybe_add_bcc_self_device_msg(context: &Context) -> Result<()> {
if !context.sql.get_raw_config_bool("bcc_self").await? {
let mut msg = Message::new(Viewtype::Text);
// TODO: define this as a stockstring once the wording is settled.
msg.text = "It seems you are using multiple devices with Delta Chat. Great!\n\n\
msg.text = Some(
"It seems you are using multiple devices with Delta Chat. Great!\n\n\
If you also want to synchronize outgoing messages across all devices, \
go to \"Settings → Advanced\" and enable \"Send Copy to Self\"."
.to_string();
.to_string(),
);
chat::add_device_msg(context, Some("bcc-self-hint"), Some(&mut msg)).await?;
}
Ok(())
@@ -586,74 +588,63 @@ async fn export_backup_inner(
Ok(())
}
/// Imports secret key from a file.
async fn import_secret_key(context: &Context, path: &Path, set_default: bool) -> Result<()> {
let buf = read_file(context, &path).await?;
let armored = std::string::String::from_utf8_lossy(&buf);
set_self_key(context, &armored, set_default, false).await?;
Ok(())
}
/// Imports secret keys from the provided file or directory.
///
/// If provided path is a file, ASCII-armored secret key is read from the file
/// and set as the default key.
///
/// If provided path is a directory, all files with .asc extension
/// containing secret keys are imported and the last successfully
/// imported which does not contain "legacy" in its filename
/// is set as the default.
async fn import_self_keys(context: &Context, path: &Path) -> Result<()> {
let attr = tokio::fs::metadata(path).await?;
if attr.is_file() {
info!(
context,
"Importing secret key from {} as the default key.",
path.display()
);
let set_default = true;
import_secret_key(context, path, set_default).await?;
return Ok(());
}
/*******************************************************************************
* Classic key import
******************************************************************************/
async fn import_self_keys(context: &Context, dir: &Path) -> Result<()> {
/* hint: even if we switch to import Autocrypt Setup Files, we should leave the possibility to import
plain ASC keys, at least keys without a password, if we do not want to implement a password entry function.
Importing ASC keys is useful to use keys in Delta Chat used by any other non-Autocrypt-PGP implementation.
Maybe we should make the "default" key handlong also a little bit smarter
(currently, the last imported key is the standard key unless it contains the string "legacy" in its name) */
let mut set_default: bool;
let mut imported_cnt = 0;
let mut dir_handle = tokio::fs::read_dir(&path).await?;
let dir_name = dir.to_string_lossy();
let mut dir_handle = tokio::fs::read_dir(&dir).await?;
while let Ok(Some(entry)) = dir_handle.next_entry().await {
let entry_fn = entry.file_name();
let name_f = entry_fn.to_string_lossy();
let path_plus_name = path.join(&entry_fn);
if let Some(suffix) = get_filesuffix_lc(&name_f) {
if suffix != "asc" {
let path_plus_name = dir.join(&entry_fn);
match get_filesuffix_lc(&name_f) {
Some(suffix) => {
if suffix != "asc" {
continue;
}
set_default = if name_f.contains("legacy") {
info!(context, "found legacy key '{}'", path_plus_name.display());
false
} else {
true
}
}
None => {
continue;
}
} else {
continue;
};
let set_default = !name_f.contains("legacy");
}
info!(
context,
"Considering key file: {}.",
"considering key file: {}",
path_plus_name.display()
);
if let Err(err) = import_secret_key(context, &path_plus_name, set_default).await {
warn!(
context,
"Failed to import secret key from {}: {:#}.",
path_plus_name.display(),
err
);
continue;
match read_file(context, &path_plus_name).await {
Ok(buf) => {
let armored = std::string::String::from_utf8_lossy(&buf);
if let Err(err) = set_self_key(context, &armored, set_default, false).await {
info!(context, "set_self_key: {}", err);
continue;
}
}
Err(_) => continue,
}
imported_cnt += 1;
}
ensure!(
imported_cnt > 0,
"No private keys found in {}.",
path.display()
"No private keys found in \"{}\".",
dir_name
);
Ok(())
}
@@ -684,8 +675,7 @@ async fn export_self_keys(context: &Context, dir: &Path) -> Result<()> {
.await?;
for (id, public_key, private_key, is_default) in keys {
let id = Some(id).filter(|_| is_default == 0);
let id = Some(id).filter(|_| is_default != 0);
if let Ok(key) = public_key {
if let Err(err) = export_key_to_asc_file(context, dir, id, &key).await {
error!(context, "Failed to export public key: {:#}.", err);
@@ -876,35 +866,14 @@ mod tests {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_export_and_import_key() {
let export_dir = tempfile::tempdir().unwrap();
let context = TestContext::new_alice().await;
if let Err(err) = imex(
&context.ctx,
ImexMode::ExportSelfKeys,
export_dir.path(),
None,
)
.await
{
let blobdir = context.ctx.get_blobdir();
if let Err(err) = imex(&context.ctx, ImexMode::ExportSelfKeys, blobdir, None).await {
panic!("got error on export: {err:#}");
}
let context2 = TestContext::new_alice().await;
if let Err(err) = imex(
&context2.ctx,
ImexMode::ImportSelfKeys,
export_dir.path(),
None,
)
.await
{
panic!("got error on import: {err:#}");
}
let keyfile = export_dir.path().join("private-key-default.asc");
let context3 = TestContext::new_alice().await;
if let Err(err) = imex(&context3.ctx, ImexMode::ImportSelfKeys, &keyfile, None).await {
if let Err(err) = imex(&context2.ctx, ImexMode::ImportSelfKeys, blobdir, None).await {
panic!("got error on import: {err:#}");
}
}
@@ -1061,7 +1030,10 @@ mod tests {
// not synchronized yet.
let sent = alice2.send_text(msg.chat_id, "Test").await;
alice.recv_msg(&sent).await;
assert_ne!(alice.get_last_msg().await.get_text(), "Test");
assert_ne!(
alice.get_last_msg().await.get_text(),
Some("Test".to_string())
);
// Transfer the key.
continue_key_transfer(&alice2, msg.id, &setup_code).await?;
@@ -1069,7 +1041,10 @@ mod tests {
// Alice sends a message to self from the new device.
let sent = alice2.send_text(msg.chat_id, "Test").await;
alice.recv_msg(&sent).await;
assert_eq!(alice.get_last_msg().await.get_text(), "Test");
assert_eq!(
alice.get_last_msg().await.get_text(),
Some("Test".to_string())
);
Ok(())
}

View File

@@ -275,7 +275,7 @@ impl BackupProvider {
Ok(_) => {
context.emit_event(SendProgress::Completed.into());
let mut msg = Message::new(Viewtype::Text);
msg.text = backup_transfer_msg_body(context).await;
msg.text = Some(backup_transfer_msg_body(context).await);
add_device_msg(context, None, Some(&mut msg)).await?;
}
Err(err) => {
@@ -611,7 +611,7 @@ mod tests {
// Write a message in the self chat
let self_chat = ctx0.get_self_chat().await;
let mut msg = Message::new(Viewtype::Text);
msg.set_text("hi there".to_string());
msg.set_text(Some("hi there".to_string()));
send_msg(&ctx0, self_chat.id, &mut msg).await.unwrap();
// Send an attachment in the self chat
@@ -643,16 +643,14 @@ mod tests {
_ => panic!("wrong chat item"),
};
let msg = Message::load_from_db(&ctx1, *msgid).await.unwrap();
let text = msg.get_text();
let text = msg.get_text().unwrap();
assert_eq!(text, "hi there");
let msgid = match msgs.get(1).unwrap() {
ChatItem::Message { msg_id } => msg_id,
_ => panic!("wrong chat item"),
};
let msg = Message::load_from_db(&ctx1, *msgid).await.unwrap();
let path = msg.get_file(&ctx1).unwrap();
assert_eq!(path.with_file_name("hello.txt"), path);
let text = fs::read_to_string(&path).await.unwrap();
assert_eq!(text, "i am attachment");

View File

@@ -155,8 +155,8 @@ impl<'a> Connection<'a> {
pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_>, mut job: Job) {
info!(context, "Job {} started...", &job);
let try_res = match perform_job_action(context, &job, &mut connection, 0).await {
Status::RetryNow => perform_job_action(context, &job, &mut connection, 1).await,
let try_res = match perform_job_action(context, &mut job, &mut connection, 0).await {
Status::RetryNow => perform_job_action(context, &mut job, &mut connection, 1).await,
x => x,
};
@@ -205,7 +205,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
async fn perform_job_action(
context: &Context,
job: &Job,
job: &mut Job,
connection: &mut Connection<'_>,
tries: u32,
) -> Status {

View File

@@ -260,7 +260,7 @@ pub(crate) async fn load_keypair(
})
}
/// Use of a key pair for encryption or decryption.
/// Use of a [KeyPair] for encryption or decryption.
///
/// This is used by [store_self_keypair] to know what kind of key is
/// being saved.

View File

@@ -44,6 +44,10 @@ where
self.keys.push(key);
}
pub fn len(&self) -> usize {
self.keys.len()
}
pub fn is_empty(&self) -> bool {
self.keys.is_empty()
}

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