mirror of
https://github.com/chatmail/core.git
synced 2026-04-14 20:16:31 +03:00
Compare commits
70 Commits
fix-stock-
...
jsonrpc-ap
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7147d32601 | ||
|
|
343bb34589 | ||
|
|
361b7f5b69 | ||
|
|
15019ce02b | ||
|
|
7bb5dc4c3c | ||
|
|
543edac105 | ||
|
|
55db8d1fe0 | ||
|
|
1d92f06834 | ||
|
|
2ff05c9dff | ||
|
|
d6b6d96e21 | ||
|
|
8776767a44 | ||
|
|
9d83057a71 | ||
|
|
8ce11ac62f | ||
|
|
918ec85767 | ||
|
|
f3a9ab6d23 | ||
|
|
e4def6a44d | ||
|
|
4b5c194ef3 | ||
|
|
97e2d85b28 | ||
|
|
81bc7bd7bf | ||
|
|
f53c456e50 | ||
|
|
64fa5675a9 | ||
|
|
1f0bdfa704 | ||
|
|
5ac347b7ae | ||
|
|
40fa2d4120 | ||
|
|
aaf27e4434 | ||
|
|
2df10857ca | ||
|
|
7eae3a1072 | ||
|
|
7fc162543a | ||
|
|
e7da0672ae | ||
|
|
69d9d48ae4 | ||
|
|
802677222b | ||
|
|
d7b9febc33 | ||
|
|
1d347f7369 | ||
|
|
e3fa42fe88 | ||
|
|
2f00b098ac | ||
|
|
bdd4aa0f10 | ||
|
|
6fee4fd878 | ||
|
|
60d3a3cacf | ||
|
|
4bb1980f8d | ||
|
|
35b70b1d1b | ||
|
|
fd53b80c17 | ||
|
|
978e4aec82 | ||
|
|
329f498651 | ||
|
|
035e208e4f | ||
|
|
3404996fdd | ||
|
|
97e0e0137a | ||
|
|
659e48bd3f | ||
|
|
271d54e420 | ||
|
|
9984ee5eb2 | ||
|
|
136bec0273 | ||
|
|
c5ff7427be | ||
|
|
2319dfc3eb | ||
|
|
d8d26b9cae | ||
|
|
d93622bc84 | ||
|
|
2fde4962a1 | ||
|
|
29a5d73f94 | ||
|
|
b51814aaaa | ||
|
|
63e7179191 | ||
|
|
9915803252 | ||
|
|
e12aeb7bd8 | ||
|
|
346fab7f26 | ||
|
|
6f6e6f24c9 | ||
|
|
079cd67da8 | ||
|
|
bfd97fdb05 | ||
|
|
688326f21c | ||
|
|
cc20d25b8d | ||
|
|
99d50615c3 | ||
|
|
a006825376 | ||
|
|
c90fd1c9ce | ||
|
|
bf5d09e74a |
@@ -1,11 +0,0 @@
|
||||
[env]
|
||||
# In unoptimised builds tokio tends to use a lot of stack space when
|
||||
# creating some complicated futures, tokio has an open issue for this:
|
||||
# https://github.com/tokio-rs/tokio/issues/2055. Some of our tests
|
||||
# manage to not fit in the default 2MiB stack anymore due to this, so
|
||||
# while the issue is not resolved we want to work around this.
|
||||
# Because compiling optimised builds takes a very long time we prefer
|
||||
# to avoid that. Setting this environment variable ensures that when
|
||||
# invoking `cargo test` threads are allowed to have a large enough
|
||||
# stack size without needing to use an optimised build.
|
||||
RUST_MIN_STACK = "8388608"
|
||||
2
.github/mergeable.yml
vendored
2
.github/mergeable.yml
vendored
@@ -16,7 +16,7 @@ mergeable:
|
||||
required: ['CHANGELOG.md']
|
||||
- do: dependent
|
||||
changed:
|
||||
file: 'deltachat-ffi/src/**'
|
||||
file: 'deltachat-ffi/**'
|
||||
required: ['CHANGELOG.md']
|
||||
fail:
|
||||
- do: checks
|
||||
|
||||
11
.github/workflows/ci.yml
vendored
11
.github/workflows/ci.yml
vendored
@@ -45,7 +45,7 @@ jobs:
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --workspace --tests --examples --benches --features repl -- -D warnings
|
||||
args: --workspace --tests --examples --benches
|
||||
|
||||
docs:
|
||||
name: Rust doc comments
|
||||
@@ -83,13 +83,13 @@ jobs:
|
||||
rust: 1.61.0
|
||||
python: false # Python bindings compilation on Windows is not supported.
|
||||
|
||||
# Minimum Supported Rust Version = 1.57.0
|
||||
# Minimum Supported Rust Version = 1.56.1
|
||||
#
|
||||
# Minimum Supported Python Version = 3.7
|
||||
# This is the minimum version for which manylinux Python wheels are
|
||||
# built.
|
||||
- os: ubuntu-latest
|
||||
rust: 1.57.0
|
||||
rust: 1.56.1
|
||||
python: 3.7
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
@@ -116,11 +116,6 @@ jobs:
|
||||
command: test
|
||||
args: --all
|
||||
|
||||
- name: test cargo vendor
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: vendor
|
||||
|
||||
- name: install python
|
||||
if: ${{ matrix.python }}
|
||||
uses: actions/setup-python@v4
|
||||
|
||||
83
.github/workflows/jsonrpc-client-npm-package.yml
vendored
83
.github/workflows/jsonrpc-client-npm-package.yml
vendored
@@ -1,83 +0,0 @@
|
||||
name: 'jsonrpc js client build'
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
- '!py-*'
|
||||
|
||||
|
||||
jobs:
|
||||
pack-module:
|
||||
name: 'Package @deltachat/jsonrpc-client and upload to download.delta.chat'
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- name: install tree
|
||||
run: sudo apt install tree
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: get tag
|
||||
id: tag
|
||||
uses: dawidd6/action-get-tag@v1
|
||||
continue-on-error: true
|
||||
- name: Get Pullrequest ID
|
||||
id: prepare
|
||||
run: |
|
||||
tag=${{ steps.tag.outputs.tag }}
|
||||
if [ -z "$tag" ]; then
|
||||
node -e "console.log('DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-' + '${{ github.ref }}'.split('/')[2] + '.tar.gz')" >> $GITHUB_ENV
|
||||
else
|
||||
echo "DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-${{ steps.tag.outputs.tag }}.tar.gz" >> $GITHUB_ENV
|
||||
echo "No preview will be uploaded this time, but the $tag release"
|
||||
fi
|
||||
- name: System info
|
||||
run: |
|
||||
npm --version
|
||||
node --version
|
||||
echo $DELTACHAT_JSONRPC_TAR_GZ
|
||||
- name: install dependencies without running scripts
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm install --ignore-scripts
|
||||
- name: package
|
||||
shell: bash
|
||||
run: |
|
||||
cd deltachat-jsonrpc/typescript
|
||||
npm run build:tsc
|
||||
npm pack .
|
||||
ls -lah
|
||||
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: deltachat-jsonrpc-client.tgz
|
||||
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
|
||||
# Upload to download.delta.chat/node/preview/
|
||||
- name: Upload deltachat-jsonrpc-client preview to download.delta.chat/node/preview/
|
||||
if: ${{ ! steps.tag.outputs.tag }}
|
||||
id: upload-preview
|
||||
shell: bash
|
||||
run: |
|
||||
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
|
||||
chmod 600 __TEMP_INPUT_KEY_FILE
|
||||
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
|
||||
continue-on-error: true
|
||||
- name: "Post links to details"
|
||||
if: steps.upload-preview.outcome == 'success'
|
||||
run: node ./node/scripts/postLinksToDetails.js
|
||||
env:
|
||||
URL: preview/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
MSG_CONTEXT: Download the deltachat-jsonrpc-client.tgz
|
||||
# Upload to download.delta.chat/node/
|
||||
- name: Upload deltachat-jsonrpc-client build to download.delta.chat/node/
|
||||
if: ${{ steps.tag.outputs.tag }}
|
||||
id: upload
|
||||
shell: bash
|
||||
run: |
|
||||
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
|
||||
chmod 600 __TEMP_INPUT_KEY_FILE
|
||||
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/"
|
||||
1
.github/workflows/jsonrpc.yml
vendored
1
.github/workflows/jsonrpc.yml
vendored
@@ -8,7 +8,6 @@ on:
|
||||
|
||||
env:
|
||||
CARGO_TERM_COLOR: always
|
||||
RUST_MIN_STACK: "8388608"
|
||||
|
||||
jobs:
|
||||
build_and_test:
|
||||
|
||||
2
.github/workflows/node-tests.yml
vendored
2
.github/workflows/node-tests.yml
vendored
@@ -52,7 +52,6 @@ jobs:
|
||||
npm install --verbose
|
||||
|
||||
- name: Test
|
||||
timeout-minutes: 10
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
cd node
|
||||
@@ -60,7 +59,6 @@ jobs:
|
||||
env:
|
||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||
- name: Run tests on Windows, except lint
|
||||
timeout-minutes: 10
|
||||
if: runner.os == 'Windows'
|
||||
run: |
|
||||
cd node
|
||||
|
||||
309
CHANGELOG.md
309
CHANGELOG.md
@@ -2,329 +2,28 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Changes
|
||||
|
||||
### API-Changes
|
||||
|
||||
### Fixes
|
||||
- fix detection of "All mail", "Trash", "Junk" etc folders. #3760
|
||||
- fix info messages shown for non-dc MUA #3754
|
||||
|
||||
|
||||
## 1.101.0
|
||||
|
||||
### Changes
|
||||
- add `configured_inbox_folder` to account info #3748
|
||||
- `dc_delete_contact()` hides contacts if referenced #3751
|
||||
- add IMAP UIDs to message info #3755
|
||||
|
||||
### Fixes
|
||||
- improve IMAP logging, in particular fix incorrect "IMAP IDLE protocol
|
||||
timed out" message on network error during IDLE #3749
|
||||
- pop Recently Seen Loop event out of the queue when it is in the past
|
||||
to avoid busy looping #3753
|
||||
- fix build failures by going back to standard `async_zip` #3747
|
||||
|
||||
|
||||
## 1.100.0
|
||||
|
||||
### API-Changes
|
||||
- jsonrpc: add `miscSaveSticker` method
|
||||
|
||||
### Changes
|
||||
- add JSON-RPC stdio server `deltachat-rpc-server` and use it for JSON-RPC tests #3695
|
||||
- update rPGP from 0.8 to 0.9 #3737
|
||||
- jsonrpc: typescript client: use npm released deltachat fork of the tiny emitter package #3741
|
||||
- jsonrpc: show sticker image in quote #3744
|
||||
|
||||
|
||||
|
||||
## 1.99.0
|
||||
|
||||
### API-Changes
|
||||
- breaking jsonrpc: changed function naming
|
||||
- `autocryptInitiateKeyTransfer` -> `initiateAutocryptKeyTransfer`
|
||||
- `autocryptContinueKeyTransfer` -> `continueAutocryptKeyTransfer`
|
||||
- `chatlistGetFullChatById` -> `getFullChatById`
|
||||
- `messageGetMessage` -> `getMessage`
|
||||
- `messageGetMessages` -> `getMessages`
|
||||
- `messageGetNotificationInfo` -> `getMessageNotificationInfo`
|
||||
- `contactsGetContact` -> `getContact`
|
||||
- `contactsCreateContact` -> `createContact`
|
||||
- `contactsCreateChatByContactId` -> `createChatByContactId`
|
||||
- `contactsBlock` -> `blockContact`
|
||||
- `contactsUnblock` -> `unblockContact`
|
||||
- `contactsGetBlocked` -> `getBlockedContacts`
|
||||
- `contactsGetContactIds` -> `getContactIds`
|
||||
- `contactsGetContacts` -> `getContacts`
|
||||
- `contactsGetContactsByIds` -> `getContactsByIds`
|
||||
- `chatGetMedia` -> `getChatMedia`
|
||||
- `chatGetNeighboringMedia` -> `getNeighboringChatMedia`
|
||||
- `webxdcSendStatusUpdate` -> `sendWebxdcStatusUpdate`
|
||||
- `webxdcGetStatusUpdates` -> `getWebxdcStatusUpdates`
|
||||
- `messageGetWebxdcInfo` -> `getWebxdcInfo`
|
||||
- jsonrpc: changed method signature
|
||||
- `miscSendTextMessage(accountId, text, chatId)` -> `miscSendTextMessage(accountId, chatId, text)`
|
||||
- jsonrpc: add `SystemMessageType` to `Message`
|
||||
- cffi: add missing `DC_INFO_` constants
|
||||
- Add DC_EVENT_INCOMING_MSG_BUNCH event #3643
|
||||
- Python bindings: Make get_matching() only match the
|
||||
whole event name, e.g. events.get_matching("DC_EVENT_INCOMING_MSG")
|
||||
won't match DC_EVENT_INCOMING_MSG_BUNCH anymore #3643
|
||||
|
||||
|
||||
- Rust: Introduce a ContextBuilder #3698
|
||||
|
||||
### Changes
|
||||
- allow sender timestamp to be in the future, but not too much
|
||||
- Disable the new "Authentication-Results/DKIM checking" security feature
|
||||
until we have tested it a bit #3728
|
||||
- refactorings #3706
|
||||
|
||||
### Fixes
|
||||
- `dc_search_msgs()` returns unaccepted requests #3694
|
||||
- emit "contacts changed" event when the contact is no longer "seen recently" #3703
|
||||
- do not allow peerstate reset if DKIM check failed #3731
|
||||
|
||||
|
||||
## 1.98.0
|
||||
|
||||
### API-Changes
|
||||
- jsonrpc: typescript client: export constants under `C` enum, similar to how its exported from `deltachat-node` #3681
|
||||
- added reactions support #3644
|
||||
- jsonrpc: reactions: added reactions to `Message` type and the `sendReaction()` method #3686
|
||||
|
||||
### Changes
|
||||
- simplify `UPSERT` queries #3676
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
## 1.97.0
|
||||
|
||||
### API-Changes
|
||||
- jsonrpc: add function: #3641, #3645, #3653
|
||||
- `getChatContacts()`
|
||||
- `createGroupChat()`
|
||||
- `createBroadcastList()`
|
||||
- `setChatName()`
|
||||
- `setChatProfileImage()`
|
||||
- `downloadFullMessage()`
|
||||
- `lookupContactIdByAddr()`
|
||||
- `sendVideochatInvitation()`
|
||||
- `searchMessages()`
|
||||
- `messageIdsToSearchResults()`
|
||||
- `setChatVisibility()`
|
||||
- `getChatEphemeralTimer()`
|
||||
- `setChatEphemeralTimer()`
|
||||
- `getLocations()`
|
||||
- `getAccountFileSize()`
|
||||
- `estimateAutoDeletionCount()`
|
||||
- `setStockStrings()`
|
||||
- `exportSelfKeys()`
|
||||
- `importSelfKeys()`
|
||||
- `sendSticker()`
|
||||
- `changeContactName()`
|
||||
- `deleteContact()`
|
||||
- `joinSecurejoin()`
|
||||
- `stopIoForAllAccounts()`
|
||||
- `startIoForAllAccounts()`
|
||||
- `startIo()`
|
||||
- `stopIo()`
|
||||
- `exportBackup()`
|
||||
- `importBackup()`
|
||||
- `getMessageHtml()` #3671
|
||||
- `miscGetStickerFolder` and `miscGetStickers` #3672
|
||||
- breaking: jsonrpc: remove function `messageListGetMessageIds()`, it is replaced by `getMessageIds()` and `getMessageListItems()` the latter returns a new `MessageListItem` type, which is the now prefered way of using the message list.
|
||||
- jsonrpc: add type: #3641, #3645
|
||||
- `MessageSearchResult`
|
||||
- `Location`
|
||||
- jsonrpc: add `viewType` to quoted message(`MessageQuote` type) in `Message` object type #3651
|
||||
|
||||
|
||||
### Changes
|
||||
- Look at Authentication-Results. Don't accept Autocrypt key changes
|
||||
if they come with negative authentiation results while this contact
|
||||
sent emails with positive authentication results in the past. #3583
|
||||
- jsonrpc in cffi also sends events now #3662
|
||||
- jsonrpc: new format for events and better typescript autocompletion
|
||||
- Join all "[migration] vXX" log messages into one
|
||||
|
||||
### Fixes
|
||||
- share stock string translations across accounts created by the same account manager #3640
|
||||
- suppress welcome device messages after account import #3642
|
||||
- fix unix timestamp used for daymarker #3660
|
||||
|
||||
## 1.96.0
|
||||
|
||||
### Changes
|
||||
- jsonrpc js client:
|
||||
- Change package name from `deltachat-jsonrpc-client` to `@deltachat/jsonrpc-client`
|
||||
- remove relative file dependency to it from `deltachat-node` (because it did not work anyway and broke the nix build of desktop)
|
||||
- ci: add github ci action to upload it to our download server automaticaly on realease
|
||||
|
||||
## 1.95.0
|
||||
|
||||
### API-Changes
|
||||
- jsonrpc: add `mailingListAddress` property to `FullChat` #3607
|
||||
- jsonrpc: add `MessageNotificationInfo` & `messageGetNotificationInfo()` #3614
|
||||
- jsonrpc: add `chat_get_neighboring_media` function #3610
|
||||
|
||||
### Changes
|
||||
- added `dclogin:` scheme to allow configuration from a qr code
|
||||
(data inside qrcode, contrary to `dcaccount:` which points to an API to create an account) #3541
|
||||
- truncate incoming messages by lines instead of just length #3480
|
||||
- emit separate `DC_EVENT_MSGS_CHANGED` for each expired message,
|
||||
and `DC_EVENT_WEBXDC_INSTANCE_DELETED` when a message contains a webxdc #3605
|
||||
- enable `bcc_self` by default #3612
|
||||
|
||||
|
||||
## 1.94.0
|
||||
|
||||
### API-Changes
|
||||
- breaking change: replace `dc_accounts_event_emitter_t` with `dc_event_emitter_t` #3422
|
||||
|
||||
Type `dc_accounts_event_emitter_t` is removed.
|
||||
`dc_accounts_get_event_emitter()` returns `dc_event_emitter_t` now, so
|
||||
`dc_get_next_event()` should be used instead of `dc_accounts_get_next_event`
|
||||
and `dc_event_emitter_unref()` should be used instead of
|
||||
`dc_accounts_event_emitter_unref`.
|
||||
- add `dc_contact_was_seen_recently()` #3560
|
||||
- Fix `get_connectivity_html` and `get_encrinfo` futures not being Send. See rust-lang/rust#101650 for more information
|
||||
- jsonrpc: add functions: #3586, #3587, #3590
|
||||
- `deleteChat()`
|
||||
- `getChatEncryptionInfo()`
|
||||
- `getChatSecurejoinQrCodeSvg()`
|
||||
- `leaveGroup()`
|
||||
- `removeContactFromChat()`
|
||||
- `addContactToChat()`
|
||||
- `deleteMessages()`
|
||||
- `getMessageInfo()`
|
||||
- `getBasicChatInfo()`
|
||||
- `marknoticedChat()`
|
||||
- `getFirstUnreadMessageOfChat()`
|
||||
- `markseenMsgs()`
|
||||
- `forwardMessages()`
|
||||
- `removeDraft()`
|
||||
- `getDraft()`
|
||||
- `miscSendMsg()`
|
||||
- `miscSetDraft()`
|
||||
- `maybeNetwork()`
|
||||
- `getConnectivity()`
|
||||
- `getContactEncryptionInfo()`
|
||||
- `getConnectivityHtml()`
|
||||
- jsonrpc: add `is_broadcast` property to `ChatListItemFetchResult` #3584
|
||||
- jsonrpc: add `was_seen_recently` property to `ChatListItemFetchResult`, `FullChat` and `Contact` #3584
|
||||
- jsonrpc: add `webxdc_info` property to `Message` #3588
|
||||
- python: move `get_dc_event_name()` from `deltachat` to `deltachat.events` #3564
|
||||
- jsonrpc: add `webxdc_info`, `parent_id` and `download_state` property to `Message` #3588, #3590
|
||||
- jsonrpc: add `BasicChat` object as a leaner alternative to `FullChat` #3590
|
||||
- jsonrpc: add `last_seen` property to `Contact` #3590
|
||||
- breaking! jsonrpc: replace `Message.quoted_text` and `Message.quoted_message_id` with `Message.quote` #3590
|
||||
- add separate stock strings for actions done by contacts to make them easier to translate #3518
|
||||
- `dc_initiate_key_transfer()` is non-blocking now. #3553
|
||||
UIs don't need to display a button to cancel sending Autocrypt Setup Message with
|
||||
`dc_stop_ongoing_process()` anymore.
|
||||
|
||||
### Changes
|
||||
- order contact lists by "last seen";
|
||||
this affects `dc_get_chat_contacts()`, `dc_get_contacts()` and `dc_get_blocked_contacts()` #3562
|
||||
- add `internet_access` flag to `dc_msg_get_webxdc_info()` #3516
|
||||
- `DC_EVENT_WEBXDC_INSTANCE_DELETED` is emitted when a message containing a webxdc gets deleted #3592
|
||||
|
||||
### Fixes
|
||||
- do not emit notifications for blocked chats #3557
|
||||
- Show attached .eml files correctly #3561
|
||||
- Auto accept contact requests if `Config::Bot` is set for a client #3567
|
||||
- Don't prepend the subject to chat messages in mailinglists
|
||||
- fix `set_core_version.py` script to also update version in `deltachat-jsonrpc/typescript/package.json` #3585
|
||||
- Reject webxcd-updates from contacts who are not group members #3568
|
||||
|
||||
|
||||
## 1.93.0
|
||||
|
||||
### API-Changes
|
||||
- added a JSON RPC API, accessible through a WebSocket server, the CFFI bindings and the Node.js bindings #3463 #3554 #3542
|
||||
- JSON RPC methods in CFFI #3463:
|
||||
- jsonrpc api over websocket server (basically a new api next to the cffi) #3463
|
||||
- jsonrpc methods in cffi #3463:
|
||||
- `dc_jsonrpc_instance_t* dc_jsonrpc_init(dc_accounts_t* account_manager);`
|
||||
- `void dc_jsonrpc_unref(dc_jsonrpc_instance_t* jsonrpc_instance);`
|
||||
- `void dc_jsonrpc_request(dc_jsonrpc_instance_t* jsonrpc_instance, char* request);`
|
||||
- `char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);`
|
||||
- node: JSON RPC methods #3463:
|
||||
- node: json rpc methods #3463:
|
||||
- `AccountManager.prototype.startJsonRpcHandler(callback: ((response: string) => void)): void`
|
||||
- `AccountManager.prototype.jsonRpcRequest(message: string): void`
|
||||
|
||||
### Changes
|
||||
- use [pathlib](https://docs.python.org/3/library/pathlib.html) in provider update script #3543
|
||||
- `dc_get_chat_media()` can return media globally #3528
|
||||
- node: add `getMailinglistAddr()` #3524
|
||||
- avoid duplicate encoded-words package and test `cargo vendor` in ci #3549
|
||||
- python: don't raise an error if addr changes #3530
|
||||
- improve coverage script #3530
|
||||
|
||||
### Fixes
|
||||
- improved error handling for account setup from qrcode #3474
|
||||
- python: enable certificate checks in cloned accounts #3443
|
||||
|
||||
|
||||
## 1.92.0
|
||||
|
||||
### API-Changes
|
||||
- add `dc_chat_get_mailinglist_addr()` #3520
|
||||
|
||||
|
||||
## 1.91.0
|
||||
|
||||
### Added
|
||||
- python bindings: extra method to get an account running
|
||||
|
||||
### Changes
|
||||
- refactorings #3437
|
||||
|
||||
### Fixes
|
||||
- mark "group image changed" as system message on receiver side #3517
|
||||
|
||||
|
||||
## 1.90.0
|
||||
|
||||
### Changes
|
||||
- handle drafts from mailto links in scanned QR #3492
|
||||
- do not overflow ratelimiter leaky bucket #3496
|
||||
- (AEAP) Add device message after you changed your address #3505
|
||||
- (AEAP) Revert #3491, instead only replace contacts in verified groups #3510
|
||||
- improve python bindings and tests #3502 #3503
|
||||
|
||||
### Fixes
|
||||
- don't squash text parts of NDN into attachments #3497
|
||||
- do not treat non-failed DSNs as NDNs #3506
|
||||
|
||||
|
||||
## 1.89.0
|
||||
|
||||
### Changes
|
||||
|
||||
- (AEAP) When one of your contacts changed their address, they are
|
||||
only replaced in the chat where you got a message from them
|
||||
for now #3491
|
||||
|
||||
### Fixes
|
||||
- replace musl libc name resolution errors with a better message #3485
|
||||
- handle updates for not yet downloaded webxdc instances #3487
|
||||
|
||||
|
||||
## 1.88.0
|
||||
|
||||
### Changes
|
||||
- Implemented "Automatic e-mail address Porting" (AEAP). You can
|
||||
configure a new address in DC now, and when receivers get messages
|
||||
they will automatically recognize your moving to a new address. #3385
|
||||
- added a JSON RPC API, accessible through a WebSocket server, the CFFI bindings and the Node.js bindings #3463
|
||||
- switch from `async-std` to `tokio` as the async runtime #3449
|
||||
- upgrade to `pgp@0.8.0` #3467
|
||||
- add IMAP ID extension support #3468
|
||||
- configure DeltaChat folder by selecting it, so it is configured even if not LISTed #3371
|
||||
- build PyPy wheels #6683
|
||||
- improve default error if NDN does not provide an error #3456
|
||||
- increase ratelimit from 3 to 6 messages per 60 seconds #3481
|
||||
|
||||
### Fixes
|
||||
- mailing list: remove square-brackets only for first name #3452
|
||||
|
||||
989
Cargo.lock
generated
989
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
36
Cargo.toml
36
Cargo.toml
@@ -1,19 +1,16 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.101.0"
|
||||
version = "1.87.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
rust-version = "1.57"
|
||||
rust-version = "1.56"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
panic = 'abort'
|
||||
opt-level = 1
|
||||
|
||||
[profile.test]
|
||||
opt-level = 0
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = 'abort'
|
||||
@@ -26,7 +23,7 @@ anyhow = "1"
|
||||
async-imap = { git = "https://github.com/async-email/async-imap", branch = "master", default-features = false, features = ["runtime-tokio"] }
|
||||
async-native-tls = { version = "0.4", default-features = false, features = ["runtime-tokio"] }
|
||||
async-smtp = { version = "0.5", default-features = false, features = ["smtp-transport", "socks5", "runtime-tokio"] }
|
||||
trust-dns-resolver = "0.22"
|
||||
trust-dns-resolver = "0.21"
|
||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||
backtrace = "0.3"
|
||||
@@ -35,11 +32,11 @@ bitflags = "1.3"
|
||||
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
|
||||
dirs = { version = "4", optional=true }
|
||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
||||
encoded-words = "0.2"
|
||||
escaper = "0.1"
|
||||
futures = "0.3"
|
||||
hex = "0.4.0"
|
||||
image = { version = "0.24.4", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
image = { version = "0.24.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
kamadak-exif = "0.5"
|
||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||
libc = "0.2"
|
||||
@@ -49,18 +46,18 @@ native-tls = "0.2"
|
||||
num_cpus = "1.13"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.16.0"
|
||||
percent-encoding = "2.2"
|
||||
pgp = { version = "0.9", default-features = false }
|
||||
once_cell = "1.12.0"
|
||||
percent-encoding = "2.0"
|
||||
pgp = { version = "0.8", default-features = false }
|
||||
pretty_env_logger = { version = "0.4", optional = true }
|
||||
quick-xml = "0.23"
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.20"
|
||||
rand = "0.8"
|
||||
regex = "1.6"
|
||||
regex = "1.5"
|
||||
rusqlite = { version = "0.27", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustyline = { version = "10", optional = true }
|
||||
rustyline = { version = "9", optional = true }
|
||||
sanitize-filename = "0.4"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -77,16 +74,16 @@ fast-socks5 = "0.8"
|
||||
humansize = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
tagger = "4.3.3"
|
||||
textwrap = "0.16.0"
|
||||
textwrap = "0.15.0"
|
||||
async-channel = "1.6.1"
|
||||
futures-lite = "1.12.0"
|
||||
tokio-stream = { version = "0.1.11", features = ["fs"] }
|
||||
reqwest = { version = "0.11.12", features = ["json"] }
|
||||
async_zip = { version = "0.0.9", default-features = false, features = ["deflate"] }
|
||||
tokio-stream = { version = "0.1.9", features = ["fs"] }
|
||||
reqwest = { version = "0.11.11", features = ["json"] }
|
||||
async_zip = { git = "https://github.com/dignifiedquire/rs-async-zip", branch = "main", default-features = false, features = ["deflate"] }
|
||||
|
||||
[dev-dependencies]
|
||||
ansi_term = "0.12.0"
|
||||
criterion = { version = "0.3.6", features = ["async_tokio"] }
|
||||
criterion = { version = "0.3.4", features = ["async_tokio"] }
|
||||
futures-lite = "1.12"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
@@ -98,8 +95,7 @@ tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"]
|
||||
members = [
|
||||
"deltachat-ffi",
|
||||
"deltachat_derive",
|
||||
"deltachat-jsonrpc",
|
||||
"deltachat-rpc-server"
|
||||
"deltachat-jsonrpc"
|
||||
]
|
||||
|
||||
[[example]]
|
||||
|
||||
10
README.md
10
README.md
@@ -125,12 +125,10 @@ $ cargo test -- --ignored
|
||||
Language bindings are available for:
|
||||
|
||||
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
||||
- **Node.js**
|
||||
- over cffi (legacy): \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
|
||||
- over jsonrpc built with napi.rs: \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
|
||||
- **Node.js** \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
|
||||
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
|
||||
- **Go**[^1] \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
|
||||
- **Free Pascal**[^1] \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
|
||||
- **Go** \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
|
||||
- **Free Pascal** \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
|
||||
- **Java** and **Swift** (contained in the Android/iOS repos)
|
||||
|
||||
The following "frontend" projects make use of the Rust-library
|
||||
@@ -142,5 +140,3 @@ or its language bindings:
|
||||
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
|
||||
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
||||
- several **Bots**
|
||||
|
||||
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::Events;
|
||||
use tempfile::tempdir;
|
||||
|
||||
@@ -9,9 +8,7 @@ async fn address_book_benchmark(n: u32, read_count: u32) {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let id = 100;
|
||||
let context = Context::new(&dbfile, id, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let context = Context::new(&dbfile, id, Events::new()).await.unwrap();
|
||||
|
||||
let book = (0..n)
|
||||
.map(|i| format!("Name {}\naddr{}@example.org\n", i, i))
|
||||
|
||||
@@ -5,14 +5,11 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use deltachat::chat::{self, ChatId};
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::Events;
|
||||
|
||||
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
|
||||
let id = 100;
|
||||
let context = Context::new(dbfile, id, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let context = Context::new(dbfile, id, Events::new()).await.unwrap();
|
||||
|
||||
for c in chats.iter().take(10) {
|
||||
black_box(chat::get_chat_msgs(&context, *c, 0).await.ok());
|
||||
@@ -26,7 +23,7 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
|
||||
let chats: Vec<_> = rt.block_on(async {
|
||||
let context = Context::new(Path::new(&path), 100, Events::new(), StockStrings::new())
|
||||
let context = Context::new(Path::new(&path), 100, Events::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap();
|
||||
|
||||
@@ -4,7 +4,6 @@ use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
|
||||
use deltachat::chatlist::Chatlist;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::Events;
|
||||
|
||||
async fn get_chat_list_benchmark(context: &Context) {
|
||||
@@ -17,7 +16,7 @@ fn criterion_benchmark(c: &mut Criterion) {
|
||||
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
|
||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
let context = rt.block_on(async {
|
||||
Context::new(Path::new(&path), 100, Events::new(), StockStrings::new())
|
||||
Context::new(Path::new(&path), 100, Events::new())
|
||||
.await
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
@@ -6,7 +6,6 @@ use deltachat::{
|
||||
context::Context,
|
||||
imex::{imex, ImexMode},
|
||||
receive_imf::receive_imf,
|
||||
stock_str::StockStrings,
|
||||
Events,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
@@ -42,9 +41,7 @@ async fn create_context() -> Context {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
let id = 100;
|
||||
let context = Context::new(&dbfile, id, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let context = Context::new(&dbfile, id, Events::new()).await.unwrap();
|
||||
|
||||
let backup: PathBuf = std::env::current_dir()
|
||||
.unwrap()
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||
use deltachat::context::Context;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::Events;
|
||||
use std::path::Path;
|
||||
|
||||
async fn search_benchmark(dbfile: impl AsRef<Path>) {
|
||||
let id = 100;
|
||||
let context = Context::new(dbfile.as_ref(), id, Events::new(), StockStrings::new())
|
||||
let context = Context::new(dbfile.as_ref(), id, Events::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.101.0"
|
||||
version = "1.87.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
@@ -25,7 +25,7 @@ tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||
anyhow = "1"
|
||||
thiserror = "1"
|
||||
rand = "0.7"
|
||||
once_cell = "1.16.0"
|
||||
once_cell = "1.12.0"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -11,23 +11,20 @@ extern "C" {
|
||||
#endif
|
||||
|
||||
|
||||
typedef struct _dc_context dc_context_t;
|
||||
typedef struct _dc_accounts dc_accounts_t;
|
||||
typedef struct _dc_array dc_array_t;
|
||||
typedef struct _dc_chatlist dc_chatlist_t;
|
||||
typedef struct _dc_chat dc_chat_t;
|
||||
typedef struct _dc_msg dc_msg_t;
|
||||
typedef struct _dc_reactions dc_reactions_t;
|
||||
typedef struct _dc_contact dc_contact_t;
|
||||
typedef struct _dc_lot dc_lot_t;
|
||||
typedef struct _dc_provider dc_provider_t;
|
||||
typedef struct _dc_event dc_event_t;
|
||||
typedef struct _dc_context dc_context_t;
|
||||
typedef struct _dc_accounts dc_accounts_t;
|
||||
typedef struct _dc_array dc_array_t;
|
||||
typedef struct _dc_chatlist dc_chatlist_t;
|
||||
typedef struct _dc_chat dc_chat_t;
|
||||
typedef struct _dc_msg dc_msg_t;
|
||||
typedef struct _dc_contact dc_contact_t;
|
||||
typedef struct _dc_lot dc_lot_t;
|
||||
typedef struct _dc_provider dc_provider_t;
|
||||
typedef struct _dc_event dc_event_t;
|
||||
typedef struct _dc_event_emitter dc_event_emitter_t;
|
||||
typedef struct _dc_accounts_event_emitter dc_accounts_event_emitter_t;
|
||||
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
|
||||
|
||||
// Alias for backwards compatibility, use dc_event_emitter_t instead.
|
||||
typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
|
||||
|
||||
/**
|
||||
* @mainpage Getting started
|
||||
*
|
||||
@@ -394,8 +391,7 @@ char* dc_get_blobdir (const dc_context_t* context);
|
||||
* If no type is prefixed, the videochat is handled completely in a browser.
|
||||
* - `bot` = Set to "1" if this is a bot.
|
||||
* Prevents adding the "Device messages" and "Saved messages" chats,
|
||||
* adds Auto-Submitted header to outgoing messages
|
||||
* and accepts contact requests automatically (calling dc_accept_chat() is not needed for bots).
|
||||
* adds Auto-Submitted header to outgoing messages.
|
||||
* - `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.
|
||||
@@ -469,10 +465,10 @@ int dc_set_stock_translation(dc_context_t* context, uint32_t stock_i
|
||||
/**
|
||||
* Set configuration values from a QR code.
|
||||
* Before this function is called, dc_check_qr() should confirm the type of the
|
||||
* QR code is DC_QR_ACCOUNT, DC_QR_LOGIN or DC_QR_WEBRTC_INSTANCE.
|
||||
* QR code is DC_QR_ACCOUNT or DC_QR_WEBRTC_INSTANCE.
|
||||
*
|
||||
* Internally, the function will call dc_set_config() with the appropriate keys,
|
||||
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT and DC_QR_LOGIN
|
||||
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT
|
||||
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -992,34 +988,6 @@ uint32_t dc_send_text_msg (dc_context_t* context, uint32_t ch
|
||||
uint32_t dc_send_videochat_invitation (dc_context_t* context, uint32_t chat_id);
|
||||
|
||||
|
||||
/**
|
||||
* Send a reaction to message.
|
||||
*
|
||||
* Reaction is a string of emojis separated by spaces. Reaction to a
|
||||
* single message can be sent multiple times. The last reaction
|
||||
* received overrides all previously received reactions. It is
|
||||
* possible to remove all reactions by sending an empty string.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id ID of the message you react to.
|
||||
* @param reaction A string consisting of emojis separated by spaces.
|
||||
* @return The ID of the message sent out or 0 for errors.
|
||||
*/
|
||||
uint32_t dc_send_reaction (dc_context_t* context, uint32_t msg_id, char *reaction);
|
||||
|
||||
|
||||
/**
|
||||
* Get a structure with reactions to the message.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param msg_id The message ID to get reactions for.
|
||||
* @return A structure with all reactions to the message.
|
||||
*/
|
||||
dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
|
||||
|
||||
|
||||
/**
|
||||
* A webxdc instance sends a status update to its other members.
|
||||
*
|
||||
@@ -1295,7 +1263,7 @@ void dc_marknoticed_chat (dc_context_t* context, uint32_t ch
|
||||
|
||||
|
||||
/**
|
||||
* Returns all message IDs of the given types in a given chat or any chat.
|
||||
* Returns all message IDs of the given types in a chat.
|
||||
* Typically used to show a gallery.
|
||||
* The result must be dc_array_unref()'d
|
||||
*
|
||||
@@ -1305,8 +1273,7 @@ void dc_marknoticed_chat (dc_context_t* context, uint32_t ch
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param chat_id >0: get messages with media from this chat ID.
|
||||
* 0: get messages with media from any chat of the currently used account.
|
||||
* @param chat_id The chat ID to get all messages with media from.
|
||||
* @param msg_type Specify a message type to query here, one of the @ref DC_MSG constants.
|
||||
* @param msg_type2 Alternative message type to search for. 0 to skip.
|
||||
* @param msg_type3 Alternative message type to search for. 0 to skip.
|
||||
@@ -1317,6 +1284,7 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
|
||||
|
||||
/**
|
||||
* Search next/previous message based on a given message and a list of types.
|
||||
* The
|
||||
* Typically used to implement the "next" and "previous" buttons
|
||||
* in a gallery or in a media player.
|
||||
*
|
||||
@@ -1417,9 +1385,6 @@ void dc_block_chat (dc_context_t* context, uint32_t ch
|
||||
*
|
||||
* Use it to accept "contact request" chats as indicated by dc_chat_is_contact_request().
|
||||
*
|
||||
* If the dc_set_config()-option `bot` is set,
|
||||
* all chats are accepted automatically and calling this function has no effect.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object as returned from dc_context_new().
|
||||
* @param chat_id The ID of the chat to accept.
|
||||
@@ -2056,9 +2021,8 @@ char* dc_get_contact_encrinfo (dc_context_t* context, uint32_t co
|
||||
|
||||
|
||||
/**
|
||||
* Delete a contact so that it disappears from the corresponding lists.
|
||||
* Depending on whether there are ongoing chats, deletion is done by physical deletion or hiding.
|
||||
* The contact is deleted from the local device.
|
||||
* Delete a contact. The contact is deleted from the local device. It may happen that this is not
|
||||
* possible as the contact is in use. In this case, the contact can be blocked.
|
||||
*
|
||||
* May result in a #DC_EVENT_CONTACTS_CHANGED event.
|
||||
*
|
||||
@@ -2204,10 +2168,11 @@ char* dc_imex_has_backup (dc_context_t* context, const char*
|
||||
* ~~~
|
||||
*
|
||||
* After that, this function should be called to send the Autocrypt Setup Message.
|
||||
* The function creates the setup message and adds it to outgoing message queue.
|
||||
* The message is sent asynchronously.
|
||||
* The function creates the setup message and waits until it is really sent.
|
||||
* As this may take a while, it is recommended to start the function in a separate thread;
|
||||
* to interrupt it, you can use dc_stop_ongoing_process().
|
||||
*
|
||||
* The required setup code is returned in the following format:
|
||||
* After everything succeeded, the required setup code is returned in the following format:
|
||||
*
|
||||
* ~~~
|
||||
* 1234-1234-1234-1234-1234-1234-1234-1234-1234
|
||||
@@ -2273,8 +2238,8 @@ int dc_continue_key_transfer (dc_context_t* context, uint32_t ms
|
||||
* The ongoing process will return ASAP then, however, it may
|
||||
* still take a moment.
|
||||
*
|
||||
* Typical ongoing processes are started by dc_configure()
|
||||
* or dc_imex(). As there is always at most only
|
||||
* Typical ongoing processes are started by dc_configure(),
|
||||
* dc_initiate_key_transfer() or dc_imex(). As there is always at most only
|
||||
* one onging process at the same time, there is no need to define _which_ process to exit.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -2300,7 +2265,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
#define DC_QR_WITHDRAW_VERIFYGROUP 502 // text1=groupname
|
||||
#define DC_QR_REVIVE_VERIFYCONTACT 510
|
||||
#define DC_QR_REVIVE_VERIFYGROUP 512 // text1=groupname
|
||||
#define DC_QR_LOGIN 520 // text1=email_address
|
||||
|
||||
/**
|
||||
* Check a scanned QR code.
|
||||
@@ -2328,7 +2292,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* - DC_QR_FPR_MISMATCH with dc_lot_t::id=Contact ID:
|
||||
* scanned fingerprint does not match last seen fingerprint.
|
||||
*
|
||||
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::text1=Formatted fingerprint
|
||||
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::test1=Formatted fingerprint
|
||||
* the scanned QR code contains a fingerprint but no e-mail address;
|
||||
* suggest the user to establish an encrypted connection first.
|
||||
*
|
||||
@@ -2341,8 +2305,7 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* if so, call dc_set_config_from_qr().
|
||||
*
|
||||
* - DC_QR_ADDR with dc_lot_t::id=Contact ID:
|
||||
* e-mail address scanned, optionally, a draft message could be set in
|
||||
* dc_lot_t::text1 in which case dc_lot_t::text1_meaning will be DC_TEXT1_DRAFT;
|
||||
* e-mail address scanned,
|
||||
* ask the user if they want to start chatting;
|
||||
* if so, call dc_create_chat_by_contact_id().
|
||||
*
|
||||
@@ -2373,10 +2336,6 @@ void dc_stop_ongoing_process (dc_context_t* context);
|
||||
* ask the user if they want to revive the withdrawn group-invite code;
|
||||
* if so, call dc_set_config_from_qr().
|
||||
*
|
||||
* - DC_QR_LOGIN with dc_lot_t::text1=email_address:
|
||||
* ask the user if they want to login with the email_address,
|
||||
* if so, call dc_set_config_from_qr() and then dc_configure().
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
* @param context The context object.
|
||||
* @param qr The text of the scanned QR code.
|
||||
@@ -2550,9 +2509,9 @@ int dc_set_location (dc_context_t* context, double latit
|
||||
* Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC.
|
||||
* 0 for "all up to now".
|
||||
* @return An array of locations, NULL is never returned.
|
||||
* The array is sorted descending;
|
||||
* The array is sorted decending;
|
||||
* the first entry in the array is the location with the newest timestamp.
|
||||
* Note that this is only related to the recent position of the user
|
||||
* Note that this is only realated to the recent postion of the user
|
||||
* if dc_array_is_independent() returns 0.
|
||||
* The returned array must be freed using dc_array_unref().
|
||||
*
|
||||
@@ -2876,7 +2835,7 @@ void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
|
||||
*
|
||||
* The library will emit various @ref DC_EVENT events as "new message", "message read" etc.
|
||||
* To get these events, you have to create an event emitter using this function
|
||||
* and call dc_get_next_event() on the emitter.
|
||||
* and call dc_accounts_get_next_event() on the emitter.
|
||||
*
|
||||
* This is similar to dc_get_event_emitter(), which, however,
|
||||
* must not be called for accounts handled by the account manager.
|
||||
@@ -2884,13 +2843,13 @@ void dc_accounts_maybe_network_lost (dc_accounts_t* accounts);
|
||||
* @memberof dc_accounts_t
|
||||
* @param accounts The account manager as created by dc_accounts_new().
|
||||
* @return Returns the event emitter, NULL on errors.
|
||||
* Must be freed using dc_event_emitter_unref() after usage.
|
||||
* Must be freed using dc_accounts_event_emitter_unref() after usage.
|
||||
*
|
||||
* Note: Use only one event emitter per account manager.
|
||||
* Having more than one event emitter running at the same time on the same account manager
|
||||
* will result in events randomly delivered to the one or to the other.
|
||||
*/
|
||||
dc_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts);
|
||||
dc_accounts_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts);
|
||||
|
||||
|
||||
/**
|
||||
@@ -3297,19 +3256,6 @@ uint32_t dc_chat_get_id (const dc_chat_t* chat);
|
||||
int dc_chat_get_type (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Returns the address where messages are sent to if the chat is a mailing list.
|
||||
* If you just want to know if a mailing list can be written to,
|
||||
* use dc_chat_can_send() instead.
|
||||
*
|
||||
* @memberof dc_chat_t
|
||||
* @param chat The chat object.
|
||||
* @return The mailing list address. Must be released using dc_str_unref() after usage.
|
||||
* If there is no such address, an empty string is returned, NULL is never returned.
|
||||
*/
|
||||
char* dc_chat_get_mailinglist_addr (const dc_chat_t* chat);
|
||||
|
||||
|
||||
/**
|
||||
* Get name of a chat. For one-to-one chats, this is the name of the contact.
|
||||
* For group chats, this is the name given e.g. to dc_create_group_chat() or
|
||||
@@ -3797,11 +3743,6 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
|
||||
* URL where the source code of the Webxdc and other information can be found;
|
||||
* defaults to an empty string.
|
||||
* Implementations may offer an menu or a button to open this URL.
|
||||
* - internet_access:
|
||||
* true if the Webxdc should get full internet access, including Webrtc.
|
||||
* currently, this is only true for encrypted Webxdc's in the self chat
|
||||
* that have requested internet access in the manifest.
|
||||
* this is useful for development and maybe for internal integrations at some point.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The webxdc instance.
|
||||
@@ -4098,19 +4039,9 @@ int dc_msg_get_info_type (const dc_msg_t* msg);
|
||||
|
||||
|
||||
// DC_INFO* uses the same values as SystemMessage in rust-land
|
||||
#define DC_INFO_UNKNOWN 0
|
||||
#define DC_INFO_GROUP_NAME_CHANGED 2
|
||||
#define DC_INFO_GROUP_IMAGE_CHANGED 3
|
||||
#define DC_INFO_MEMBER_ADDED_TO_GROUP 4
|
||||
#define DC_INFO_MEMBER_REMOVED_FROM_GROUP 5
|
||||
#define DC_INFO_AUTOCRYPT_SETUP_MESSAGE 6
|
||||
#define DC_INFO_SECURE_JOIN_MESSAGE 7
|
||||
#define DC_INFO_LOCATIONSTREAMING_ENABLED 8
|
||||
#define DC_INFO_LOCATION_ONLY 9
|
||||
#define DC_INFO_EPHEMERAL_TIMER_CHANGED 10
|
||||
#define DC_INFO_PROTECTION_ENABLED 11
|
||||
#define DC_INFO_PROTECTION_DISABLED 12
|
||||
#define DC_INFO_WEBXDC_INFO_MESSAGE 32
|
||||
#define DC_INFO_PROTECTION_ENABLED 11
|
||||
#define DC_INFO_PROTECTION_DISABLED 12
|
||||
|
||||
|
||||
/**
|
||||
* Check if a message is still in creation. A message is in creation between
|
||||
@@ -4693,22 +4624,6 @@ char* dc_contact_get_status (const dc_contact_t* contact);
|
||||
*/
|
||||
int64_t dc_contact_get_last_seen (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Check if the contact was seen recently.
|
||||
*
|
||||
* The UI may highlight these contacts,
|
||||
* eg. draw a little green dot on the avatars of the users recently seen.
|
||||
* DC_CONTACT_ID_SELF and other special contact IDs are defined as never seen recently (they should not get a dot).
|
||||
* To get the time a contact was seen, use dc_contact_get_last_seen().
|
||||
*
|
||||
* @memberof dc_contact_t
|
||||
* @param contact The contact object.
|
||||
* @return 1=contact seen recently, 0=contact not seen recently.
|
||||
*/
|
||||
int dc_contact_was_seen_recently (const dc_contact_t* contact);
|
||||
|
||||
|
||||
/**
|
||||
* Check if a contact is blocked.
|
||||
*
|
||||
@@ -4922,49 +4837,7 @@ uint32_t dc_lot_get_id (const dc_lot_t* lot);
|
||||
* @param lot The lot object.
|
||||
* @return The timestamp as defined by the creator of the object. 0 if there is not timestamp or on errors.
|
||||
*/
|
||||
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_reactions_t
|
||||
*
|
||||
* An object representing all reactions for a single message.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns array of contacts which reacted to the given message.
|
||||
*
|
||||
* @memberof dc_reactions_t
|
||||
* @param reactions The object containing message reactions.
|
||||
* @return array of contact IDs. Use dc_array_get_cnt() to get array length and
|
||||
* dc_array_get_id() to get the IDs. Should be freed using `dc_array_unref()` after usage.
|
||||
*/
|
||||
dc_array_t* dc_reactions_get_contacts(dc_reactions_t* reactions);
|
||||
|
||||
|
||||
/**
|
||||
* Returns a string containing space-separated reactions of a single contact.
|
||||
*
|
||||
* @memberof dc_reactions_t
|
||||
* @param reactions The object containing message reactions.
|
||||
* @param contact_id ID of the contact.
|
||||
* @return Space-separated list of emoji sequences, which could be empty.
|
||||
* Returned string should not be modified and should be freed
|
||||
* with dc_str_unref() after usage.
|
||||
*/
|
||||
char* dc_reactions_get_by_contact_id(dc_reactions_t* reactions, uint32_t contact_id);
|
||||
|
||||
|
||||
/**
|
||||
* Frees an object containing message reactions.
|
||||
*
|
||||
* Reactions objects are created by dc_get_msg_reactions().
|
||||
*
|
||||
* @memberof dc_reactions_t
|
||||
* @param reactions The object to free.
|
||||
* If NULL is given, nothing is done.
|
||||
*/
|
||||
void dc_reactions_unref (dc_reactions_t* reactions);
|
||||
int64_t dc_lot_get_timestamp (const dc_lot_t* lot);
|
||||
|
||||
|
||||
/**
|
||||
@@ -5358,8 +5231,9 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
|
||||
* @class dc_event_emitter_t
|
||||
*
|
||||
* Opaque object that is used to get events from a single context.
|
||||
* You can get an event emitter from a context using dc_get_event_emitter()
|
||||
* or dc_accounts_get_event_emitter().
|
||||
* You can get an event emitter from a context using dc_get_event_emitter().
|
||||
* If you are using the dc_accounts_t account manager,
|
||||
* dc_accounts_event_emitter_t must be used instead.
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -5375,8 +5249,6 @@ char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
|
||||
*/
|
||||
dc_event_t* dc_get_next_event(dc_event_emitter_t* emitter);
|
||||
|
||||
// Alias for backwards compatibility, use dc_get_next_event instead.
|
||||
#define dc_accounts_get_next_event dc_get_next_event
|
||||
|
||||
/**
|
||||
* Free a context event emitter object.
|
||||
@@ -5387,8 +5259,39 @@ dc_event_t* dc_get_next_event(dc_event_emitter_t* emitter);
|
||||
*/
|
||||
void dc_event_emitter_unref(dc_event_emitter_t* emitter);
|
||||
|
||||
// Alias for backwards compatibility, use dc_event_emtitter_unref instead.
|
||||
#define dc_accounts_event_emitter_unref dc_event_emitter_unref
|
||||
|
||||
/**
|
||||
* @class dc_accounts_event_emitter_t
|
||||
*
|
||||
* Opaque object that is used to get events from the dc_accounts_t account manager.
|
||||
* You get an event emitter from the account manager using dc_accounts_get_event_emitter().
|
||||
* If you are not using the dc_accounts_t account manager but just a single dc_context_t object,
|
||||
* dc_event_emitter_t must be used instead.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Get the next event from an accounts event emitter object.
|
||||
*
|
||||
* @memberof dc_accounts_event_emitter_t
|
||||
* @param emitter Event emitter object as returned from dc_accounts_get_event_emitter().
|
||||
* @return An event as an dc_event_t object.
|
||||
* You can query the event for information using dc_event_get_id(), dc_event_get_data1_int() and so on;
|
||||
* if you are done with the event, you have to free the event using dc_event_unref().
|
||||
* If NULL is returned, the contexts belonging to the event emitter are unref'd and no more events will come;
|
||||
* in this case, free the event emitter using dc_accounts_event_emitter_unref().
|
||||
*/
|
||||
dc_event_t* dc_accounts_get_next_event (dc_accounts_event_emitter_t* emitter);
|
||||
|
||||
|
||||
/**
|
||||
* Free an accounts event emitter object.
|
||||
*
|
||||
* @memberof dc_accounts_event_emitter_t
|
||||
* @param emitter Event emitter object as returned from dc_accounts_get_event_emitter().
|
||||
* If NULL is given, nothing is done and an error is logged.
|
||||
*/
|
||||
void dc_accounts_event_emitter_unref(dc_accounts_event_emitter_t* emitter);
|
||||
|
||||
|
||||
/**
|
||||
* @class dc_event_t
|
||||
@@ -5460,7 +5363,7 @@ char* dc_event_get_data2_str(dc_event_t* event);
|
||||
* To get the context object belonging to the event, use dc_accounts_get_account().
|
||||
*
|
||||
* @memberof dc_event_t
|
||||
* @param event The event object as returned from dc_get_next_event().
|
||||
* @param event The event object as returned from dc_accounts_get_next_event().
|
||||
* @return The account ID belonging to the event, 0 for account manager errors.
|
||||
*/
|
||||
uint32_t dc_event_get_account_id(dc_event_t* event);
|
||||
@@ -5615,15 +5518,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
#define DC_EVENT_MSGS_CHANGED 2000
|
||||
|
||||
|
||||
/**
|
||||
* Message reactions changed.
|
||||
*
|
||||
* @param data1 (int) chat_id ID of the chat affected by the changes.
|
||||
* @param data2 (int) msg_id ID of the message for which reactions were changed.
|
||||
*/
|
||||
#define DC_EVENT_REACTIONS_CHANGED 2001
|
||||
|
||||
|
||||
/**
|
||||
* There is a fresh message. Typically, the user will show an notification
|
||||
* when receiving this message.
|
||||
@@ -5635,17 +5529,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
*/
|
||||
#define DC_EVENT_INCOMING_MSG 2005
|
||||
|
||||
/**
|
||||
* Downloading a bunch of messages just finished. This is an experimental
|
||||
* event to allow the UI to only show one notification per message bunch,
|
||||
* instead of cluttering the user with many notifications.
|
||||
* For each of the msg_ids, an additional #DC_EVENT_INCOMING_MSG event was emitted before.
|
||||
*
|
||||
* @param data1 0
|
||||
* @param data2 (char*) msg_ids, a json object with the message ids.
|
||||
*/
|
||||
#define DC_EVENT_INCOMING_MSG_BUNCH 2006
|
||||
|
||||
|
||||
/**
|
||||
* Messages were marked noticed or seen.
|
||||
@@ -5830,15 +5713,7 @@ void dc_event_unref(dc_event_t* event);
|
||||
* @param data1 (int) msg_id
|
||||
* @param data2 (int) status_update_serial - must not be used by UI implementations.
|
||||
*/
|
||||
#define DC_EVENT_WEBXDC_STATUS_UPDATE 2120
|
||||
|
||||
/**
|
||||
* Message deleted which contained a webxdc instance.
|
||||
*
|
||||
* @param data1 (int) msg_id
|
||||
*/
|
||||
|
||||
#define DC_EVENT_WEBXDC_INSTANCE_DELETED 2121
|
||||
#define DC_EVENT_WEBXDC_STATUS_UPDATE 2120
|
||||
|
||||
|
||||
/**
|
||||
@@ -6072,38 +5947,28 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in status messages for group name changes.
|
||||
/// - %1$s will be replaced by the old group name
|
||||
/// - %2$s will be replaced by the new group name
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_MSGGRPNAME 15
|
||||
|
||||
/// "Group image changed."
|
||||
///
|
||||
/// Used in status messages for group images changes.
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_MSGGRPIMGCHANGED 16
|
||||
|
||||
/// "Member %1$s added."
|
||||
///
|
||||
/// Used in status messages for added members.
|
||||
/// - %1$s will be replaced by the name of the added member
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_MSGADDMEMBER 17
|
||||
|
||||
/// "Member %1$s removed."
|
||||
///
|
||||
/// Used in status messages for removed members.
|
||||
/// - %1$s will be replaced by the name of the removed member
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_MSGDELMEMBER 18
|
||||
|
||||
/// "Group left."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_MSGGROUPLEFT 19
|
||||
|
||||
/// "GIF"
|
||||
@@ -6150,7 +6015,9 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// - %1$s will be replaced by the subject of the displayed message
|
||||
#define DC_STR_READRCPT_MAILBODY 32
|
||||
|
||||
/// @deprecated Deprecated, this string is no longer needed.
|
||||
/// "Group image deleted."
|
||||
///
|
||||
/// Used in status messages for deleted group images.
|
||||
#define DC_STR_MSGGRPIMGDELETED 33
|
||||
|
||||
/// "End-to-end encryption preferred."
|
||||
@@ -6203,8 +6070,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// - %1$s will be replaced by an action
|
||||
/// as #DC_STR_MSGADDMEMBER or #DC_STR_MSGGRPIMGCHANGED (full-stop removed, if any)
|
||||
/// - %2$s will be replaced by the name of the user taking that action
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_MSGACTIONBYUSER 62
|
||||
|
||||
/// "%1$s by me"
|
||||
@@ -6212,8 +6077,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used to concretize actions.
|
||||
/// - %1$s will be replaced by an action
|
||||
/// as #DC_STR_MSGADDMEMBER or #DC_STR_MSGGRPIMGCHANGED (full-stop removed, if any)
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_MSGACTIONBYME 63
|
||||
|
||||
/// "Location streaming enabled."
|
||||
@@ -6277,8 +6140,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "Message deletion timer is disabled."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_EPHEMERAL_DISABLED 75
|
||||
|
||||
/// "Message deletion timer is set to %1$s s."
|
||||
@@ -6286,36 +6147,26 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used in status messages when the other constants
|
||||
/// (#DC_STR_EPHEMERAL_MINUTE, #DC_STR_EPHEMERAL_HOUR and so on) do not match the timer.
|
||||
/// - %1$s will be replaced by the number of seconds the timer is set to
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_EPHEMERAL_SECONDS 76
|
||||
|
||||
/// "Message deletion timer is set to 1 minute."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// @deperecated 2022-09-10
|
||||
#define DC_STR_EPHEMERAL_MINUTE 77
|
||||
|
||||
/// "Message deletion timer is set to 1 hour."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_EPHEMERAL_HOUR 78
|
||||
|
||||
/// "Message deletion timer is set to 1 day."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_EPHEMERAL_DAY 79
|
||||
|
||||
/// "Message deletion timer is set to 1 week."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// @deprecated 2022-09-10
|
||||
#define DC_STR_EPHEMERAL_WEEK 80
|
||||
|
||||
/// @deprecated Deprecated 2021-01-30, DC_STR_EPHEMERAL_WEEKS is used instead.
|
||||
@@ -6356,11 +6207,12 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Chat protection enabled."
|
||||
///
|
||||
|
||||
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_ENABLED_PROTECTION and DC_STR_MSG_PROTECTION_ENABLED_BY.
|
||||
/// Used in status messages.
|
||||
#define DC_STR_PROTECTION_ENABLED 88
|
||||
|
||||
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_DISABLED_PROTECTION and DC_STR_MSG_PROTECTION_DISABLED_BY.
|
||||
/// "Chat protection disabled."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_PROTECTION_DISABLED 89
|
||||
|
||||
/// "Reply"
|
||||
@@ -6382,37 +6234,29 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// "Message deletion timer is set to %1$s minutes."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of minutes (always >1) the timer is set to.
|
||||
///
|
||||
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_MINUTES and DC_STR_MSG_EPHEMERAL_TIMER_MINUTES_BY.
|
||||
//
|
||||
/// `%1$s` will be replaced by the number of minutes (alwasy >1) the timer is set to.
|
||||
#define DC_STR_EPHEMERAL_MINUTES 93
|
||||
|
||||
/// "Message deletion timer is set to %1$s hours."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
//
|
||||
/// `%1$s` will be replaced by the number of hours (always >1) the timer is set to.
|
||||
///
|
||||
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_HOURS and DC_STR_MSG_EPHEMERAL_TIMER_HOURS_BY.
|
||||
#define DC_STR_EPHEMERAL_HOURS 94
|
||||
|
||||
/// "Message deletion timer is set to %1$s days."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
//
|
||||
/// `%1$s` will be replaced by the number of days (always >1) the timer is set to.
|
||||
///
|
||||
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_DAYS and DC_STR_MSG_EPHEMERAL_TIMER_DAYS_BY.
|
||||
#define DC_STR_EPHEMERAL_DAYS 95
|
||||
|
||||
/// "Message deletion timer is set to %1$s weeks."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
//
|
||||
/// `%1$s` will be replaced by the number of weeks (always >1) the timer is set to.
|
||||
///
|
||||
/// @deprecated Replaced by DC_STR_MSG_YOU_EPHEMERAL_TIMER_WEEKS and DC_STR_MSG_EPHEMERAL_TIMER_WEEKS_BY.
|
||||
#define DC_STR_EPHEMERAL_WEEKS 96
|
||||
|
||||
/// "Forwarded"
|
||||
@@ -6569,264 +6413,6 @@ void dc_event_unref(dc_event_t* event);
|
||||
/// Used as status in the connectivity view.
|
||||
#define DC_STR_NOT_CONNECTED 121
|
||||
|
||||
/// "%1$s changed their address from %2$s to %3$s"
|
||||
///
|
||||
/// Used as an info message to chats with contacts that changed their address.
|
||||
#define DC_STR_AEAP_ADDR_CHANGED 122
|
||||
|
||||
/// "You changed your email address from %1$s to %2$s.
|
||||
/// If you now send a message to a group, contacts there will automatically
|
||||
/// replace the old with your new address.\n\nIt's highly advised to set up
|
||||
/// your old email provider to forward all emails to your new email address.
|
||||
/// Otherwise you might miss messages of contacts who did not get your new
|
||||
/// address yet." + the link to the AEAP blog post
|
||||
///
|
||||
/// As soon as there is a post about AEAP, the UIs should add it:
|
||||
/// set_stock_translation(123, getString(aeap_explanation) + "\n\n" + AEAP_BLOG_LINK)
|
||||
///
|
||||
/// Used in a device message that explains AEAP.
|
||||
#define DC_STR_AEAP_EXPLANATION_AND_LINK 123
|
||||
|
||||
/// "You changed group name from \"%1$s\" to \"%2$s\"."
|
||||
///
|
||||
/// `%1$s` will be replaced by the old group name.
|
||||
/// `%2$s` will be replaced by the new group name.
|
||||
#define DC_STR_GROUP_NAME_CHANGED_BY_YOU 124
|
||||
|
||||
/// "Group name changed from \"%1$s\" to \"%2$s\" by %3$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by the old group name.
|
||||
/// `%2$s` will be replaced by the new group name.
|
||||
/// `%3$s` will be replaced by name and address of the contact who did the action.
|
||||
#define DC_STR_GROUP_NAME_CHANGED_BY_OTHER 125
|
||||
|
||||
/// "You changed the group image."
|
||||
#define DC_STR_GROUP_IMAGE_CHANGED_BY_YOU 126
|
||||
|
||||
/// "Group image changed by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact who did the action.
|
||||
#define DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER 127
|
||||
|
||||
/// "You added member %1$s."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_ADD_MEMBER_BY_YOU 128
|
||||
|
||||
/// "Member %1$s added by %2$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact added to the group.
|
||||
/// `%2$s` will be replaced by name and address of the contact who did the action.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_ADD_MEMBER_BY_OTHER 129
|
||||
|
||||
/// "You removed member %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact removed from the group.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_REMOVE_MEMBER_BY_YOU 130
|
||||
|
||||
/// "Member %1$s removed by %2$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact removed from the group.
|
||||
/// `%2$s` will be replaced by name and address of the contact who did the action.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_REMOVE_MEMBER_BY_OTHER 131
|
||||
|
||||
/// "You left the group."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_GROUP_LEFT_BY_YOU 132
|
||||
|
||||
/// "Group left by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_GROUP_LEFT_BY_OTHER 133
|
||||
|
||||
/// "You deleted the group image."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_GROUP_IMAGE_DELETED_BY_YOU 134
|
||||
|
||||
/// "Group image deleted by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_GROUP_IMAGE_DELETED_BY_OTHER 135
|
||||
|
||||
/// "You enabled location streaming."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_LOCATION_ENABLED_BY_YOU 136
|
||||
|
||||
/// "Location streaming enabled by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_LOCATION_ENABLED_BY_OTHER 137
|
||||
|
||||
/// "You disabled message deletion timer."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU 138
|
||||
|
||||
/// "Message deletion timer is disabled by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER 139
|
||||
|
||||
/// "You set message deletion timer to %1$s s."
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of seconds (always >1) the timer is set to.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU 140
|
||||
|
||||
/// "Message deletion timer is set to %1$s s by %2$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of seconds (always >1) the timer is set to.
|
||||
/// `%2$s` will be replaced by name and address of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER 141
|
||||
|
||||
/// "You set message deletion timer to 1 minute."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU 142
|
||||
|
||||
/// "Message deletion timer is set to 1 minute by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER 143
|
||||
|
||||
/// "You set message deletion timer to 1 hour."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU 144
|
||||
|
||||
/// "Message deletion timer is set to 1 hour by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER 145
|
||||
|
||||
/// "You set message deletion timer to 1 day."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU 146
|
||||
|
||||
/// "Message deletion timer is set to 1 day by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER 147
|
||||
|
||||
/// "You set message deletion timer to 1 week."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU 148
|
||||
|
||||
/// "Message deletion timer is set to 1 week by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER 149
|
||||
|
||||
/// "You set message deletion timer to %1$s minutes."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of minutes (always >1) the timer is set to.
|
||||
#define DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU 150
|
||||
|
||||
/// "Message deletion timer is set to %1$s minutes by %2$s."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of minutes (always >1) the timer is set to.
|
||||
/// `%2$s` will be replaced by name and address of the contact.
|
||||
#define DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER 151
|
||||
|
||||
/// "You set message deletion timer to %1$s hours."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of hours (always >1) the timer is set to.
|
||||
#define DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU 152
|
||||
|
||||
/// "Message deletion timer is set to %1$s hours by %2$s."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of hours (always >1) the timer is set to.
|
||||
/// `%2$s` will be replaced by name and address of the contact.
|
||||
#define DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER 153
|
||||
|
||||
/// "You set message deletion timer to %1$s days."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of days (always >1) the timer is set to.
|
||||
#define DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU 154
|
||||
|
||||
/// "Message deletion timer is set to %1$s days by %2$s."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of days (always >1) the timer is set to.
|
||||
/// `%2$s` will be replaced by name and address of the contact.
|
||||
#define DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER 155
|
||||
|
||||
/// "You set message deletion timer to %1$s weeks."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of weeks (always >1) the timer is set to.
|
||||
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU 156
|
||||
|
||||
/// "Message deletion timer is set to %1$s weeks by %2$s."
|
||||
///
|
||||
/// Used in status messages.
|
||||
///
|
||||
/// `%1$s` will be replaced by the number of weeks (always >1) the timer is set to.
|
||||
/// `%2$s` will be replaced by name and address of the contact.
|
||||
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
|
||||
|
||||
/// "You enabled chat protection."
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_PROTECTION_ENABLED_BY_YOU 158
|
||||
|
||||
/// "Chat protection enabled by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
///
|
||||
/// Used in status messages.
|
||||
#define DC_STR_PROTECTION_ENABLED_BY_OTHER 159
|
||||
|
||||
/// "You disabled chat protection."
|
||||
#define DC_STR_PROTECTION_DISABLED_BY_YOU 160
|
||||
|
||||
/// "Chat protection disabled by %1$s."
|
||||
///
|
||||
/// `%1$s` will be replaced by name and address of the contact.
|
||||
#define DC_STR_PROTECTION_DISABLED_BY_OTHER 161
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use crate::chat::ChatItem;
|
||||
use crate::constants::DC_MSG_ID_DAYMARKER;
|
||||
use crate::contact::ContactId;
|
||||
use crate::location::Location;
|
||||
use crate::message::MsgId;
|
||||
|
||||
@@ -8,7 +7,6 @@ use crate::message::MsgId;
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum dc_array_t {
|
||||
MsgIds(Vec<MsgId>),
|
||||
ContactIds(Vec<ContactId>),
|
||||
Chat(Vec<ChatItem>),
|
||||
Locations(Vec<Location>),
|
||||
Uint(Vec<u32>),
|
||||
@@ -18,7 +16,6 @@ impl dc_array_t {
|
||||
pub(crate) fn get_id(&self, index: usize) -> u32 {
|
||||
match self {
|
||||
Self::MsgIds(array) => array[index].to_u32(),
|
||||
Self::ContactIds(array) => array[index].to_u32(),
|
||||
Self::Chat(array) => match array[index] {
|
||||
ChatItem::Message { msg_id } => msg_id.to_u32(),
|
||||
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
|
||||
@@ -31,7 +28,6 @@ impl dc_array_t {
|
||||
pub(crate) fn get_timestamp(&self, index: usize) -> Option<i64> {
|
||||
match self {
|
||||
Self::MsgIds(_) => None,
|
||||
Self::ContactIds(_) => None,
|
||||
Self::Chat(array) => array.get(index).and_then(|item| match item {
|
||||
ChatItem::Message { .. } => None,
|
||||
ChatItem::DayMarker { timestamp } => Some(*timestamp),
|
||||
@@ -44,7 +40,6 @@ impl dc_array_t {
|
||||
pub(crate) fn get_marker(&self, index: usize) -> Option<&str> {
|
||||
match self {
|
||||
Self::MsgIds(_) => None,
|
||||
Self::ContactIds(_) => None,
|
||||
Self::Chat(_) => None,
|
||||
Self::Locations(array) => array
|
||||
.get(index)
|
||||
@@ -65,7 +60,6 @@ impl dc_array_t {
|
||||
pub(crate) fn len(&self) -> usize {
|
||||
match self {
|
||||
Self::MsgIds(array) => array.len(),
|
||||
Self::ContactIds(array) => array.len(),
|
||||
Self::Chat(array) => array.len(),
|
||||
Self::Locations(array) => array.len(),
|
||||
Self::Uint(array) => array.len(),
|
||||
@@ -89,12 +83,6 @@ impl From<Vec<MsgId>> for dc_array_t {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<ContactId>> for dc_array_t {
|
||||
fn from(array: Vec<ContactId>) -> Self {
|
||||
dc_array_t::ContactIds(array)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<ChatItem>> for dc_array_t {
|
||||
fn from(array: Vec<ChatItem>) -> Self {
|
||||
dc_array_t::Chat(array)
|
||||
|
||||
@@ -37,9 +37,7 @@ use deltachat::context::Context;
|
||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::key::DcKey;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
use deltachat::{accounts::Accounts, log::LogExt};
|
||||
@@ -67,8 +65,6 @@ use deltachat::chatlist::Chatlist;
|
||||
/// Struct representing the deltachat context.
|
||||
pub type dc_context_t = Context;
|
||||
|
||||
pub type dc_reactions_t = Reactions;
|
||||
|
||||
static RT: Lazy<Runtime> = Lazy::new(|| Runtime::new().expect("unable to create tokio runtime"));
|
||||
|
||||
fn block_on<T>(fut: T) -> T::Output
|
||||
@@ -102,12 +98,7 @@ pub unsafe extern "C" fn dc_context_new(
|
||||
let ctx = if blobdir.is_null() || *blobdir == 0 {
|
||||
// generate random ID as this functionality is not yet available on the C-api.
|
||||
let id = rand::thread_rng().gen();
|
||||
block_on(Context::new(
|
||||
as_path(dbfile),
|
||||
id,
|
||||
Events::new(),
|
||||
StockStrings::new(),
|
||||
))
|
||||
block_on(Context::new(as_path(dbfile), id, Events::new()))
|
||||
} else {
|
||||
eprintln!("blobdir can not be defined explicitly anymore");
|
||||
return ptr::null_mut();
|
||||
@@ -131,12 +122,7 @@ pub unsafe extern "C" fn dc_context_new_closed(dbfile: *const libc::c_char) -> *
|
||||
}
|
||||
|
||||
let id = rand::thread_rng().gen();
|
||||
match block_on(Context::new_closed(
|
||||
as_path(dbfile),
|
||||
id,
|
||||
Events::new(),
|
||||
StockStrings::new(),
|
||||
)) {
|
||||
match block_on(Context::new_closed(as_path(dbfile), id, Events::new())) {
|
||||
Ok(context) => Box::into_raw(Box::new(context)),
|
||||
Err(err) => {
|
||||
eprintln!("failed to create context: {:#}", err);
|
||||
@@ -183,7 +169,7 @@ pub unsafe extern "C" fn dc_context_unref(context: *mut dc_context_t) {
|
||||
eprintln!("ignoring careless call to dc_context_unref()");
|
||||
return;
|
||||
}
|
||||
drop(Box::from_raw(context));
|
||||
Box::from_raw(context);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -477,7 +463,7 @@ pub unsafe extern "C" fn dc_event_unref(a: *mut dc_event_t) {
|
||||
return;
|
||||
}
|
||||
|
||||
drop(Box::from_raw(a));
|
||||
Box::from_raw(a);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -501,9 +487,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::Error(_) => 400,
|
||||
EventType::ErrorSelfNotInGroup(_) => 410,
|
||||
EventType::MsgsChanged { .. } => 2000,
|
||||
EventType::ReactionsChanged { .. } => 2001,
|
||||
EventType::IncomingMsg { .. } => 2005,
|
||||
EventType::IncomingMsgBunch { .. } => 2006,
|
||||
EventType::MsgsNoticed { .. } => 2008,
|
||||
EventType::MsgDelivered { .. } => 2010,
|
||||
EventType::MsgFailed { .. } => 2012,
|
||||
@@ -520,7 +504,6 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
||||
EventType::ConnectivityChanged => 2100,
|
||||
EventType::SelfavatarChanged => 2110,
|
||||
EventType::WebxdcStatusUpdate { .. } => 2120,
|
||||
EventType::WebxdcInstanceDeleted { .. } => 2121,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -545,10 +528,8 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::Error(_)
|
||||
| EventType::ConnectivityChanged
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::ErrorSelfNotInGroup(_) => 0,
|
||||
EventType::MsgsChanged { chat_id, .. }
|
||||
| EventType::ReactionsChanged { chat_id, .. }
|
||||
| EventType::IncomingMsg { chat_id, .. }
|
||||
| EventType::MsgsNoticed(chat_id)
|
||||
| EventType::MsgDelivered { chat_id, .. }
|
||||
@@ -569,7 +550,6 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
||||
contact_id.to_u32() as libc::c_int
|
||||
}
|
||||
EventType::WebxdcStatusUpdate { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
EventType::WebxdcInstanceDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -601,12 +581,9 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
||||
| EventType::ImexFileWritten(_)
|
||||
| EventType::MsgsNoticed(_)
|
||||
| EventType::ConnectivityChanged
|
||||
| EventType::WebxdcInstanceDeleted { .. }
|
||||
| EventType::IncomingMsgBunch { .. }
|
||||
| EventType::SelfavatarChanged => 0,
|
||||
EventType::ChatModified(_) => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
| EventType::ReactionsChanged { msg_id, .. }
|
||||
| EventType::IncomingMsg { msg_id, .. }
|
||||
| EventType::MsgDelivered { msg_id, .. }
|
||||
| EventType::MsgFailed { msg_id, .. }
|
||||
@@ -646,7 +623,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::MsgsChanged { .. }
|
||||
| EventType::ReactionsChanged { .. }
|
||||
| EventType::IncomingMsg { .. }
|
||||
| EventType::MsgsNoticed(_)
|
||||
| EventType::MsgDelivered { .. }
|
||||
@@ -661,7 +637,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
| EventType::ConnectivityChanged
|
||||
| EventType::SelfavatarChanged
|
||||
| EventType::WebxdcStatusUpdate { .. }
|
||||
| EventType::WebxdcInstanceDeleted { .. }
|
||||
| EventType::ChatEphemeralTimerModified { .. } => ptr::null_mut(),
|
||||
EventType::ConfigureProgress { comment, .. } => {
|
||||
if let Some(comment) = comment {
|
||||
@@ -674,11 +649,6 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
||||
let data2 = file.to_c_string().unwrap_or_default();
|
||||
data2.into_raw()
|
||||
}
|
||||
EventType::IncomingMsgBunch { msg_ids } => serde_json::to_string(msg_ids)
|
||||
.unwrap_or_default()
|
||||
.to_c_string()
|
||||
.unwrap_or_default()
|
||||
.into_raw(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -713,7 +683,7 @@ pub unsafe extern "C" fn dc_event_emitter_unref(emitter: *mut dc_event_emitter_t
|
||||
return;
|
||||
}
|
||||
|
||||
drop(Box::from_raw(emitter));
|
||||
Box::from_raw(emitter);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -963,48 +933,6 @@ pub unsafe extern "C" fn dc_send_videochat_invitation(
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_reaction(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
reaction: *const libc::c_char,
|
||||
) -> u32 {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_send_reaction()");
|
||||
return 0;
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
block_on(async move {
|
||||
send_reaction(ctx, MsgId::new(msg_id), &to_string_lossy(reaction))
|
||||
.await
|
||||
.map(|msg_id| msg_id.to_u32())
|
||||
.unwrap_or_log_default(ctx, "Failed to send reaction")
|
||||
})
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_get_msg_reactions(
|
||||
context: *mut dc_context_t,
|
||||
msg_id: u32,
|
||||
) -> *mut dc_reactions_t {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_msg_reactions()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
|
||||
let reactions = if let Ok(reactions) = block_on(get_msg_reactions(ctx, MsgId::new(msg_id)))
|
||||
.log_err(ctx, "failed dc_get_msg_reactions() call")
|
||||
{
|
||||
reactions
|
||||
} else {
|
||||
return ptr::null_mut();
|
||||
};
|
||||
|
||||
Box::into_raw(Box::new(reactions))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_send_webxdc_status_update(
|
||||
context: *mut dc_context_t,
|
||||
@@ -1280,11 +1208,6 @@ pub unsafe extern "C" fn dc_get_chat_media(
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
let chat_id = if chat_id == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(ChatId::new(chat_id))
|
||||
};
|
||||
let msg_type = from_prim(msg_type).expect(&format!("invalid msg_type = {}", msg_type));
|
||||
let or_msg_type2 =
|
||||
from_prim(or_msg_type2).expect(&format!("incorrect or_msg_type2 = {}", or_msg_type2));
|
||||
@@ -1293,10 +1216,16 @@ pub unsafe extern "C" fn dc_get_chat_media(
|
||||
|
||||
block_on(async move {
|
||||
Box::into_raw(Box::new(
|
||||
chat::get_chat_media(ctx, chat_id, msg_type, or_msg_type2, or_msg_type3)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "Failed get_chat_media")
|
||||
.into(),
|
||||
chat::get_chat_media(
|
||||
ctx,
|
||||
ChatId::new(chat_id),
|
||||
msg_type,
|
||||
or_msg_type2,
|
||||
or_msg_type3,
|
||||
)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "Failed get_chat_media")
|
||||
.into(),
|
||||
))
|
||||
})
|
||||
}
|
||||
@@ -2144,10 +2073,7 @@ pub unsafe extern "C" fn dc_delete_contact(
|
||||
block_on(async move {
|
||||
match Contact::delete(ctx, contact_id).await {
|
||||
Ok(_) => 1,
|
||||
Err(err) => {
|
||||
error!(ctx, "cannot delete contact: {}", err);
|
||||
0
|
||||
}
|
||||
Err(_) => 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -2497,7 +2423,7 @@ pub unsafe extern "C" fn dc_array_unref(a: *mut dc_array::dc_array_t) {
|
||||
return;
|
||||
}
|
||||
|
||||
drop(Box::from_raw(a));
|
||||
Box::from_raw(a);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2678,7 +2604,7 @@ pub unsafe extern "C" fn dc_chatlist_unref(chatlist: *mut dc_chatlist_t) {
|
||||
eprintln!("ignoring careless call to dc_chatlist_unref()");
|
||||
return;
|
||||
}
|
||||
drop(Box::from_raw(chatlist));
|
||||
Box::from_raw(chatlist);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2823,7 +2749,7 @@ pub unsafe extern "C" fn dc_chat_unref(chat: *mut dc_chat_t) {
|
||||
return;
|
||||
}
|
||||
|
||||
drop(Box::from_raw(chat));
|
||||
Box::from_raw(chat);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2856,20 +2782,6 @@ pub unsafe extern "C" fn dc_chat_get_name(chat: *mut dc_chat_t) -> *mut libc::c_
|
||||
ffi_chat.chat.get_name().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_get_mailinglist_addr(chat: *mut dc_chat_t) -> *mut libc::c_char {
|
||||
if chat.is_null() {
|
||||
eprintln!("ignoring careless call to dc_chat_get_mailinglist_addr()");
|
||||
return "".strdup();
|
||||
}
|
||||
let ffi_chat = &*chat;
|
||||
ffi_chat
|
||||
.chat
|
||||
.get_mailinglist_addr()
|
||||
.unwrap_or_default()
|
||||
.strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_chat_get_profile_image(chat: *mut dc_chat_t) -> *mut libc::c_char {
|
||||
if chat.is_null() {
|
||||
@@ -3097,7 +3009,7 @@ pub unsafe extern "C" fn dc_msg_unref(msg: *mut dc_msg_t) {
|
||||
return;
|
||||
}
|
||||
|
||||
drop(Box::from_raw(msg));
|
||||
Box::from_raw(msg);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3819,7 +3731,7 @@ pub unsafe extern "C" fn dc_contact_unref(contact: *mut dc_contact_t) {
|
||||
eprintln!("ignoring careless call to dc_contact_unref()");
|
||||
return;
|
||||
}
|
||||
drop(Box::from_raw(contact));
|
||||
Box::from_raw(contact);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3938,16 +3850,6 @@ pub unsafe extern "C" fn dc_contact_get_last_seen(contact: *mut dc_contact_t) ->
|
||||
ffi_contact.contact.last_seen()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_was_seen_recently(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
eprintln!("ignoring careless call to dc_contact_was_seen_recently()");
|
||||
return 0;
|
||||
}
|
||||
let ffi_contact = &*contact;
|
||||
ffi_contact.contact.was_seen_recently() as libc::c_int
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_contact_is_blocked(contact: *mut dc_contact_t) -> libc::c_int {
|
||||
if contact.is_null() {
|
||||
@@ -3983,7 +3885,7 @@ pub unsafe extern "C" fn dc_lot_unref(lot: *mut dc_lot_t) {
|
||||
return;
|
||||
}
|
||||
|
||||
drop(Box::from_raw(lot));
|
||||
Box::from_raw(lot);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -4052,45 +3954,6 @@ pub unsafe extern "C" fn dc_lot_get_timestamp(lot: *mut dc_lot_t) -> i64 {
|
||||
lot.get_timestamp()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_reactions_get_contacts(
|
||||
reactions: *mut dc_reactions_t,
|
||||
) -> *mut dc_array::dc_array_t {
|
||||
if reactions.is_null() {
|
||||
eprintln!("ignoring careless call to dc_reactions_get_contacts()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let reactions = &*reactions;
|
||||
let array: dc_array_t = reactions.contacts().into();
|
||||
|
||||
Box::into_raw(Box::new(array))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_reactions_get_by_contact_id(
|
||||
reactions: *mut dc_reactions_t,
|
||||
contact_id: u32,
|
||||
) -> *mut libc::c_char {
|
||||
if reactions.is_null() {
|
||||
eprintln!("ignoring careless call to dc_reactions_get_by_contact_id()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let reactions = &*reactions;
|
||||
reactions.get(ContactId::new(contact_id)).as_str().strdup()
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_reactions_unref(reactions: *mut dc_reactions_t) {
|
||||
if reactions.is_null() {
|
||||
eprintln!("ignoring careless call to dc_reactions_unref()");
|
||||
return;
|
||||
}
|
||||
|
||||
drop(Box::from_raw(reactions));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_str_unref(s: *mut libc::c_char) {
|
||||
libc::free(s as *mut _)
|
||||
@@ -4314,8 +4177,7 @@ pub unsafe extern "C" fn dc_accounts_get_account(
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(accounts.read())
|
||||
.get_account(id)
|
||||
block_on(async move { accounts.read().await.get_account(id).await })
|
||||
.map(|ctx| Box::into_raw(Box::new(ctx)))
|
||||
.unwrap_or_else(std::ptr::null_mut)
|
||||
}
|
||||
@@ -4330,8 +4192,7 @@ pub unsafe extern "C" fn dc_accounts_get_selected_account(
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(accounts.read())
|
||||
.get_selected_account()
|
||||
block_on(async move { accounts.read().await.get_selected_account().await })
|
||||
.map(|ctx| Box::into_raw(Box::new(ctx)))
|
||||
.unwrap_or_else(std::ptr::null_mut)
|
||||
}
|
||||
@@ -4476,7 +4337,7 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
let list = block_on(accounts.read()).get_all();
|
||||
let list = block_on(async move { accounts.read().await.get_all().await });
|
||||
let array: dc_array_t = list.into();
|
||||
|
||||
Box::into_raw(Box::new(array))
|
||||
@@ -4536,77 +4397,87 @@ pub unsafe extern "C" fn dc_accounts_maybe_network_lost(accounts: *mut dc_accoun
|
||||
block_on(async move { accounts.write().await.maybe_network_lost().await });
|
||||
}
|
||||
|
||||
pub type dc_accounts_event_emitter_t = EventEmitter;
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
||||
accounts: *mut dc_accounts_t,
|
||||
) -> *mut dc_event_emitter_t {
|
||||
) -> *mut dc_accounts_event_emitter_t {
|
||||
if accounts.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_get_event_emitter()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
let emitter = block_on(accounts.read()).get_event_emitter();
|
||||
let emitter = block_on(async move { accounts.read().await.get_event_emitter().await });
|
||||
|
||||
Box::into_raw(Box::new(emitter))
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_event_emitter_unref(
|
||||
emitter: *mut dc_accounts_event_emitter_t,
|
||||
) {
|
||||
if emitter.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_event_emitter_unref()");
|
||||
return;
|
||||
}
|
||||
let _ = Box::from_raw(emitter);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_accounts_get_next_event(
|
||||
emitter: *mut dc_accounts_event_emitter_t,
|
||||
) -> *mut dc_event_t {
|
||||
if emitter.is_null() {
|
||||
eprintln!("ignoring careless call to dc_accounts_get_next_event()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let emitter = &mut *emitter;
|
||||
block_on(emitter.recv())
|
||||
.map(|ev| Box::into_raw(Box::new(ev)))
|
||||
.unwrap_or_else(ptr::null_mut)
|
||||
}
|
||||
|
||||
#[cfg(feature = "jsonrpc")]
|
||||
mod jsonrpc {
|
||||
use super::*;
|
||||
use deltachat_jsonrpc::api::CommandApi;
|
||||
use deltachat_jsonrpc::events::event_to_json_rpc_notification;
|
||||
use deltachat_jsonrpc::api::DeltaChatApiV0;
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||
|
||||
pub struct dc_jsonrpc_instance_t {
|
||||
receiver: OutReceiver,
|
||||
handle: RpcSession<CommandApi>,
|
||||
event_thread: JoinHandle<Result<(), anyhow::Error>>,
|
||||
handle: RpcSession<DeltaChatApiV0>,
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
pub unsafe extern "C" fn dc_jsonrpc_init(
|
||||
account_manager: *mut dc_accounts_t,
|
||||
api_version: *const libc::c_char,
|
||||
) -> *mut dc_jsonrpc_instance_t {
|
||||
if account_manager.is_null() {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_init()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let api_version = to_string_lossy(api_version);
|
||||
|
||||
let cmd_api =
|
||||
deltachat_jsonrpc::api::CommandApi::from_arc((*account_manager).inner.clone());
|
||||
let rpc_api = match api_version.as_str() {
|
||||
"v0" => {
|
||||
deltachat_jsonrpc::api::DeltaChatApiV0::from_arc((*account_manager).inner.clone())
|
||||
}
|
||||
version => {
|
||||
eprintln!(
|
||||
"Error initializing JSON-RPC API: API version {} is not supported.",
|
||||
version
|
||||
);
|
||||
return ptr::null_mut();
|
||||
}
|
||||
};
|
||||
|
||||
let (request_handle, receiver) = RpcClient::new();
|
||||
let request_handle2 = request_handle.clone();
|
||||
let handle = RpcSession::new(request_handle, cmd_api);
|
||||
let handle = RpcSession::new(request_handle, rpc_api);
|
||||
|
||||
let events = block_on({
|
||||
async {
|
||||
let am = (*account_manager).inner.clone();
|
||||
let ev = am.read().await.get_event_emitter();
|
||||
drop(am);
|
||||
ev
|
||||
}
|
||||
});
|
||||
|
||||
let event_thread = spawn({
|
||||
async move {
|
||||
while let Some(event) = events.recv().await {
|
||||
let event = event_to_json_rpc_notification(event);
|
||||
request_handle2
|
||||
.send_notification("event", Some(event))
|
||||
.await?;
|
||||
}
|
||||
let res: Result<(), anyhow::Error> = Ok(());
|
||||
res
|
||||
}
|
||||
});
|
||||
|
||||
let instance = dc_jsonrpc_instance_t {
|
||||
receiver,
|
||||
handle,
|
||||
event_thread,
|
||||
};
|
||||
let instance = dc_jsonrpc_instance_t { receiver, handle };
|
||||
|
||||
Box::into_raw(Box::new(instance))
|
||||
}
|
||||
@@ -4617,8 +4488,8 @@ mod jsonrpc {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
|
||||
return;
|
||||
}
|
||||
(*jsonrpc_instance).event_thread.abort();
|
||||
drop(Box::from_raw(jsonrpc_instance));
|
||||
|
||||
Box::from_raw(jsonrpc_instance);
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
@@ -51,14 +51,13 @@ impl Lot {
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
|
||||
Qr::Account { domain } => Some(domain),
|
||||
Qr::WebrtcInstance { domain, .. } => Some(domain),
|
||||
Qr::Addr { draft, .. } => draft.as_deref(),
|
||||
Qr::Addr { .. } => None,
|
||||
Qr::Url { url } => Some(url),
|
||||
Qr::Text { text } => Some(text),
|
||||
Qr::WithdrawVerifyContact { .. } => None,
|
||||
Qr::WithdrawVerifyGroup { grpname, .. } => Some(grpname),
|
||||
Qr::ReviveVerifyContact { .. } => None,
|
||||
Qr::ReviveVerifyGroup { grpname, .. } => Some(grpname),
|
||||
Qr::Login { address, .. } => Some(address),
|
||||
},
|
||||
Self::Error(err) => Some(err),
|
||||
}
|
||||
@@ -80,13 +79,7 @@ impl Lot {
|
||||
Some(SummaryPrefix::Username(_username)) => Meaning::Text1Username,
|
||||
Some(SummaryPrefix::Me(_text)) => Meaning::Text1Self,
|
||||
},
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::Addr {
|
||||
draft: Some(_draft),
|
||||
..
|
||||
} => Meaning::Text1Draft,
|
||||
_ => Meaning::None,
|
||||
},
|
||||
Self::Qr(_qr) => Meaning::None,
|
||||
Self::Error(_err) => Meaning::None,
|
||||
}
|
||||
}
|
||||
@@ -109,7 +102,6 @@ impl Lot {
|
||||
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
|
||||
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
|
||||
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
|
||||
Qr::Login { .. } => LotState::QrLogin,
|
||||
},
|
||||
Self::Error(_err) => LotState::QrError,
|
||||
}
|
||||
@@ -126,14 +118,13 @@ impl Lot {
|
||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||
Qr::Account { .. } => Default::default(),
|
||||
Qr::WebrtcInstance { .. } => Default::default(),
|
||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::Addr { contact_id } => contact_id.to_u32(),
|
||||
Qr::Url { .. } => Default::default(),
|
||||
Qr::Text { .. } => Default::default(),
|
||||
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::WithdrawVerifyGroup { .. } => Default::default(),
|
||||
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||
Qr::ReviveVerifyGroup { .. } => Default::default(),
|
||||
Qr::Login { .. } => Default::default(),
|
||||
},
|
||||
Self::Error(_) => Default::default(),
|
||||
}
|
||||
@@ -198,9 +189,6 @@ pub enum LotState {
|
||||
/// text1=groupname
|
||||
QrReviveVerifyGroup = 512,
|
||||
|
||||
/// text1=email_address
|
||||
QrLogin = 520,
|
||||
|
||||
// Message States
|
||||
MsgInFresh = 10,
|
||||
MsgInNoticed = 13,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat-jsonrpc"
|
||||
version = "1.101.0"
|
||||
version = "1.86.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
@@ -20,20 +20,18 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tempfile = "3.3.0"
|
||||
log = "0.4"
|
||||
async-channel = { version = "1.6.1" }
|
||||
futures = { version = "0.3.25" }
|
||||
serde_json = "1.0.87"
|
||||
futures = { version = "0.3.19" }
|
||||
serde_json = "1.0.75"
|
||||
yerpc = { version = "^0.3.1", features = ["anyhow_expose"] }
|
||||
typescript-type-def = { version = "0.5.3", features = ["json_value"] }
|
||||
tokio = { version = "1.21.2" }
|
||||
sanitize-filename = "0.4"
|
||||
walkdir = "2.3.2"
|
||||
tokio = { version = "1.19.2" }
|
||||
|
||||
# optional dependencies
|
||||
axum = { version = "0.5.17", optional = true, features = ["ws"] }
|
||||
env_logger = { version = "0.9.1", optional = true }
|
||||
axum = { version = "0.5.9", optional = true, features = ["ws"] }
|
||||
env_logger = { version = "0.9.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.21.2", features = ["full", "rt-multi-thread"] }
|
||||
tokio = { version = "1.19.2", features = ["full", "rt-multi-thread"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
@@ -4,383 +4,139 @@ use serde_json::{json, Value};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
pub fn event_to_json_rpc_notification(event: Event) -> Value {
|
||||
let id: JSONRPCEventType = event.typ.into();
|
||||
let (field1, field2): (Value, Value) = match &event.typ {
|
||||
// events with a single string in field1
|
||||
EventType::Info(txt)
|
||||
| EventType::SmtpConnected(txt)
|
||||
| EventType::ImapConnected(txt)
|
||||
| EventType::SmtpMessageSent(txt)
|
||||
| EventType::ImapMessageDeleted(txt)
|
||||
| EventType::ImapMessageMoved(txt)
|
||||
| EventType::NewBlobFile(txt)
|
||||
| EventType::DeletedBlobFile(txt)
|
||||
| EventType::Warning(txt)
|
||||
| EventType::Error(txt)
|
||||
| EventType::ErrorSelfNotInGroup(txt) => (json!(txt), Value::Null),
|
||||
EventType::ImexFileWritten(path) => (json!(path.to_str()), Value::Null),
|
||||
// single number
|
||||
EventType::MsgsNoticed(chat_id) | EventType::ChatModified(chat_id) => {
|
||||
(json!(chat_id), Value::Null)
|
||||
}
|
||||
EventType::ImexProgress(progress) => (json!(progress), Value::Null),
|
||||
// both fields contain numbers
|
||||
EventType::MsgsChanged { chat_id, msg_id }
|
||||
| EventType::IncomingMsg { chat_id, msg_id }
|
||||
| EventType::MsgDelivered { chat_id, msg_id }
|
||||
| EventType::MsgFailed { chat_id, msg_id }
|
||||
| EventType::MsgRead { chat_id, msg_id } => (json!(chat_id), json!(msg_id)),
|
||||
EventType::ChatEphemeralTimerModified { chat_id, timer } => (json!(chat_id), json!(timer)),
|
||||
EventType::SecurejoinInviterProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
}
|
||||
| EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => (json!(contact_id), json!(progress)),
|
||||
// field 1 number or null
|
||||
EventType::ContactsChanged(maybe_number) | EventType::LocationChanged(maybe_number) => (
|
||||
match maybe_number {
|
||||
Some(number) => json!(number),
|
||||
None => Value::Null,
|
||||
},
|
||||
Value::Null,
|
||||
),
|
||||
// number and maybe string
|
||||
EventType::ConfigureProgress { progress, comment } => (
|
||||
json!(progress),
|
||||
match comment {
|
||||
Some(content) => json!(content),
|
||||
None => Value::Null,
|
||||
},
|
||||
),
|
||||
EventType::ConnectivityChanged => (Value::Null, Value::Null),
|
||||
EventType::SelfavatarChanged => (Value::Null, Value::Null),
|
||||
EventType::WebxdcStatusUpdate {
|
||||
msg_id,
|
||||
status_update_serial,
|
||||
} => (json!(msg_id), json!(status_update_serial)),
|
||||
};
|
||||
|
||||
let id: EventTypeName = event.typ.into();
|
||||
json!({
|
||||
"event": id,
|
||||
"id": id,
|
||||
"contextId": event.id,
|
||||
"field1": field1,
|
||||
"field2": field2
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(tag = "type", rename = "Event")]
|
||||
pub enum JSONRPCEventType {
|
||||
/// The library-user may write an informational string to the log.
|
||||
///
|
||||
/// This event should *not* be reported to the end-user using a popup or something like
|
||||
/// that.
|
||||
Info {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// Emitted when SMTP connection is established and login was successful.
|
||||
SmtpConnected {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// Emitted when IMAP connection is established and login was successful.
|
||||
ImapConnected {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// Emitted when a message was successfully sent to the SMTP server.
|
||||
SmtpMessageSent {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// Emitted when an IMAP message has been marked as deleted
|
||||
ImapMessageDeleted {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// Emitted when an IMAP message has been moved
|
||||
ImapMessageMoved {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// Emitted when an new file in the $BLOBDIR was created
|
||||
NewBlobFile {
|
||||
file: String,
|
||||
},
|
||||
|
||||
/// Emitted when an file in the $BLOBDIR was deleted
|
||||
DeletedBlobFile {
|
||||
file: String,
|
||||
},
|
||||
|
||||
/// The library-user should write a warning string to the log.
|
||||
///
|
||||
/// This event should *not* be reported to the end-user using a popup or something like
|
||||
/// that.
|
||||
Warning {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// The library-user should report an error to the end-user.
|
||||
///
|
||||
/// As most things are asynchronous, things may go wrong at any time and the user
|
||||
/// should not be disturbed by a dialog or so. Instead, use a bubble or so.
|
||||
///
|
||||
/// However, for ongoing processes (eg. configure())
|
||||
/// or for functions that are expected to fail (eg. autocryptContinueKeyTransfer())
|
||||
/// it might be better to delay showing these events until the function has really
|
||||
/// failed (returned false). It should be sufficient to report only the *last* error
|
||||
/// in a messasge box then.
|
||||
Error {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// An action cannot be performed because the user is not in the group.
|
||||
/// Reported eg. after a call to
|
||||
/// setChatName(), setChatProfileImage(),
|
||||
/// addContactToChat(), removeContactFromChat(),
|
||||
/// and messages sending functions.
|
||||
ErrorSelfNotInGroup {
|
||||
msg: String,
|
||||
},
|
||||
|
||||
/// Messages or chats changed. One or more messages or chats changed for various
|
||||
/// reasons in the database:
|
||||
/// - Messages sent, received or removed
|
||||
/// - Chats created, deleted or archived
|
||||
/// - A draft has been set
|
||||
///
|
||||
/// `chatId` is set if only a single chat is affected by the changes, otherwise 0.
|
||||
/// `msgId` is set if only a single message is affected by the changes, otherwise 0.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgsChanged {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// Reactions for the message changed.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ReactionsChanged {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
contact_id: u32,
|
||||
},
|
||||
|
||||
/// There is a fresh message. Typically, the user will show an notification
|
||||
/// when receiving this message.
|
||||
///
|
||||
/// There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
IncomingMsg {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// Downloading a bunch of messages just finished. This is an experimental
|
||||
/// event to allow the UI to only show one notification per message bunch,
|
||||
/// instead of cluttering the user with many notifications.
|
||||
///
|
||||
/// msg_ids contains the message ids.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
IncomingMsgBunch {
|
||||
msg_ids: Vec<u32>,
|
||||
},
|
||||
|
||||
/// Messages were seen or noticed.
|
||||
/// chat id is always set.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgsNoticed {
|
||||
chat_id: u32,
|
||||
},
|
||||
|
||||
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
||||
/// DC_STATE_OUT_DELIVERED, see `Message.state`.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgDelivered {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
|
||||
/// DC_STATE_OUT_FAILED, see `Message.state`.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgFailed {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
||||
/// DC_STATE_OUT_MDN_RCVD, see `Message.state`.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
MsgRead {
|
||||
chat_id: u32,
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
/// Or the verify state of a chat has changed.
|
||||
/// See setChatName(), setChatProfileImage(), addContactToChat()
|
||||
/// and removeContactFromChat().
|
||||
///
|
||||
/// This event does not include ephemeral timer modification, which
|
||||
/// is a separate event.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChatModified {
|
||||
chat_id: u32,
|
||||
},
|
||||
|
||||
/// Chat ephemeral timer changed.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ChatEphemeralTimerModified {
|
||||
chat_id: u32,
|
||||
timer: u32,
|
||||
},
|
||||
|
||||
/// Contact(s) created, renamed, blocked or deleted.
|
||||
///
|
||||
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ContactsChanged {
|
||||
contact_id: Option<u32>,
|
||||
},
|
||||
|
||||
/// Location of one or more contact has changed.
|
||||
///
|
||||
/// @param data1 (u32) contact_id of the contact for which the location has changed.
|
||||
/// If the locations of several contacts have been changed,
|
||||
/// this parameter is set to `None`.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
LocationChanged {
|
||||
contact_id: Option<u32>,
|
||||
},
|
||||
|
||||
/// Inform about the configuration progress started by configure().
|
||||
ConfigureProgress {
|
||||
/// Progress.
|
||||
///
|
||||
/// 0=error, 1-999=progress in permille, 1000=success and done
|
||||
progress: usize,
|
||||
|
||||
/// Progress comment or error, something to display to the user.
|
||||
comment: Option<String>,
|
||||
},
|
||||
|
||||
/// Inform about the import/export progress started by imex().
|
||||
///
|
||||
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
||||
/// @param data2 0
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ImexProgress {
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
/// A file has been exported. A file has been written by imex().
|
||||
/// This event may be sent multiple times by a single call to imex().
|
||||
///
|
||||
/// A typical purpose for a handler of this event may be to make the file public to some system
|
||||
/// services.
|
||||
///
|
||||
/// @param data2 0
|
||||
#[serde(rename_all = "camelCase")]
|
||||
ImexFileWritten {
|
||||
path: String,
|
||||
},
|
||||
|
||||
/// Progress information of a secure-join handshake from the view of the inviter
|
||||
/// (Alice, the person who shows the QR code).
|
||||
///
|
||||
/// These events are typically sent after a joiner has scanned the QR code
|
||||
/// generated by getChatSecurejoinQrCodeSvg().
|
||||
///
|
||||
/// @param data1 (int) ID of the contact that wants to join.
|
||||
/// @param data2 (int) Progress as:
|
||||
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
||||
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
||||
/// 1000=Protocol finished for this contact.
|
||||
#[serde(rename_all = "camelCase")]
|
||||
SecurejoinInviterProgress {
|
||||
contact_id: u32,
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
/// Progress information of a secure-join handshake from the view of the joiner
|
||||
/// (Bob, the person who scans the QR code).
|
||||
/// The events are typically sent while secureJoin(), which
|
||||
/// may take some time, is executed.
|
||||
/// @param data1 (int) ID of the inviting contact.
|
||||
/// @param data2 (int) Progress as:
|
||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||
#[serde(rename_all = "camelCase")]
|
||||
SecurejoinJoinerProgress {
|
||||
contact_id: u32,
|
||||
progress: usize,
|
||||
},
|
||||
|
||||
/// The connectivity to the server changed.
|
||||
/// This means that you should refresh the connectivity view
|
||||
/// and possibly the connectivtiy HTML; see getConnectivity() and
|
||||
/// getConnectivityHtml() for details.
|
||||
pub enum EventTypeName {
|
||||
Info,
|
||||
SmtpConnected,
|
||||
ImapConnected,
|
||||
SmtpMessageSent,
|
||||
ImapMessageDeleted,
|
||||
ImapMessageMoved,
|
||||
NewBlobFile,
|
||||
DeletedBlobFile,
|
||||
Warning,
|
||||
Error,
|
||||
ErrorSelfNotInGroup,
|
||||
MsgsChanged,
|
||||
IncomingMsg,
|
||||
MsgsNoticed,
|
||||
MsgDelivered,
|
||||
MsgFailed,
|
||||
MsgRead,
|
||||
ChatModified,
|
||||
ChatEphemeralTimerModified,
|
||||
ContactsChanged,
|
||||
LocationChanged,
|
||||
ConfigureProgress,
|
||||
ImexProgress,
|
||||
ImexFileWritten,
|
||||
SecurejoinInviterProgress,
|
||||
SecurejoinJoinerProgress,
|
||||
ConnectivityChanged,
|
||||
|
||||
SelfavatarChanged,
|
||||
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WebxdcStatusUpdate {
|
||||
msg_id: u32,
|
||||
status_update_serial: u32,
|
||||
},
|
||||
|
||||
/// Inform that a message containing a webxdc instance has been deleted
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WebxdcInstanceDeleted {
|
||||
msg_id: u32,
|
||||
},
|
||||
WebxdcStatusUpdate,
|
||||
}
|
||||
|
||||
impl From<EventType> for JSONRPCEventType {
|
||||
impl From<EventType> for EventTypeName {
|
||||
fn from(event: EventType) -> Self {
|
||||
use JSONRPCEventType::*;
|
||||
use EventTypeName::*;
|
||||
match event {
|
||||
EventType::Info(msg) => Info { msg },
|
||||
EventType::SmtpConnected(msg) => SmtpConnected { msg },
|
||||
EventType::ImapConnected(msg) => ImapConnected { msg },
|
||||
EventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
|
||||
EventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
|
||||
EventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
|
||||
EventType::NewBlobFile(file) => NewBlobFile { file },
|
||||
EventType::DeletedBlobFile(file) => DeletedBlobFile { file },
|
||||
EventType::Warning(msg) => Warning { msg },
|
||||
EventType::Error(msg) => Error { msg },
|
||||
EventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg },
|
||||
EventType::MsgsChanged { chat_id, msg_id } => MsgsChanged {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
EventType::ReactionsChanged {
|
||||
chat_id,
|
||||
msg_id,
|
||||
contact_id,
|
||||
} => ReactionsChanged {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
contact_id: contact_id.to_u32(),
|
||||
},
|
||||
EventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
EventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
|
||||
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
|
||||
},
|
||||
EventType::MsgsNoticed(chat_id) => MsgsNoticed {
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
EventType::MsgDelivered { chat_id, msg_id } => MsgDelivered {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
EventType::MsgFailed { chat_id, msg_id } => MsgFailed {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
EventType::MsgRead { chat_id, msg_id } => MsgRead {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
EventType::ChatModified(chat_id) => ChatModified {
|
||||
chat_id: chat_id.to_u32(),
|
||||
},
|
||||
EventType::ChatEphemeralTimerModified { chat_id, timer } => {
|
||||
ChatEphemeralTimerModified {
|
||||
chat_id: chat_id.to_u32(),
|
||||
timer: timer.to_u32(),
|
||||
}
|
||||
}
|
||||
EventType::ContactsChanged(contact) => ContactsChanged {
|
||||
contact_id: contact.map(|c| c.to_u32()),
|
||||
},
|
||||
EventType::LocationChanged(contact) => LocationChanged {
|
||||
contact_id: contact.map(|c| c.to_u32()),
|
||||
},
|
||||
EventType::ConfigureProgress { progress, comment } => {
|
||||
ConfigureProgress { progress, comment }
|
||||
}
|
||||
EventType::ImexProgress(progress) => ImexProgress { progress },
|
||||
EventType::ImexFileWritten(path) => ImexFileWritten {
|
||||
path: path.to_str().unwrap_or_default().to_owned(),
|
||||
},
|
||||
EventType::SecurejoinInviterProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => SecurejoinInviterProgress {
|
||||
contact_id: contact_id.to_u32(),
|
||||
progress,
|
||||
},
|
||||
EventType::SecurejoinJoinerProgress {
|
||||
contact_id,
|
||||
progress,
|
||||
} => SecurejoinJoinerProgress {
|
||||
contact_id: contact_id.to_u32(),
|
||||
progress,
|
||||
},
|
||||
EventType::Info(_) => Info,
|
||||
EventType::SmtpConnected(_) => SmtpConnected,
|
||||
EventType::ImapConnected(_) => ImapConnected,
|
||||
EventType::SmtpMessageSent(_) => SmtpMessageSent,
|
||||
EventType::ImapMessageDeleted(_) => ImapMessageDeleted,
|
||||
EventType::ImapMessageMoved(_) => ImapMessageMoved,
|
||||
EventType::NewBlobFile(_) => NewBlobFile,
|
||||
EventType::DeletedBlobFile(_) => DeletedBlobFile,
|
||||
EventType::Warning(_) => Warning,
|
||||
EventType::Error(_) => Error,
|
||||
EventType::ErrorSelfNotInGroup(_) => ErrorSelfNotInGroup,
|
||||
EventType::MsgsChanged { .. } => MsgsChanged,
|
||||
EventType::IncomingMsg { .. } => IncomingMsg,
|
||||
EventType::MsgsNoticed(_) => MsgsNoticed,
|
||||
EventType::MsgDelivered { .. } => MsgDelivered,
|
||||
EventType::MsgFailed { .. } => MsgFailed,
|
||||
EventType::MsgRead { .. } => MsgRead,
|
||||
EventType::ChatModified(_) => ChatModified,
|
||||
EventType::ChatEphemeralTimerModified { .. } => ChatEphemeralTimerModified,
|
||||
EventType::ContactsChanged(_) => ContactsChanged,
|
||||
EventType::LocationChanged(_) => LocationChanged,
|
||||
EventType::ConfigureProgress { .. } => ConfigureProgress,
|
||||
EventType::ImexProgress(_) => ImexProgress,
|
||||
EventType::ImexFileWritten(_) => ImexFileWritten,
|
||||
EventType::SecurejoinInviterProgress { .. } => SecurejoinInviterProgress,
|
||||
EventType::SecurejoinJoinerProgress { .. } => SecurejoinJoinerProgress,
|
||||
EventType::ConnectivityChanged => ConnectivityChanged,
|
||||
EventType::SelfavatarChanged => SelfavatarChanged,
|
||||
EventType::WebxdcStatusUpdate {
|
||||
msg_id,
|
||||
status_update_serial,
|
||||
} => WebxdcStatusUpdate {
|
||||
msg_id: msg_id.to_u32(),
|
||||
status_update_serial: status_update_serial.to_u32(),
|
||||
},
|
||||
EventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
EventType::WebxdcStatusUpdate { .. } => WebxdcStatusUpdate,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -394,8 +150,7 @@ fn generate_events_ts_types_definition() {
|
||||
root_namespace: None,
|
||||
..typescript_type_def::DefinitionFileOptions::default()
|
||||
};
|
||||
typescript_type_def::write_definition_file::<_, JSONRPCEventType>(&mut buf, options)
|
||||
.unwrap();
|
||||
typescript_type_def::write_definition_file::<_, EventTypeName>(&mut buf, options).unwrap();
|
||||
String::from_utf8(buf).unwrap()
|
||||
};
|
||||
std::fs::write("typescript/generated/events.ts", events).unwrap();
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,13 +1,10 @@
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
|
||||
use anyhow::{anyhow, Result};
|
||||
use deltachat::chat::get_chat_contacts;
|
||||
use deltachat::chat::{Chat, ChatId};
|
||||
use deltachat::constants::Chattype;
|
||||
use deltachat::contact::{Contact, ContactId};
|
||||
use deltachat::context::Context;
|
||||
use num_traits::cast::ToPrimitive;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
@@ -36,8 +33,6 @@ pub struct FullChat {
|
||||
is_muted: bool,
|
||||
ephemeral_timer: u32, //TODO look if there are more important properties in newer core versions
|
||||
can_send: bool,
|
||||
was_seen_recently: bool,
|
||||
mailing_list_address: Option<String>,
|
||||
}
|
||||
|
||||
impl FullChat {
|
||||
@@ -70,25 +65,12 @@ impl FullChat {
|
||||
|
||||
let can_send = chat.can_send(context).await?;
|
||||
|
||||
let was_seen_recently = if chat.get_type() == Chattype::Single {
|
||||
match contact_ids.get(0) {
|
||||
Some(contact) => Contact::load_from_db(context, *contact)
|
||||
.await?
|
||||
.was_seen_recently(),
|
||||
None => false,
|
||||
}
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let mailing_list_address = chat.get_mailinglist_addr().map(|s| s.to_string());
|
||||
|
||||
Ok(FullChat {
|
||||
id: chat_id,
|
||||
name: chat.name.clone(),
|
||||
is_protected: chat.is_protected(),
|
||||
profile_image, //BLOBS ?
|
||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||
archived: chat.get_visibility() == deltachat::chat::ChatVisibility::Archived,
|
||||
chat_type: chat
|
||||
.get_type()
|
||||
.to_u32()
|
||||
@@ -105,109 +87,6 @@ impl FullChat {
|
||||
is_muted: chat.is_muted(),
|
||||
ephemeral_timer,
|
||||
can_send,
|
||||
was_seen_recently,
|
||||
mailing_list_address,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// cheaper version of fullchat, omits:
|
||||
/// - contacts
|
||||
/// - contact_ids
|
||||
/// - fresh_message_counter
|
||||
/// - ephemeral_timer
|
||||
/// - self_in_group
|
||||
/// - was_seen_recently
|
||||
/// - can_send
|
||||
///
|
||||
/// used when you only need the basic metadata of a chat like type, name, profile picture
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BasicChat {
|
||||
id: u32,
|
||||
name: String,
|
||||
is_protected: bool,
|
||||
profile_image: Option<String>, //BLOBS ?
|
||||
archived: bool,
|
||||
chat_type: u32,
|
||||
is_unpromoted: bool,
|
||||
is_self_talk: bool,
|
||||
color: String,
|
||||
is_contact_request: bool,
|
||||
is_device_chat: bool,
|
||||
is_muted: bool,
|
||||
}
|
||||
|
||||
impl BasicChat {
|
||||
pub async fn try_from_dc_chat_id(context: &Context, chat_id: u32) -> Result<Self> {
|
||||
let rust_chat_id = ChatId::new(chat_id);
|
||||
let chat = Chat::load_from_db(context, rust_chat_id).await?;
|
||||
|
||||
let profile_image = match chat.get_profile_image(context).await? {
|
||||
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||
None => None,
|
||||
};
|
||||
let color = color_int_to_hex_string(chat.get_color(context).await?);
|
||||
|
||||
Ok(BasicChat {
|
||||
id: chat_id,
|
||||
name: chat.name.clone(),
|
||||
is_protected: chat.is_protected(),
|
||||
profile_image, //BLOBS ?
|
||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||
chat_type: chat
|
||||
.get_type()
|
||||
.to_u32()
|
||||
.ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap?
|
||||
is_unpromoted: chat.is_unpromoted(),
|
||||
is_self_talk: chat.is_self_talk(),
|
||||
color,
|
||||
is_contact_request: chat.is_contact_request(),
|
||||
is_device_chat: chat.is_device_talk(),
|
||||
is_muted: chat.is_muted(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, TypeDef)]
|
||||
pub enum MuteDuration {
|
||||
NotMuted,
|
||||
Forever,
|
||||
Until(i64),
|
||||
}
|
||||
|
||||
impl MuteDuration {
|
||||
pub fn try_into_core_type(self) -> Result<chat::MuteDuration> {
|
||||
match self {
|
||||
MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted),
|
||||
MuteDuration::Forever => Ok(chat::MuteDuration::Forever),
|
||||
MuteDuration::Until(n) => {
|
||||
if n <= 0 {
|
||||
bail!("failed to read mute duration")
|
||||
}
|
||||
|
||||
Ok(SystemTime::now()
|
||||
.checked_add(Duration::from_secs(n as u64))
|
||||
.map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, TypeDef)]
|
||||
#[serde(rename = "ChatVisibility")]
|
||||
pub enum JSONRPCChatVisibility {
|
||||
Normal,
|
||||
Archived,
|
||||
Pinned,
|
||||
}
|
||||
|
||||
impl JSONRPCChatVisibility {
|
||||
pub fn into_core_type(self) -> ChatVisibility {
|
||||
match self {
|
||||
JSONRPCChatVisibility::Normal => ChatVisibility::Normal,
|
||||
JSONRPCChatVisibility::Archived => ChatVisibility::Archived,
|
||||
JSONRPCChatVisibility::Pinned => ChatVisibility::Pinned,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use deltachat::constants::*;
|
||||
use deltachat::contact::{Contact, ContactId};
|
||||
use deltachat::contact::ContactId;
|
||||
use deltachat::{
|
||||
chat::{get_chat_contacts, ChatVisibility},
|
||||
chatlist::Chatlist,
|
||||
@@ -42,11 +42,8 @@ pub enum ChatListItemFetchResult {
|
||||
is_pinned: bool,
|
||||
is_muted: bool,
|
||||
is_contact_request: bool,
|
||||
/// true when chat is a broadcastlist
|
||||
is_broadcast: bool,
|
||||
/// contact id if this is a dm chat (for view profile entry in context menu)
|
||||
dm_chat_contact: Option<u32>,
|
||||
was_seen_recently: bool,
|
||||
},
|
||||
ArchiveLink,
|
||||
#[serde(rename_all = "camelCase")]
|
||||
@@ -95,20 +92,10 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
|
||||
let self_in_group = chat_contacts.contains(&ContactId::SELF);
|
||||
|
||||
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
|
||||
let contact = chat_contacts.get(0);
|
||||
let was_seen_recently = match contact {
|
||||
Some(contact) => Contact::load_from_db(ctx, *contact)
|
||||
.await?
|
||||
.was_seen_recently(),
|
||||
None => false,
|
||||
};
|
||||
(
|
||||
contact.map(|contact_id| contact_id.to_u32()),
|
||||
was_seen_recently,
|
||||
)
|
||||
let dm_chat_contact = if chat.get_type() == Chattype::Single {
|
||||
chat_contacts.get(0).map(|contact_id| contact_id.to_u32())
|
||||
} else {
|
||||
(None, false)
|
||||
None
|
||||
};
|
||||
|
||||
let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?;
|
||||
@@ -134,8 +121,6 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
is_pinned: visibility == ChatVisibility::Pinned,
|
||||
is_muted: chat.is_muted(),
|
||||
is_contact_request: chat.is_contact_request(),
|
||||
is_broadcast: chat.get_type() == Chattype::Broadcast,
|
||||
dm_chat_contact,
|
||||
was_seen_recently,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -20,9 +20,6 @@ pub struct ContactObject {
|
||||
name_and_addr: String,
|
||||
is_blocked: bool,
|
||||
is_verified: bool,
|
||||
/// the contact's last seen timestamp
|
||||
last_seen: i64,
|
||||
was_seen_recently: bool,
|
||||
}
|
||||
|
||||
impl ContactObject {
|
||||
@@ -48,8 +45,6 @@ impl ContactObject {
|
||||
name_and_addr: contact.get_name_n_addr(),
|
||||
is_blocked: contact.is_blocked(),
|
||||
is_verified,
|
||||
last_seen: contact.last_seen(),
|
||||
was_seen_recently: contact.was_seen_recently(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
use deltachat::location::Location;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename = "Location", rename_all = "camelCase")]
|
||||
pub struct JsonrpcLocation {
|
||||
pub location_id: u32,
|
||||
pub is_independent: bool,
|
||||
pub latitude: f64,
|
||||
pub longitude: f64,
|
||||
pub accuracy: f64,
|
||||
pub timestamp: i64,
|
||||
pub contact_id: u32,
|
||||
pub msg_id: u32,
|
||||
pub chat_id: u32,
|
||||
pub marker: Option<String>,
|
||||
}
|
||||
|
||||
impl From<Location> for JsonrpcLocation {
|
||||
fn from(location: Location) -> Self {
|
||||
let Location {
|
||||
location_id,
|
||||
independent,
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
timestamp,
|
||||
contact_id,
|
||||
msg_id,
|
||||
chat_id,
|
||||
marker,
|
||||
} = location;
|
||||
Self {
|
||||
location_id,
|
||||
is_independent: independent != 0,
|
||||
latitude,
|
||||
longitude,
|
||||
accuracy,
|
||||
timestamp,
|
||||
contact_id: contact_id.to_u32(),
|
||||
msg_id,
|
||||
chat_id: chat_id.to_u32(),
|
||||
marker,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +1,15 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use deltachat::chat::Chat;
|
||||
use deltachat::chat::ChatItem;
|
||||
use deltachat::constants::Chattype;
|
||||
use deltachat::contact::Contact;
|
||||
use deltachat::context::Context;
|
||||
use deltachat::download;
|
||||
use deltachat::message::Message;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::message::Viewtype;
|
||||
use deltachat::reaction::get_msg_reactions;
|
||||
use num_traits::cast::ToPrimitive;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
use super::contact::ContactObject;
|
||||
use super::reactions::JSONRPCReactions;
|
||||
use super::webxdc::WebxdcMessageInfo;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename = "Message", rename_all = "camelCase")]
|
||||
@@ -25,9 +17,8 @@ pub struct MessageObject {
|
||||
id: u32,
|
||||
chat_id: u32,
|
||||
from_id: u32,
|
||||
quote: Option<MessageQuote>,
|
||||
parent_id: Option<u32>,
|
||||
|
||||
quoted_text: Option<String>,
|
||||
quoted_message_id: Option<u32>,
|
||||
text: Option<String>,
|
||||
has_location: bool,
|
||||
has_html: bool,
|
||||
@@ -45,8 +36,6 @@ pub struct MessageObject {
|
||||
is_setupmessage: bool,
|
||||
is_info: bool,
|
||||
is_forwarded: bool,
|
||||
/// when is_info is true this describes what type of system message it is
|
||||
system_message_type: SystemMessageType,
|
||||
|
||||
duration: i32,
|
||||
dimensions_height: i32,
|
||||
@@ -64,101 +53,29 @@ pub struct MessageObject {
|
||||
file_mime: Option<String>,
|
||||
file_bytes: u64,
|
||||
file_name: Option<String>,
|
||||
|
||||
webxdc_info: Option<WebxdcMessageInfo>,
|
||||
|
||||
download_state: DownloadState,
|
||||
|
||||
reactions: Option<JSONRPCReactions>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(tag = "kind")]
|
||||
enum MessageQuote {
|
||||
JustText {
|
||||
text: String,
|
||||
},
|
||||
#[serde(rename_all = "camelCase")]
|
||||
WithMessage {
|
||||
text: String,
|
||||
message_id: u32,
|
||||
author_display_name: String,
|
||||
author_display_color: String,
|
||||
override_sender_name: Option<String>,
|
||||
image: Option<String>,
|
||||
is_forwarded: bool,
|
||||
view_type: MessageViewtype,
|
||||
},
|
||||
}
|
||||
|
||||
impl MessageObject {
|
||||
pub async fn from_message_id(context: &Context, message_id: u32) -> Result<Self> {
|
||||
let msg_id = MsgId::new(message_id);
|
||||
Self::from_msg_id(context, msg_id).await
|
||||
}
|
||||
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||
let message = Message::load_from_db(context, msg_id).await?;
|
||||
|
||||
let quoted_message_id = message
|
||||
.quoted_message(context)
|
||||
.await?
|
||||
.map(|m| m.get_id().to_u32());
|
||||
|
||||
let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?;
|
||||
let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?;
|
||||
let file_bytes = message.get_filebytes(context).await;
|
||||
let override_sender_name = message.get_override_sender_name();
|
||||
|
||||
let webxdc_info = if message.get_viewtype() == Viewtype::Webxdc {
|
||||
Some(WebxdcMessageInfo::get_for_message(context, msg_id).await?)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let parent_id = message.parent(context).await?.map(|m| m.get_id().to_u32());
|
||||
|
||||
let download_state = message.download_state().into();
|
||||
|
||||
let quote = if let Some(quoted_text) = message.quoted_text() {
|
||||
match message.quoted_message(context).await? {
|
||||
Some(quote) => {
|
||||
let quote_author = Contact::load_from_db(context, quote.get_from_id()).await?;
|
||||
Some(MessageQuote::WithMessage {
|
||||
text: quoted_text,
|
||||
message_id: quote.get_id().to_u32(),
|
||||
author_display_name: quote_author.get_display_name().to_owned(),
|
||||
author_display_color: color_int_to_hex_string(quote_author.get_color()),
|
||||
override_sender_name: quote.get_override_sender_name(),
|
||||
image: if quote.get_viewtype() == Viewtype::Image
|
||||
|| quote.get_viewtype() == Viewtype::Gif
|
||||
|| quote.get_viewtype() == Viewtype::Sticker
|
||||
{
|
||||
match quote.get_file(context) {
|
||||
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||
None => None,
|
||||
}
|
||||
} else {
|
||||
None
|
||||
},
|
||||
is_forwarded: quote.is_forwarded(),
|
||||
view_type: quote.get_viewtype().into(),
|
||||
})
|
||||
}
|
||||
None => Some(MessageQuote::JustText { text: quoted_text }),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let reactions = get_msg_reactions(context, msg_id).await?;
|
||||
let reactions = if reactions.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(reactions.into())
|
||||
};
|
||||
|
||||
Ok(MessageObject {
|
||||
id: msg_id.to_u32(),
|
||||
id: message_id,
|
||||
chat_id: message.get_chat_id().to_u32(),
|
||||
from_id: message.get_from_id().to_u32(),
|
||||
quote,
|
||||
parent_id,
|
||||
quoted_text: message.quoted_text(),
|
||||
quoted_message_id,
|
||||
text: message.get_text(),
|
||||
has_location: message.has_location(),
|
||||
has_html: message.has_html(),
|
||||
@@ -178,7 +95,6 @@ impl MessageObject {
|
||||
is_setupmessage: message.is_setupmessage(),
|
||||
is_info: message.is_info(),
|
||||
is_forwarded: message.is_forwarded(),
|
||||
system_message_type: message.get_info_type().into(),
|
||||
|
||||
duration: message.get_duration(),
|
||||
dimensions_height: message.get_height(),
|
||||
@@ -205,11 +121,6 @@ impl MessageObject {
|
||||
file_mime: message.get_filemime(),
|
||||
file_bytes,
|
||||
file_name: message.get_filename(),
|
||||
webxdc_info,
|
||||
|
||||
download_state,
|
||||
|
||||
reactions,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -289,200 +200,3 @@ impl From<MessageViewtype> for Viewtype {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
pub enum DownloadState {
|
||||
Done,
|
||||
Available,
|
||||
Failure,
|
||||
InProgress,
|
||||
}
|
||||
|
||||
impl From<download::DownloadState> for DownloadState {
|
||||
fn from(state: download::DownloadState) -> Self {
|
||||
match state {
|
||||
download::DownloadState::Done => DownloadState::Done,
|
||||
download::DownloadState::Available => DownloadState::Available,
|
||||
download::DownloadState::Failure => DownloadState::Failure,
|
||||
download::DownloadState::InProgress => DownloadState::InProgress,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
pub enum SystemMessageType {
|
||||
Unknown,
|
||||
GroupNameChanged,
|
||||
GroupImageChanged,
|
||||
MemberAddedToGroup,
|
||||
MemberRemovedFromGroup,
|
||||
AutocryptSetupMessage,
|
||||
SecurejoinMessage,
|
||||
LocationStreamingEnabled,
|
||||
LocationOnly,
|
||||
|
||||
/// Chat ephemeral message timer is changed.
|
||||
EphemeralTimerChanged,
|
||||
|
||||
// Chat protection state changed
|
||||
ChatProtectionEnabled,
|
||||
ChatProtectionDisabled,
|
||||
|
||||
/// Self-sent-message that contains only json used for multi-device-sync;
|
||||
/// if possible, we attach that to other messages as for locations.
|
||||
MultiDeviceSync,
|
||||
|
||||
// Sync message that contains a json payload
|
||||
// sent to the other webxdc instances
|
||||
// These messages are not shown in the chat.
|
||||
WebxdcStatusUpdate,
|
||||
|
||||
/// Webxdc info added with `info` set in `send_webxdc_status_update()`.
|
||||
WebxdcInfoMessage,
|
||||
}
|
||||
|
||||
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||
fn from(system_message_type: deltachat::mimeparser::SystemMessage) -> Self {
|
||||
use deltachat::mimeparser::SystemMessage;
|
||||
match system_message_type {
|
||||
SystemMessage::Unknown => SystemMessageType::Unknown,
|
||||
SystemMessage::GroupNameChanged => SystemMessageType::GroupNameChanged,
|
||||
SystemMessage::GroupImageChanged => SystemMessageType::GroupImageChanged,
|
||||
SystemMessage::MemberAddedToGroup => SystemMessageType::MemberAddedToGroup,
|
||||
SystemMessage::MemberRemovedFromGroup => SystemMessageType::MemberRemovedFromGroup,
|
||||
SystemMessage::AutocryptSetupMessage => SystemMessageType::AutocryptSetupMessage,
|
||||
SystemMessage::SecurejoinMessage => SystemMessageType::SecurejoinMessage,
|
||||
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
|
||||
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
|
||||
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
|
||||
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
|
||||
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
|
||||
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
||||
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
|
||||
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageNotificationInfo {
|
||||
id: u32,
|
||||
chat_id: u32,
|
||||
account_id: u32,
|
||||
|
||||
image: Option<String>,
|
||||
image_mime_type: Option<String>,
|
||||
|
||||
chat_name: String,
|
||||
chat_profile_image: Option<String>,
|
||||
|
||||
/// also known as summary_text1
|
||||
summary_prefix: Option<String>,
|
||||
/// also known as summary_text2
|
||||
summary_text: String,
|
||||
}
|
||||
|
||||
impl MessageNotificationInfo {
|
||||
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 image = if matches!(
|
||||
message.get_viewtype(),
|
||||
Viewtype::Image | Viewtype::Gif | Viewtype::Sticker
|
||||
) {
|
||||
message
|
||||
.get_file(context)
|
||||
.map(|path_buf| path_buf.to_str().map(|s| s.to_owned()))
|
||||
.unwrap_or_default()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let chat_profile_image = chat
|
||||
.get_profile_image(context)
|
||||
.await?
|
||||
.map(|path_buf| path_buf.to_str().map(|s| s.to_owned()))
|
||||
.unwrap_or_default();
|
||||
|
||||
let summary = message.get_summary(context, Some(&chat)).await?;
|
||||
|
||||
Ok(MessageNotificationInfo {
|
||||
id: msg_id.to_u32(),
|
||||
chat_id: message.get_chat_id().to_u32(),
|
||||
account_id: context.get_id(),
|
||||
image,
|
||||
image_mime_type: message.get_filemime(),
|
||||
chat_name: chat.name,
|
||||
chat_profile_image,
|
||||
summary_prefix: summary.prefix.map(|s| s.to_string()),
|
||||
summary_text: summary.text,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct MessageSearchResult {
|
||||
id: u32,
|
||||
author_profile_image: Option<String>,
|
||||
author_name: String,
|
||||
author_color: String,
|
||||
chat_name: Option<String>,
|
||||
message: String,
|
||||
timestamp: i64,
|
||||
}
|
||||
|
||||
impl MessageSearchResult {
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||
let message = Message::load_from_db(context, msg_id).await?;
|
||||
let chat = Chat::load_from_db(context, message.get_chat_id()).await?;
|
||||
let sender = Contact::load_from_db(context, message.get_from_id()).await?;
|
||||
|
||||
let profile_image = match sender.get_profile_image(context).await? {
|
||||
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||
None => None,
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
id: msg_id.to_u32(),
|
||||
author_profile_image: profile_image,
|
||||
author_name: sender.get_display_name().to_owned(),
|
||||
author_color: color_int_to_hex_string(sender.get_color()),
|
||||
chat_name: if chat.get_type() == Chattype::Single {
|
||||
Some(chat.get_name().to_owned())
|
||||
} else {
|
||||
None
|
||||
},
|
||||
message: message.get_text().unwrap_or_default(),
|
||||
timestamp: message.get_timestamp(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")]
|
||||
pub enum JSONRPCMessageListItem {
|
||||
Message {
|
||||
msg_id: u32,
|
||||
},
|
||||
|
||||
/// Day marker, separating messages that correspond to different
|
||||
/// days according to local time.
|
||||
DayMarker {
|
||||
/// Marker timestamp, for day markers, in unix milliseconds
|
||||
timestamp: i64,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<ChatItem> for JSONRPCMessageListItem {
|
||||
fn from(item: ChatItem) -> Self {
|
||||
match item {
|
||||
ChatItem::Message { msg_id } => JSONRPCMessageListItem::Message {
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
ChatItem::DayMarker { timestamp } => JSONRPCMessageListItem::DayMarker { timestamp },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,8 @@ pub mod account;
|
||||
pub mod chat;
|
||||
pub mod chat_list;
|
||||
pub mod contact;
|
||||
pub mod location;
|
||||
pub mod message;
|
||||
pub mod provider_info;
|
||||
pub mod qr;
|
||||
pub mod reactions;
|
||||
pub mod webxdc;
|
||||
|
||||
pub fn color_int_to_hex_string(color: u32) -> String {
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
use deltachat::qr::Qr;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||
#[serde(tag = "type")]
|
||||
pub enum QrObject {
|
||||
AskVerifyContact {
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
AskVerifyGroup {
|
||||
grpname: String,
|
||||
grpid: String,
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
FprOk {
|
||||
contact_id: u32,
|
||||
},
|
||||
FprMismatch {
|
||||
contact_id: Option<u32>,
|
||||
},
|
||||
FprWithoutAddr {
|
||||
fingerprint: String,
|
||||
},
|
||||
Account {
|
||||
domain: String,
|
||||
},
|
||||
WebrtcInstance {
|
||||
domain: String,
|
||||
instance_pattern: String,
|
||||
},
|
||||
Addr {
|
||||
contact_id: u32,
|
||||
draft: Option<String>,
|
||||
},
|
||||
Url {
|
||||
url: String,
|
||||
},
|
||||
Text {
|
||||
text: String,
|
||||
},
|
||||
WithdrawVerifyContact {
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
WithdrawVerifyGroup {
|
||||
grpname: String,
|
||||
grpid: String,
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
ReviveVerifyContact {
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
ReviveVerifyGroup {
|
||||
grpname: String,
|
||||
grpid: String,
|
||||
contact_id: u32,
|
||||
fingerprint: String,
|
||||
invitenumber: String,
|
||||
authcode: String,
|
||||
},
|
||||
Login {
|
||||
address: String,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<Qr> for QrObject {
|
||||
fn from(qr: Qr) -> Self {
|
||||
match qr {
|
||||
Qr::AskVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::AskVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::AskVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::AskVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::FprOk { contact_id } => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
QrObject::FprOk { contact_id }
|
||||
}
|
||||
Qr::FprMismatch { contact_id } => {
|
||||
let contact_id = contact_id.map(|contact_id| contact_id.to_u32());
|
||||
QrObject::FprMismatch { contact_id }
|
||||
}
|
||||
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
|
||||
Qr::Account { domain } => QrObject::Account { domain },
|
||||
Qr::WebrtcInstance {
|
||||
domain,
|
||||
instance_pattern,
|
||||
} => QrObject::WebrtcInstance {
|
||||
domain,
|
||||
instance_pattern,
|
||||
},
|
||||
Qr::Addr { contact_id, draft } => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
QrObject::Addr { contact_id, draft }
|
||||
}
|
||||
Qr::Url { url } => QrObject::Url { url },
|
||||
Qr::Text { text } => QrObject::Text { text },
|
||||
Qr::WithdrawVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::WithdrawVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::WithdrawVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::WithdrawVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::ReviveVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::ReviveVerifyContact {
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::ReviveVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
} => {
|
||||
let contact_id = contact_id.to_u32();
|
||||
let fingerprint = fingerprint.to_string();
|
||||
QrObject::ReviveVerifyGroup {
|
||||
grpname,
|
||||
grpid,
|
||||
contact_id,
|
||||
fingerprint,
|
||||
invitenumber,
|
||||
authcode,
|
||||
}
|
||||
}
|
||||
Qr::Login { address, .. } => QrObject::Login { address },
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use deltachat::reaction::Reactions;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
/// Structure representing all reactions to a particular message.
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename = "Reactions", rename_all = "camelCase")]
|
||||
pub struct JSONRPCReactions {
|
||||
/// Map from a contact to it's reaction to message.
|
||||
reactions_by_contact: BTreeMap<u32, Vec<String>>,
|
||||
/// Unique reactions and their count
|
||||
reactions: BTreeMap<String, u32>,
|
||||
}
|
||||
|
||||
impl From<Reactions> for JSONRPCReactions {
|
||||
fn from(reactions: Reactions) -> Self {
|
||||
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
|
||||
let mut unique_reactions: BTreeMap<String, u32> = BTreeMap::new();
|
||||
|
||||
for contact_id in reactions.contacts() {
|
||||
let reaction = reactions.get(contact_id);
|
||||
if reaction.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let emojis: Vec<String> = reaction
|
||||
.emojis()
|
||||
.into_iter()
|
||||
.map(|emoji| emoji.to_owned())
|
||||
.collect();
|
||||
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
|
||||
for emoji in emojis {
|
||||
if let Some(x) = unique_reactions.get_mut(&emoji) {
|
||||
*x += 1;
|
||||
} else {
|
||||
unique_reactions.insert(emoji, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
JSONRPCReactions {
|
||||
reactions_by_contact,
|
||||
reactions: unique_reactions,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,8 +33,6 @@ pub struct WebxdcMessageInfo {
|
||||
/// defaults to an empty string.
|
||||
/// Implementations may offer an menu or a button to open this URL.
|
||||
source_code_url: Option<String>,
|
||||
/// True if full internet access should be granted to the app.
|
||||
internet_access: bool,
|
||||
}
|
||||
|
||||
impl WebxdcMessageInfo {
|
||||
@@ -49,7 +47,6 @@ impl WebxdcMessageInfo {
|
||||
document,
|
||||
summary,
|
||||
source_code_url,
|
||||
internet_access,
|
||||
} = message.get_webxdc_info(context).await?;
|
||||
|
||||
Ok(Self {
|
||||
@@ -58,7 +55,6 @@ impl WebxdcMessageInfo {
|
||||
document: maybe_empty_string_to_option(document),
|
||||
summary: maybe_empty_string_to_option(summary),
|
||||
source_code_url: maybe_empty_string_to_option(source_code_url),
|
||||
internet_access,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
pub mod api;
|
||||
pub use api::events;
|
||||
pub use api::{Accounts, DeltaChatApiV0};
|
||||
pub use yerpc;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::api::{Accounts, CommandApi};
|
||||
use super::api::{Accounts, DeltaChatApiV0};
|
||||
use async_channel::unbounded;
|
||||
use futures::StreamExt;
|
||||
use tempfile::TempDir;
|
||||
@@ -14,7 +15,7 @@ mod tests {
|
||||
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new().unwrap().path().into();
|
||||
let accounts = Accounts::new(tmp_dir).await?;
|
||||
let api = CommandApi::new(accounts);
|
||||
let api = DeltaChatApiV0::new(accounts);
|
||||
|
||||
let (sender, mut receiver) = unbounded::<String>();
|
||||
|
||||
@@ -55,7 +56,7 @@ mod tests {
|
||||
async fn test_batch_set_config() -> anyhow::Result<()> {
|
||||
let tmp_dir = TempDir::new().unwrap().path().into();
|
||||
let accounts = Accounts::new(tmp_dir).await?;
|
||||
let api = CommandApi::new(accounts);
|
||||
let api = DeltaChatApiV0::new(accounts);
|
||||
|
||||
let (sender, mut receiver) = unbounded::<String>();
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ use yerpc::{RpcClient, RpcSession};
|
||||
|
||||
mod api;
|
||||
use api::events::event_to_json_rpc_notification;
|
||||
use api::{Accounts, CommandApi};
|
||||
use api::{Accounts, DeltaChatApiV0};
|
||||
|
||||
const DEFAULT_PORT: u16 = 20808;
|
||||
|
||||
@@ -20,10 +20,10 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
.unwrap_or(DEFAULT_PORT);
|
||||
log::info!("Starting with accounts directory `{path}`.");
|
||||
let accounts = Accounts::new(PathBuf::from(&path)).await.unwrap();
|
||||
let state = CommandApi::new(accounts);
|
||||
let state = DeltaChatApiV0::new(accounts);
|
||||
|
||||
let app = Router::new()
|
||||
.route("/ws", get(handler))
|
||||
.route("/rpc/v0", get(handler))
|
||||
.layer(Extension(state.clone()));
|
||||
|
||||
tokio::spawn(async move {
|
||||
@@ -40,11 +40,11 @@ async fn main() -> Result<(), std::io::Error> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response {
|
||||
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<DeltaChatApiV0>) -> Response {
|
||||
let (client, out_receiver) = RpcClient::new();
|
||||
let session = RpcSession::new(client.clone(), api.clone());
|
||||
tokio::spawn(async move {
|
||||
let events = api.accounts.read().await.get_event_emitter();
|
||||
let events = api.accounts.read().await.get_event_emitter().await;
|
||||
while let Some(event) = events.recv().await {
|
||||
let event = event_to_json_rpc_notification(event);
|
||||
client.send_notification("event", Some(event)).await.ok();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DcEvent, DeltaChat } from "../deltachat.js";
|
||||
import { DeltaChat, DeltaChatEvent } from "../deltachat.js";
|
||||
|
||||
var SELECTED_ACCOUNT = 0;
|
||||
|
||||
@@ -7,7 +7,7 @@ window.addEventListener("DOMContentLoaded", (_event) => {
|
||||
SELECTED_ACCOUNT = Number(id);
|
||||
window.dispatchEvent(new Event("account-changed"));
|
||||
};
|
||||
console.log("launch run script...");
|
||||
console.log('launch run script...')
|
||||
run().catch((err) => console.error("run failed", err));
|
||||
});
|
||||
|
||||
@@ -16,13 +16,13 @@ async function run() {
|
||||
const $side = document.getElementById("side")!;
|
||||
const $head = document.getElementById("header")!;
|
||||
|
||||
const client = new DeltaChat("ws://localhost:20808/ws");
|
||||
const client = new DeltaChat('ws://localhost:20808/ws')
|
||||
|
||||
(window as any).client = client.rpc;
|
||||
;(window as any).client = client.rpc;
|
||||
|
||||
client.on("ALL", (accountId, event) => {
|
||||
onIncomingEvent(accountId, event);
|
||||
});
|
||||
client.on("ALL", event => {
|
||||
onIncomingEvent(event)
|
||||
})
|
||||
|
||||
window.addEventListener("account-changed", async (_event: Event) => {
|
||||
listChatsForSelectedAccount();
|
||||
@@ -31,9 +31,9 @@ async function run() {
|
||||
await Promise.all([loadAccountsInHeader(), listChatsForSelectedAccount()]);
|
||||
|
||||
async function loadAccountsInHeader() {
|
||||
console.log("load accounts");
|
||||
console.log('load accounts')
|
||||
const accounts = await client.rpc.getAllAccounts();
|
||||
console.log("accounts loaded", accounts);
|
||||
console.log('accounts loaded', accounts)
|
||||
for (const account of accounts) {
|
||||
if (account.type === "Configured") {
|
||||
write(
|
||||
@@ -48,14 +48,14 @@ async function run() {
|
||||
`<a href="#">
|
||||
${account.id}: (unconfigured)
|
||||
</a> `
|
||||
);
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function listChatsForSelectedAccount() {
|
||||
clear($main);
|
||||
const selectedAccount = SELECTED_ACCOUNT;
|
||||
const selectedAccount = SELECTED_ACCOUNT
|
||||
const info = await client.rpc.getAccountInfo(selectedAccount);
|
||||
if (info.type !== "Configured") {
|
||||
return write($main, "Account is not configured");
|
||||
@@ -68,17 +68,17 @@ async function run() {
|
||||
null
|
||||
);
|
||||
for (const [chatId, _messageId] of chats) {
|
||||
const chat = await client.rpc.getFullChatById(
|
||||
const chat = await client.rpc.chatlistGetFullChatById(
|
||||
selectedAccount,
|
||||
chatId
|
||||
);
|
||||
write($main, `<h3>${chat.name}</h3>`);
|
||||
const messageIds = await client.rpc.getMessageIds(
|
||||
const messageIds = await client.rpc.messageListGetMessageIds(
|
||||
selectedAccount,
|
||||
chatId,
|
||||
0
|
||||
);
|
||||
const messages = await client.rpc.getMessages(
|
||||
const messages = await client.rpc.messageGetMessages(
|
||||
selectedAccount,
|
||||
messageIds
|
||||
);
|
||||
@@ -88,15 +88,14 @@ async function run() {
|
||||
}
|
||||
}
|
||||
|
||||
function onIncomingEvent(accountId: number, event: DcEvent) {
|
||||
function onIncomingEvent(event: DeltaChatEvent) {
|
||||
write(
|
||||
$side,
|
||||
`
|
||||
<p class="message">
|
||||
[<strong>${event.type}</strong> on account ${accountId}]<br>
|
||||
<em>f1:</em> ${JSON.stringify(
|
||||
Object.assign({}, event, { type: undefined })
|
||||
)}
|
||||
[<strong>${event.id}</strong> on account ${event.contextId}]<br>
|
||||
<em>f1:</em> ${JSON.stringify(event.field1)}<br>
|
||||
<em>f2:</em> ${JSON.stringify(event.field2)}
|
||||
</p>`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,26 +66,6 @@ export class RawClient {
|
||||
return (this._transport.request('get_all_accounts', [] as RPC.Params)) as Promise<(T.Account)[]>;
|
||||
}
|
||||
|
||||
|
||||
public startIoForAllAccounts(): Promise<null> {
|
||||
return (this._transport.request('start_io_for_all_accounts', [] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public stopIoForAllAccounts(): Promise<null> {
|
||||
return (this._transport.request('stop_io_for_all_accounts', [] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public startIo(id: T.U32): Promise<null> {
|
||||
return (this._transport.request('start_io', [id] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public stopIo(id: T.U32): Promise<null> {
|
||||
return (this._transport.request('stop_io', [id] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get top-level info for an account.
|
||||
*/
|
||||
@@ -93,13 +73,6 @@ export class RawClient {
|
||||
return (this._transport.request('get_account_info', [accountId] as RPC.Params)) as Promise<T.Account>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the combined filesize of an account in bytes
|
||||
*/
|
||||
public getAccountFileSize(accountId: T.U32): Promise<T.U64> {
|
||||
return (this._transport.request('get_account_file_size', [accountId] as RPC.Params)) as Promise<T.U64>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns provider for the given domain.
|
||||
*
|
||||
@@ -138,21 +111,18 @@ export class RawClient {
|
||||
|
||||
/**
|
||||
* Set configuration values from a QR code. (technically from the URI that is stored in the qrcode)
|
||||
* Before this function is called, `checkQr()` should confirm the type of the
|
||||
* QR code is `account` or `webrtcInstance`.
|
||||
* Before this function is called, dc_check_qr() should confirm the type of the
|
||||
* QR code is DC_QR_ACCOUNT or DC_QR_WEBRTC_INSTANCE.
|
||||
*
|
||||
* Internally, the function will call dc_set_config() with the appropriate keys,
|
||||
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT
|
||||
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
|
||||
*/
|
||||
public setConfigFromQr(accountId: T.U32, qrContent: string): Promise<null> {
|
||||
return (this._transport.request('set_config_from_qr', [accountId, qrContent] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public checkQr(accountId: T.U32, qrContent: string): Promise<T.Qr> {
|
||||
return (this._transport.request('check_qr', [accountId, qrContent] as RPC.Params)) as Promise<T.Qr>;
|
||||
}
|
||||
|
||||
|
||||
public getConfig(accountId: T.U32, key: string): Promise<(string|null)> {
|
||||
return (this._transport.request('get_config', [accountId, key] as RPC.Params)) as Promise<(string|null)>;
|
||||
}
|
||||
@@ -162,11 +132,6 @@ export class RawClient {
|
||||
return (this._transport.request('batch_get_config', [accountId, keys] as RPC.Params)) as Promise<Record<string,(string|null)>>;
|
||||
}
|
||||
|
||||
|
||||
public setStockStrings(strings: Record<T.U32,string>): Promise<null> {
|
||||
return (this._transport.request('set_stock_strings', [strings] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures this account with the currently set parameters.
|
||||
* Setup the credential config before calling this.
|
||||
@@ -182,16 +147,6 @@ export class RawClient {
|
||||
return (this._transport.request('stop_ongoing_process', [accountId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public exportSelfKeys(accountId: T.U32, path: string, passphrase: (string|null)): Promise<null> {
|
||||
return (this._transport.request('export_self_keys', [accountId, path, passphrase] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public importSelfKeys(accountId: T.U32, path: string, passphrase: (string|null)): Promise<null> {
|
||||
return (this._transport.request('import_self_keys', [accountId, path, passphrase] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the message IDs of all _fresh_ messages of any chat.
|
||||
* Typically used for implementing notification summaries
|
||||
@@ -221,24 +176,14 @@ export class RawClient {
|
||||
return (this._transport.request('get_fresh_msg_cnt', [accountId, chatId] as RPC.Params)) as Promise<T.Usize>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Estimate the number of messages that will be deleted
|
||||
* by the set_config()-options `delete_device_after` or `delete_server_after`.
|
||||
* This is typically used to show the estimated impact to the user
|
||||
* before actually enabling deletion of old messages.
|
||||
*/
|
||||
public estimateAutoDeletionCount(accountId: T.U32, fromServer: boolean, seconds: T.I64): Promise<T.Usize> {
|
||||
return (this._transport.request('estimate_auto_deletion_count', [accountId, fromServer, seconds] as RPC.Params)) as Promise<T.Usize>;
|
||||
|
||||
public autocryptInitiateKeyTransfer(accountId: T.U32): Promise<string> {
|
||||
return (this._transport.request('autocrypt_initiate_key_transfer', [accountId] as RPC.Params)) as Promise<string>;
|
||||
}
|
||||
|
||||
|
||||
public initiateAutocryptKeyTransfer(accountId: T.U32): Promise<string> {
|
||||
return (this._transport.request('initiate_autocrypt_key_transfer', [accountId] as RPC.Params)) as Promise<string>;
|
||||
}
|
||||
|
||||
|
||||
public continueAutocryptKeyTransfer(accountId: T.U32, messageId: T.U32, setupCode: string): Promise<null> {
|
||||
return (this._transport.request('continue_autocrypt_key_transfer', [accountId, messageId, setupCode] as RPC.Params)) as Promise<null>;
|
||||
public autocryptContinueKeyTransfer(accountId: T.U32, messageId: T.U32, setupCode: string): Promise<null> {
|
||||
return (this._transport.request('autocrypt_continue_key_transfer', [accountId, messageId, setupCode] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
@@ -252,16 +197,8 @@ export class RawClient {
|
||||
}
|
||||
|
||||
|
||||
public getFullChatById(accountId: T.U32, chatId: T.U32): Promise<T.FullChat> {
|
||||
return (this._transport.request('get_full_chat_by_id', [accountId, chatId] as RPC.Params)) as Promise<T.FullChat>;
|
||||
}
|
||||
|
||||
/**
|
||||
* get basic info about a chat,
|
||||
* use chatlist_get_full_chat_by_id() instead if you need more information
|
||||
*/
|
||||
public getBasicChatInfo(accountId: T.U32, chatId: T.U32): Promise<T.BasicChat> {
|
||||
return (this._transport.request('get_basic_chat_info', [accountId, chatId] as RPC.Params)) as Promise<T.BasicChat>;
|
||||
public chatlistGetFullChatById(accountId: T.U32, chatId: T.U32): Promise<T.FullChat> {
|
||||
return (this._transport.request('chatlist_get_full_chat_by_id', [accountId, chatId] as RPC.Params)) as Promise<T.FullChat>;
|
||||
}
|
||||
|
||||
|
||||
@@ -274,410 +211,31 @@ export class RawClient {
|
||||
return (this._transport.request('block_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a chat.
|
||||
*
|
||||
* Messages are deleted from the device and the chat database entry is deleted.
|
||||
* After that, the event #DC_EVENT_MSGS_CHANGED is posted.
|
||||
*
|
||||
* Things that are _not done_ implicitly:
|
||||
*
|
||||
* - Messages are **not deleted from the server**.
|
||||
* - The chat or the contact is **not blocked**, so new messages from the user/the group may appear as a contact request
|
||||
* and the user may create the chat again.
|
||||
* - **Groups are not left** - this would
|
||||
* be unexpected as (1) deleting a normal chat also does not prevent new mails
|
||||
* from arriving, (2) leaving a group requires sending a message to
|
||||
* all group members - especially for groups not used for a longer time, this is
|
||||
* really unexpected when deletion results in contacting all members again,
|
||||
* (3) only leaving groups is also a valid usecase.
|
||||
*
|
||||
* To leave a chat explicitly, use leave_group()
|
||||
*/
|
||||
public deleteChat(accountId: T.U32, chatId: T.U32): Promise<null> {
|
||||
return (this._transport.request('delete_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get encryption info for a chat.
|
||||
* Get a multi-line encryption info, containing encryption preferences of all members.
|
||||
* Can be used to find out why messages sent to group are not encrypted.
|
||||
*
|
||||
* returns Multi-line text
|
||||
*/
|
||||
public getChatEncryptionInfo(accountId: T.U32, chatId: T.U32): Promise<string> {
|
||||
return (this._transport.request('get_chat_encryption_info', [accountId, chatId] as RPC.Params)) as Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get QR code (text and SVG) that will offer an Setup-Contact or Verified-Group invitation.
|
||||
* The QR code is compatible to the OPENPGP4FPR format
|
||||
* so that a basic fingerprint comparison also works e.g. with OpenKeychain.
|
||||
*
|
||||
* The scanning device will pass the scanned content to `checkQr()` then;
|
||||
* if `checkQr()` returns `askVerifyContact` or `askVerifyGroup`
|
||||
* an out-of-band-verification can be joined using `secure_join()`
|
||||
*
|
||||
* chat_id: If set to a group-chat-id,
|
||||
* the Verified-Group-Invite protocol is offered in the QR code;
|
||||
* works for protected groups as well as for normal groups.
|
||||
* If not set, the Setup-Contact protocol is offered in the QR code.
|
||||
* See https://countermitm.readthedocs.io/en/latest/new.html
|
||||
* for details about both protocols.
|
||||
*
|
||||
* return format: `[code, svg]`
|
||||
*/
|
||||
public getChatSecurejoinQrCodeSvg(accountId: T.U32, chatId: (T.U32|null)): Promise<[string,string]> {
|
||||
return (this._transport.request('get_chat_securejoin_qr_code_svg', [accountId, chatId] as RPC.Params)) as Promise<[string,string]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Continue a Setup-Contact or Verified-Group-Invite protocol
|
||||
* started on another device with `get_chat_securejoin_qr_code_svg()`.
|
||||
* This function is typically called when `check_qr()` returns
|
||||
* type=AskVerifyContact or type=AskVerifyGroup.
|
||||
*
|
||||
* The function returns immediately and the handshake runs in background,
|
||||
* sending and receiving several messages.
|
||||
* During the handshake, info messages are added to the chat,
|
||||
* showing progress, success or errors.
|
||||
*
|
||||
* Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
||||
*
|
||||
* See https://countermitm.readthedocs.io/en/latest/new.html
|
||||
* for details about both protocols.
|
||||
*
|
||||
* **qr**: The text of the scanned QR code. Typically, the same string as given
|
||||
* to `check_qr()`.
|
||||
*
|
||||
* **returns**: The chat ID of the joined chat, the UI may redirect to the this chat.
|
||||
* A returned chat ID does not guarantee that the chat is protected or the belonging contact is verified.
|
||||
*
|
||||
*/
|
||||
public secureJoin(accountId: T.U32, qr: string): Promise<T.U32> {
|
||||
return (this._transport.request('secure_join', [accountId, qr] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
|
||||
public leaveGroup(accountId: T.U32, chatId: T.U32): Promise<null> {
|
||||
return (this._transport.request('leave_group', [accountId, chatId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a member from a group.
|
||||
*
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* all group members are informed by a special status message that is sent automatically by this function.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*/
|
||||
public removeContactFromChat(accountId: T.U32, chatId: T.U32, contactId: T.U32): Promise<null> {
|
||||
return (this._transport.request('remove_contact_from_chat', [accountId, chatId, contactId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a member to a group.
|
||||
*
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* all group members are informed by a special status message that is sent automatically by this function.
|
||||
*
|
||||
* If the group has group protection enabled, only verified contacts can be added to the group.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*/
|
||||
public addContactToChat(accountId: T.U32, chatId: T.U32, contactId: T.U32): Promise<null> {
|
||||
return (this._transport.request('add_contact_to_chat', [accountId, chatId, contactId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the contact IDs belonging to a chat.
|
||||
*
|
||||
* - for normal chats, the function always returns exactly one contact,
|
||||
* DC_CONTACT_ID_SELF is returned only for SELF-chats.
|
||||
*
|
||||
* - for group chats all members are returned, DC_CONTACT_ID_SELF is returned
|
||||
* explicitly as it may happen that oneself gets removed from a still existing
|
||||
* group
|
||||
*
|
||||
* - for broadcasts, all recipients are returned, DC_CONTACT_ID_SELF is not included
|
||||
*
|
||||
* - for mailing lists, the behavior is not documented currently, we will decide on that later.
|
||||
* for now, the UI should not show the list for mailing lists.
|
||||
* (we do not know all members and there is not always a global mailing list address,
|
||||
* so we could return only SELF or the known members; this is not decided yet)
|
||||
*/
|
||||
public getChatContacts(accountId: T.U32, chatId: T.U32): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('get_chat_contacts', [accountId, chatId] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new group chat.
|
||||
*
|
||||
* After creation,
|
||||
* the group has one member with the ID DC_CONTACT_ID_SELF
|
||||
* and is in _unpromoted_ state.
|
||||
* This means, you can add or remove members, change the name,
|
||||
* the group image and so on without messages being sent to all group members.
|
||||
*
|
||||
* This changes as soon as the first message is sent to the group members
|
||||
* and the group becomes _promoted_.
|
||||
* After that, all changes are synced with all group members
|
||||
* by sending status message.
|
||||
*
|
||||
* To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of `BasicChat` or `FullChat`.
|
||||
* This may be useful if you want to show some help for just created groups.
|
||||
*
|
||||
* @param protect If set to 1 the function creates group with protection initially enabled.
|
||||
* Only verified members are allowed in these groups
|
||||
* and end-to-end-encryption is always enabled.
|
||||
*/
|
||||
public createGroupChat(accountId: T.U32, name: string, protect: boolean): Promise<T.U32> {
|
||||
return (this._transport.request('create_group_chat', [accountId, name, protect] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new broadcast list.
|
||||
*
|
||||
* Broadcast lists are similar to groups on the sending device,
|
||||
* however, recipients get the messages in normal one-to-one chats
|
||||
* and will not be aware of other members.
|
||||
*
|
||||
* Replies to broadcasts go only to the sender
|
||||
* and not to all broadcast recipients.
|
||||
* Moreover, replies will not appear in the broadcast list
|
||||
* but in the one-to-one chat with the person answering.
|
||||
*
|
||||
* The name and the image of the broadcast list is set automatically
|
||||
* and is visible to the sender only.
|
||||
* Not asking for these data allows more focused creation
|
||||
* and we bypass the question who will get which data.
|
||||
* Also, many users will have at most one broadcast list
|
||||
* so, a generic name and image is sufficient at the first place.
|
||||
*
|
||||
* Later on, however, the name can be changed using dc_set_chat_name().
|
||||
* The image cannot be changed to have a unique, recognizable icon in the chat lists.
|
||||
* All in all, this is also what other messengers are doing here.
|
||||
*/
|
||||
public createBroadcastList(accountId: T.U32): Promise<T.U32> {
|
||||
return (this._transport.request('create_broadcast_list', [accountId] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set group name.
|
||||
*
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* all group members are informed by a special status message that is sent automatically by this function.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*/
|
||||
public setChatName(accountId: T.U32, chatId: T.U32, newName: string): Promise<null> {
|
||||
return (this._transport.request('set_chat_name', [accountId, chatId, newName] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set group profile image.
|
||||
*
|
||||
* If the group is already _promoted_ (any message was sent to the group),
|
||||
* all group members are informed by a special status message that is sent automatically by this function.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
||||
*
|
||||
* To find out the profile image of a chat, use dc_chat_get_profile_image()
|
||||
*
|
||||
* @param image_path Full path of the image to use as the group image. The image will immediately be copied to the
|
||||
* `blobdir`; the original image will not be needed anymore.
|
||||
* If you pass null here, the group image is deleted (for promoted groups, all members are informed about
|
||||
* this change anyway).
|
||||
*/
|
||||
public setChatProfileImage(accountId: T.U32, chatId: T.U32, imagePath: (string|null)): Promise<null> {
|
||||
return (this._transport.request('set_chat_profile_image', [accountId, chatId, imagePath] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public setChatVisibility(accountId: T.U32, chatId: T.U32, visibility: T.ChatVisibility): Promise<null> {
|
||||
return (this._transport.request('set_chat_visibility', [accountId, chatId, visibility] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public setChatEphemeralTimer(accountId: T.U32, chatId: T.U32, timer: T.U32): Promise<null> {
|
||||
return (this._transport.request('set_chat_ephemeral_timer', [accountId, chatId, timer] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public getChatEphemeralTimer(accountId: T.U32, chatId: T.U32): Promise<T.U32> {
|
||||
return (this._transport.request('get_chat_ephemeral_timer', [accountId, chatId] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
|
||||
public addDeviceMessage(accountId: T.U32, label: string, text: string): Promise<T.U32> {
|
||||
return (this._transport.request('add_device_message', [accountId, label, text] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark all messages in a chat as _noticed_.
|
||||
* _Noticed_ messages are no longer _fresh_ and do not count as being unseen
|
||||
* but are still waiting for being marked as "seen" using markseen_msgs()
|
||||
* (IMAP/MDNs is not done for noticed messages).
|
||||
*
|
||||
* Calling this function usually results in the event #DC_EVENT_MSGS_NOTICED.
|
||||
* See also markseen_msgs().
|
||||
*/
|
||||
public marknoticedChat(accountId: T.U32, chatId: T.U32): Promise<null> {
|
||||
return (this._transport.request('marknoticed_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
|
||||
|
||||
public messageListGetMessageIds(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('message_list_get_message_ids', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
|
||||
|
||||
public getFirstUnreadMessageOfChat(accountId: T.U32, chatId: T.U32): Promise<(T.U32|null)> {
|
||||
return (this._transport.request('get_first_unread_message_of_chat', [accountId, chatId] as RPC.Params)) as Promise<(T.U32|null)>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set mute duration of a chat.
|
||||
*
|
||||
* The UI can then call is_chat_muted() when receiving a new message
|
||||
* to decide whether it should trigger an notification.
|
||||
*
|
||||
* Muted chats should not sound or vibrate
|
||||
* and should not show a visual notification in the system area.
|
||||
* Moreover, muted chats should be excluded from global badge counter
|
||||
* (get_fresh_msgs() skips muted chats therefore)
|
||||
* and the in-app, per-chat badge counter should use a less obtrusive color.
|
||||
*
|
||||
* Sends out #DC_EVENT_CHAT_MODIFIED.
|
||||
*/
|
||||
public setChatMuteDuration(accountId: T.U32, chatId: T.U32, duration: T.MuteDuration): Promise<null> {
|
||||
return (this._transport.request('set_chat_mute_duration', [accountId, chatId, duration] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the chat is currently muted (can be changed by set_chat_mute_duration()).
|
||||
*
|
||||
* This is available as a standalone function outside of fullchat, because it might be only needed for notification
|
||||
*/
|
||||
public isChatMuted(accountId: T.U32, chatId: T.U32): Promise<boolean> {
|
||||
return (this._transport.request('is_chat_muted', [accountId, chatId] as RPC.Params)) as Promise<boolean>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark messages as presented to the user.
|
||||
* Typically, UIs call this function on scrolling through the message list,
|
||||
* when the messages are presented at least for a little moment.
|
||||
* The concrete action depends on the type of the chat and on the users settings
|
||||
* (dc_msgs_presented() may be a better name therefore, but well. :)
|
||||
*
|
||||
* - For normal chats, the IMAP state is updated, MDN is sent
|
||||
* (if set_config()-options `mdns_enabled` is set)
|
||||
* and the internal state is changed to @ref DC_STATE_IN_SEEN to reflect these actions.
|
||||
*
|
||||
* - For contact requests, no IMAP or MDNs is done
|
||||
* and the internal state is not changed therefore.
|
||||
* See also marknoticed_chat().
|
||||
*
|
||||
* Moreover, timer is started for incoming ephemeral messages.
|
||||
* This also happens for contact requests chats.
|
||||
*
|
||||
* One #DC_EVENT_MSGS_NOTICED event is emitted per modified chat.
|
||||
*/
|
||||
public markseenMsgs(accountId: T.U32, msgIds: (T.U32)[]): Promise<null> {
|
||||
return (this._transport.request('markseen_msgs', [accountId, msgIds] as RPC.Params)) as Promise<null>;
|
||||
public messageGetMessage(accountId: T.U32, messageId: T.U32): Promise<T.Message> {
|
||||
return (this._transport.request('message_get_message', [accountId, messageId] as RPC.Params)) as Promise<T.Message>;
|
||||
}
|
||||
|
||||
|
||||
public getMessageIds(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('get_message_ids', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
|
||||
|
||||
public getMessageListItems(accountId: T.U32, chatId: T.U32, flags: T.U32): Promise<(T.MessageListItem)[]> {
|
||||
return (this._transport.request('get_message_list_items', [accountId, chatId, flags] as RPC.Params)) as Promise<(T.MessageListItem)[]>;
|
||||
}
|
||||
|
||||
|
||||
public getMessage(accountId: T.U32, messageId: T.U32): Promise<T.Message> {
|
||||
return (this._transport.request('get_message', [accountId, messageId] as RPC.Params)) as Promise<T.Message>;
|
||||
}
|
||||
|
||||
|
||||
public getMessageHtml(accountId: T.U32, messageId: T.U32): Promise<(string|null)> {
|
||||
return (this._transport.request('get_message_html', [accountId, messageId] as RPC.Params)) as Promise<(string|null)>;
|
||||
}
|
||||
|
||||
|
||||
public getMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
|
||||
return (this._transport.request('get_messages', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.Message>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch info desktop needs for creating a notification for a message
|
||||
*/
|
||||
public getMessageNotificationInfo(accountId: T.U32, messageId: T.U32): Promise<T.MessageNotificationInfo> {
|
||||
return (this._transport.request('get_message_notification_info', [accountId, messageId] as RPC.Params)) as Promise<T.MessageNotificationInfo>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete messages. The messages are deleted on the current device and
|
||||
* on the IMAP server.
|
||||
*/
|
||||
public deleteMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<null> {
|
||||
return (this._transport.request('delete_messages', [accountId, messageIds] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an informational text for a single message. The text is multiline and may
|
||||
* contain e.g. the raw text of the message.
|
||||
*
|
||||
* The max. text returned is typically longer (about 100000 characters) than the
|
||||
* max. text returned by dc_msg_get_text() (about 30000 characters).
|
||||
*/
|
||||
public getMessageInfo(accountId: T.U32, messageId: T.U32): Promise<string> {
|
||||
return (this._transport.request('get_message_info', [accountId, messageId] as RPC.Params)) as Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks the core to start downloading a message fully.
|
||||
* This function is typically called when the user hits the "Download" button
|
||||
* that is shown by the UI in case `download_state` is `'Available'` or `'Failure'`
|
||||
*
|
||||
* On success, the @ref DC_MSG "view type of the message" may change
|
||||
* or the message may be replaced completely by one or more messages with other message IDs.
|
||||
* That may happen e.g. in cases where the message was encrypted
|
||||
* and the type could not be determined without fully downloading.
|
||||
* Downloaded content can be accessed as usual after download.
|
||||
*
|
||||
* To reflect these changes a @ref DC_EVENT_MSGS_CHANGED event will be emitted.
|
||||
*/
|
||||
public downloadFullMessage(accountId: T.U32, messageId: T.U32): Promise<null> {
|
||||
return (this._transport.request('download_full_message', [accountId, messageId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search messages containing the given query string.
|
||||
* Searching can be done globally (chat_id=0) or in a specified chat only (chat_id set).
|
||||
*
|
||||
* Global chat results are typically displayed using dc_msg_get_summary(), chat
|
||||
* search results may just hilite the corresponding messages and present a
|
||||
* prev/next button.
|
||||
*
|
||||
* For global search, result is limited to 1000 messages,
|
||||
* this allows incremental search done fast.
|
||||
* So, when getting exactly 1000 results, the result may be truncated;
|
||||
* the UIs may display sth. as "1000+ messages found" in this case.
|
||||
* Chat search (if a chat_id is set) is not limited.
|
||||
*/
|
||||
public searchMessages(accountId: T.U32, query: string, chatId: (T.U32|null)): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('search_messages', [accountId, query, chatId] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
|
||||
|
||||
public messageIdsToSearchResults(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.MessageSearchResult>> {
|
||||
return (this._transport.request('message_ids_to_search_results', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.MessageSearchResult>>;
|
||||
public messageGetMessages(accountId: T.U32, messageIds: (T.U32)[]): Promise<Record<T.U32,T.Message>> {
|
||||
return (this._transport.request('message_get_messages', [accountId, messageIds] as RPC.Params)) as Promise<Record<T.U32,T.Message>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single contact options by ID.
|
||||
*/
|
||||
public getContact(accountId: T.U32, contactId: T.U32): Promise<T.Contact> {
|
||||
return (this._transport.request('get_contact', [accountId, contactId] as RPC.Params)) as Promise<T.Contact>;
|
||||
public contactsGetContact(accountId: T.U32, contactId: T.U32): Promise<T.Contact> {
|
||||
return (this._transport.request('contacts_get_contact', [accountId, contactId] as RPC.Params)) as Promise<T.Contact>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -685,78 +243,48 @@ export class RawClient {
|
||||
*
|
||||
* Returns contact id of the created or existing contact
|
||||
*/
|
||||
public createContact(accountId: T.U32, email: string, name: (string|null)): Promise<T.U32> {
|
||||
return (this._transport.request('create_contact', [accountId, email, name] as RPC.Params)) as Promise<T.U32>;
|
||||
public contactsCreateContact(accountId: T.U32, email: string, name: (string|null)): Promise<T.U32> {
|
||||
return (this._transport.request('contacts_create_contact', [accountId, email, name] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns contact id of the created or existing DM chat with that contact
|
||||
*/
|
||||
public createChatByContactId(accountId: T.U32, contactId: T.U32): Promise<T.U32> {
|
||||
return (this._transport.request('create_chat_by_contact_id', [accountId, contactId] as RPC.Params)) as Promise<T.U32>;
|
||||
public contactsCreateChatByContactId(accountId: T.U32, contactId: T.U32): Promise<T.U32> {
|
||||
return (this._transport.request('contacts_create_chat_by_contact_id', [accountId, contactId] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
|
||||
public blockContact(accountId: T.U32, contactId: T.U32): Promise<null> {
|
||||
return (this._transport.request('block_contact', [accountId, contactId] as RPC.Params)) as Promise<null>;
|
||||
public contactsBlock(accountId: T.U32, contactId: T.U32): Promise<null> {
|
||||
return (this._transport.request('contacts_block', [accountId, contactId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public unblockContact(accountId: T.U32, contactId: T.U32): Promise<null> {
|
||||
return (this._transport.request('unblock_contact', [accountId, contactId] as RPC.Params)) as Promise<null>;
|
||||
public contactsUnblock(accountId: T.U32, contactId: T.U32): Promise<null> {
|
||||
return (this._transport.request('contacts_unblock', [accountId, contactId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public getBlockedContacts(accountId: T.U32): Promise<(T.Contact)[]> {
|
||||
return (this._transport.request('get_blocked_contacts', [accountId] as RPC.Params)) as Promise<(T.Contact)[]>;
|
||||
public contactsGetBlocked(accountId: T.U32): Promise<(T.Contact)[]> {
|
||||
return (this._transport.request('contacts_get_blocked', [accountId] as RPC.Params)) as Promise<(T.Contact)[]>;
|
||||
}
|
||||
|
||||
|
||||
public getContactIds(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('get_contact_ids', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
public contactsGetContactIds(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('contacts_get_contact_ids', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a list of contacts.
|
||||
* (formerly called getContacts2 in desktop)
|
||||
*/
|
||||
public getContacts(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.Contact)[]> {
|
||||
return (this._transport.request('get_contacts', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.Contact)[]>;
|
||||
public contactsGetContacts(accountId: T.U32, listFlags: T.U32, query: (string|null)): Promise<(T.Contact)[]> {
|
||||
return (this._transport.request('contacts_get_contacts', [accountId, listFlags, query] as RPC.Params)) as Promise<(T.Contact)[]>;
|
||||
}
|
||||
|
||||
|
||||
public getContactsByIds(accountId: T.U32, ids: (T.U32)[]): Promise<Record<T.U32,T.Contact>> {
|
||||
return (this._transport.request('get_contacts_by_ids', [accountId, ids] as RPC.Params)) as Promise<Record<T.U32,T.Contact>>;
|
||||
}
|
||||
|
||||
|
||||
public deleteContact(accountId: T.U32, contactId: T.U32): Promise<boolean> {
|
||||
return (this._transport.request('delete_contact', [accountId, contactId] as RPC.Params)) as Promise<boolean>;
|
||||
}
|
||||
|
||||
|
||||
public changeContactName(accountId: T.U32, contactId: T.U32, name: string): Promise<null> {
|
||||
return (this._transport.request('change_contact_name', [accountId, contactId, name] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get encryption info for a contact.
|
||||
* Get a multi-line encryption info, containing your fingerprint and the
|
||||
* fingerprint of the contact, used e.g. to compare the fingerprints for a simple out-of-band verification.
|
||||
*/
|
||||
public getContactEncryptionInfo(accountId: T.U32, contactId: T.U32): Promise<string> {
|
||||
return (this._transport.request('get_contact_encryption_info', [accountId, contactId] as RPC.Params)) as Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an e-mail address belongs to a known and unblocked contact.
|
||||
* To get a list of all known and unblocked contacts, use contacts_get_contacts().
|
||||
*
|
||||
* To validate an e-mail address independently of the contact database
|
||||
* use check_email_validity().
|
||||
*/
|
||||
public lookupContactIdByAddr(accountId: T.U32, addr: string): Promise<(T.U32|null)> {
|
||||
return (this._transport.request('lookup_contact_id_by_addr', [accountId, addr] as RPC.Params)) as Promise<(T.U32|null)>;
|
||||
public contactsGetContactsByIds(accountId: T.U32, ids: (T.U32)[]): Promise<Record<T.U32,T.Contact>> {
|
||||
return (this._transport.request('contacts_get_contacts_by_ids', [accountId, ids] as RPC.Params)) as Promise<Record<T.U32,T.Contact>>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -766,182 +294,33 @@ export class RawClient {
|
||||
* The list is already sorted and starts with the oldest message.
|
||||
* Clients should not try to re-sort the list as this would be an expensive action
|
||||
* and would result in inconsistencies between clients.
|
||||
*
|
||||
* Setting `chat_id` to `None` (`null` in typescript) means get messages with media
|
||||
* from any chat of the currently used account.
|
||||
*/
|
||||
public getChatMedia(accountId: T.U32, chatId: (T.U32|null), messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('get_chat_media', [accountId, chatId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Search next/previous message based on a given message and a list of types.
|
||||
* Typically used to implement the "next" and "previous" buttons
|
||||
* in a gallery or in a media player.
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
public getNeighboringChatMedia(accountId: T.U32, msgId: T.U32, messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<[(T.U32|null),(T.U32|null)]> {
|
||||
return (this._transport.request('get_neighboring_chat_media', [accountId, msgId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<[(T.U32|null),(T.U32|null)]>;
|
||||
public chatGetMedia(accountId: T.U32, chatId: T.U32, messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<(T.U32)[]> {
|
||||
return (this._transport.request('chat_get_media', [accountId, chatId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<(T.U32)[]>;
|
||||
}
|
||||
|
||||
|
||||
public exportBackup(accountId: T.U32, destination: string, passphrase: (string|null)): Promise<null> {
|
||||
return (this._transport.request('export_backup', [accountId, destination, passphrase] as RPC.Params)) as Promise<null>;
|
||||
public webxdcSendStatusUpdate(accountId: T.U32, instanceMsgId: T.U32, updateStr: string, description: string): Promise<null> {
|
||||
return (this._transport.request('webxdc_send_status_update', [accountId, instanceMsgId, updateStr, description] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public importBackup(accountId: T.U32, path: string, passphrase: (string|null)): Promise<null> {
|
||||
return (this._transport.request('import_backup', [accountId, path, passphrase] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Indicate that the network likely has come back.
|
||||
* or just that the network conditions might have changed
|
||||
*/
|
||||
public maybeNetwork(): Promise<null> {
|
||||
return (this._transport.request('maybe_network', [] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current connectivity, i.e. whether the device is connected to the IMAP server.
|
||||
* One of:
|
||||
* - DC_CONNECTIVITY_NOT_CONNECTED (1000-1999): Show e.g. the string "Not connected" or a red dot
|
||||
* - DC_CONNECTIVITY_CONNECTING (2000-2999): Show e.g. the string "Connecting…" or a yellow dot
|
||||
* - DC_CONNECTIVITY_WORKING (3000-3999): Show e.g. the string "Getting new messages" or a spinning wheel
|
||||
* - DC_CONNECTIVITY_CONNECTED (>=4000): Show e.g. the string "Connected" or a green dot
|
||||
*
|
||||
* We don't use exact values but ranges here so that we can split up
|
||||
* states into multiple states in the future.
|
||||
*
|
||||
* Meant as a rough overview that can be shown
|
||||
* e.g. in the title of the main screen.
|
||||
*
|
||||
* If the connectivity changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
|
||||
*/
|
||||
public getConnectivity(accountId: T.U32): Promise<T.U32> {
|
||||
return (this._transport.request('get_connectivity', [accountId] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an overview of the current connectivity, and possibly more statistics.
|
||||
* Meant to give the user more insight about the current status than
|
||||
* the basic connectivity info returned by get_connectivity(); show this
|
||||
* e.g., if the user taps on said basic connectivity info.
|
||||
*
|
||||
* If this page changes, a #DC_EVENT_CONNECTIVITY_CHANGED will be emitted.
|
||||
*
|
||||
* This comes as an HTML from the core so that we can easily improve it
|
||||
* and the improvement instantly reaches all UIs.
|
||||
*/
|
||||
public getConnectivityHtml(accountId: T.U32): Promise<string> {
|
||||
return (this._transport.request('get_connectivity_html', [accountId] as RPC.Params)) as Promise<string>;
|
||||
}
|
||||
|
||||
|
||||
public getLocations(accountId: T.U32, chatId: (T.U32|null), contactId: (T.U32|null), timestampBegin: T.I64, timestampEnd: T.I64): Promise<(T.Location)[]> {
|
||||
return (this._transport.request('get_locations', [accountId, chatId, contactId, timestampBegin, timestampEnd] as RPC.Params)) as Promise<(T.Location)[]>;
|
||||
}
|
||||
|
||||
|
||||
public sendWebxdcStatusUpdate(accountId: T.U32, instanceMsgId: T.U32, updateStr: string, description: string): Promise<null> {
|
||||
return (this._transport.request('send_webxdc_status_update', [accountId, instanceMsgId, updateStr, description] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public getWebxdcStatusUpdates(accountId: T.U32, instanceMsgId: T.U32, lastKnownSerial: T.U32): Promise<string> {
|
||||
return (this._transport.request('get_webxdc_status_updates', [accountId, instanceMsgId, lastKnownSerial] as RPC.Params)) as Promise<string>;
|
||||
public webxdcGetStatusUpdates(accountId: T.U32, instanceMsgId: T.U32, lastKnownSerial: T.U32): Promise<string> {
|
||||
return (this._transport.request('webxdc_get_status_updates', [accountId, instanceMsgId, lastKnownSerial] as RPC.Params)) as Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get info from a webxdc message
|
||||
*/
|
||||
public getWebxdcInfo(accountId: T.U32, instanceMsgId: T.U32): Promise<T.WebxdcMessageInfo> {
|
||||
return (this._transport.request('get_webxdc_info', [accountId, instanceMsgId] as RPC.Params)) as Promise<T.WebxdcMessageInfo>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Forward messages to another chat.
|
||||
*
|
||||
* All types of messages can be forwarded,
|
||||
* however, they will be flagged as such (dc_msg_is_forwarded() is set).
|
||||
*
|
||||
* Original sender, info-state and webxdc updates are not forwarded on purpose.
|
||||
*/
|
||||
public forwardMessages(accountId: T.U32, messageIds: (T.U32)[], chatId: T.U32): Promise<null> {
|
||||
return (this._transport.request('forward_messages', [accountId, messageIds, chatId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
|
||||
public sendSticker(accountId: T.U32, chatId: T.U32, stickerPath: string): Promise<T.U32> {
|
||||
return (this._transport.request('send_sticker', [accountId, chatId, stickerPath] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a reaction to message.
|
||||
*
|
||||
* Reaction is a string of emojis separated by spaces. Reaction to a
|
||||
* single message can be sent multiple times. The last reaction
|
||||
* received overrides all previously received reactions. It is
|
||||
* possible to remove all reactions by sending an empty string.
|
||||
*/
|
||||
public sendReaction(accountId: T.U32, messageId: T.U32, reaction: (string)[]): Promise<T.U32> {
|
||||
return (this._transport.request('send_reaction', [accountId, messageId, reaction] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
|
||||
public removeDraft(accountId: T.U32, chatId: T.U32): Promise<null> {
|
||||
return (this._transport.request('remove_draft', [accountId, chatId] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get draft for a chat, if any.
|
||||
*/
|
||||
public getDraft(accountId: T.U32, chatId: T.U32): Promise<(T.Message|null)> {
|
||||
return (this._transport.request('get_draft', [accountId, chatId] as RPC.Params)) as Promise<(T.Message|null)>;
|
||||
}
|
||||
|
||||
|
||||
public sendVideochatInvitation(accountId: T.U32, chatId: T.U32): Promise<T.U32> {
|
||||
return (this._transport.request('send_videochat_invitation', [accountId, chatId] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
|
||||
public miscGetStickerFolder(accountId: T.U32): Promise<string> {
|
||||
return (this._transport.request('misc_get_sticker_folder', [accountId] as RPC.Params)) as Promise<string>;
|
||||
}
|
||||
|
||||
/**
|
||||
* save a sticker to a collection/folder in the account's sticker folder
|
||||
*/
|
||||
public miscSaveSticker(accountId: T.U32, msgId: T.U32, collection: string): Promise<null> {
|
||||
return (this._transport.request('misc_save_sticker', [accountId, msgId, collection] as RPC.Params)) as Promise<null>;
|
||||
}
|
||||
|
||||
/**
|
||||
* for desktop, get stickers from stickers folder,
|
||||
* grouped by the collection/folder they are in.
|
||||
*/
|
||||
public miscGetStickers(accountId: T.U32): Promise<Record<string,(string)[]>> {
|
||||
return (this._transport.request('misc_get_stickers', [accountId] as RPC.Params)) as Promise<Record<string,(string)[]>>;
|
||||
public messageGetWebxdcInfo(accountId: T.U32, instanceMsgId: T.U32): Promise<T.WebxdcMessageInfo> {
|
||||
return (this._transport.request('message_get_webxdc_info', [accountId, instanceMsgId] as RPC.Params)) as Promise<T.WebxdcMessageInfo>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the messageid of the sent message
|
||||
*/
|
||||
public miscSendTextMessage(accountId: T.U32, chatId: T.U32, text: string): Promise<T.U32> {
|
||||
return (this._transport.request('misc_send_text_message', [accountId, chatId, text] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
|
||||
public miscSendMsg(accountId: T.U32, chatId: T.U32, text: (string|null), file: (string|null), location: ([T.F64,T.F64]|null), quotedMessageId: (T.U32|null)): Promise<[T.U32,T.Message]> {
|
||||
return (this._transport.request('misc_send_msg', [accountId, chatId, text, file, location, quotedMessageId] as RPC.Params)) as Promise<[T.U32,T.Message]>;
|
||||
}
|
||||
|
||||
|
||||
public miscSetDraft(accountId: T.U32, chatId: T.U32, text: (string|null), file: (string|null), quotedMessageId: (T.U32|null)): Promise<null> {
|
||||
return (this._transport.request('misc_set_draft', [accountId, chatId, text, file, quotedMessageId] as RPC.Params)) as Promise<null>;
|
||||
public miscSendTextMessage(accountId: T.U32, text: string, chatId: T.U32): Promise<T.U32> {
|
||||
return (this._transport.request('misc_send_text_message', [accountId, text, chatId] as RPC.Params)) as Promise<T.U32>;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
// Generated!
|
||||
|
||||
export enum C {
|
||||
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
|
||||
DC_CERTCK_AUTO = 0,
|
||||
DC_CERTCK_STRICT = 1,
|
||||
DC_CHAT_ID_ALLDONE_HINT = 7,
|
||||
DC_CHAT_ID_ARCHIVED_LINK = 6,
|
||||
DC_CHAT_ID_LAST_SPECIAL = 9,
|
||||
DC_CHAT_ID_TRASH = 3,
|
||||
DC_CHAT_TYPE_BROADCAST = 160,
|
||||
DC_CHAT_TYPE_GROUP = 120,
|
||||
DC_CHAT_TYPE_MAILINGLIST = 140,
|
||||
DC_CHAT_TYPE_SINGLE = 100,
|
||||
DC_CHAT_TYPE_UNDEFINED = 0,
|
||||
DC_CONNECTIVITY_CONNECTED = 4000,
|
||||
DC_CONNECTIVITY_CONNECTING = 2000,
|
||||
DC_CONNECTIVITY_NOT_CONNECTED = 1000,
|
||||
DC_CONNECTIVITY_WORKING = 3000,
|
||||
DC_CONTACT_ID_DEVICE = 5,
|
||||
DC_CONTACT_ID_INFO = 2,
|
||||
DC_CONTACT_ID_LAST_SPECIAL = 9,
|
||||
DC_CONTACT_ID_SELF = 1,
|
||||
DC_GCL_ADD_ALLDONE_HINT = 4,
|
||||
DC_GCL_ADD_SELF = 2,
|
||||
DC_GCL_ARCHIVED_ONLY = 1,
|
||||
DC_GCL_FOR_FORWARDING = 8,
|
||||
DC_GCL_NO_SPECIALS = 2,
|
||||
DC_GCL_VERIFIED_ONLY = 1,
|
||||
DC_GCM_ADDDAYMARKER = 1,
|
||||
DC_GCM_INFO_ONLY = 2,
|
||||
DC_KEY_GEN_DEFAULT = 0,
|
||||
DC_KEY_GEN_ED25519 = 2,
|
||||
DC_KEY_GEN_RSA2048 = 1,
|
||||
DC_LP_AUTH_NORMAL = 4,
|
||||
DC_LP_AUTH_OAUTH2 = 2,
|
||||
DC_MEDIA_QUALITY_BALANCED = 0,
|
||||
DC_MEDIA_QUALITY_WORSE = 1,
|
||||
DC_MSG_ID_DAYMARKER = 9,
|
||||
DC_MSG_ID_LAST_SPECIAL = 9,
|
||||
DC_MSG_ID_MARKER1 = 1,
|
||||
DC_PROVIDER_STATUS_BROKEN = 3,
|
||||
DC_PROVIDER_STATUS_OK = 1,
|
||||
DC_PROVIDER_STATUS_PREPARATION = 2,
|
||||
DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1,
|
||||
DC_SHOW_EMAILS_ALL = 2,
|
||||
DC_SHOW_EMAILS_OFF = 0,
|
||||
DC_SOCKET_AUTO = 0,
|
||||
DC_SOCKET_PLAIN = 3,
|
||||
DC_SOCKET_SSL = 1,
|
||||
DC_SOCKET_STARTTLS = 2,
|
||||
DC_STATE_IN_FRESH = 10,
|
||||
DC_STATE_IN_NOTICED = 13,
|
||||
DC_STATE_IN_SEEN = 16,
|
||||
DC_STATE_OUT_DELIVERED = 26,
|
||||
DC_STATE_OUT_DRAFT = 19,
|
||||
DC_STATE_OUT_FAILED = 24,
|
||||
DC_STATE_OUT_MDN_RCVD = 28,
|
||||
DC_STATE_OUT_PENDING = 20,
|
||||
DC_STATE_OUT_PREPARING = 18,
|
||||
DC_STATE_UNDEFINED = 0,
|
||||
DC_STR_AC_SETUP_MSG_BODY = 43,
|
||||
DC_STR_AC_SETUP_MSG_SUBJECT = 42,
|
||||
DC_STR_ADD_MEMBER_BY_OTHER = 129,
|
||||
DC_STR_ADD_MEMBER_BY_YOU = 128,
|
||||
DC_STR_AEAP_ADDR_CHANGED = 122,
|
||||
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
|
||||
DC_STR_ARCHIVEDCHATS = 40,
|
||||
DC_STR_AUDIO = 11,
|
||||
DC_STR_BAD_TIME_MSG_BODY = 85,
|
||||
DC_STR_BROADCAST_LIST = 115,
|
||||
DC_STR_CANNOT_LOGIN = 60,
|
||||
DC_STR_CANTDECRYPT_MSG_BODY = 29,
|
||||
DC_STR_CONFIGURATION_FAILED = 84,
|
||||
DC_STR_CONNECTED = 107,
|
||||
DC_STR_CONNTECTING = 108,
|
||||
DC_STR_CONTACT_NOT_VERIFIED = 36,
|
||||
DC_STR_CONTACT_SETUP_CHANGED = 37,
|
||||
DC_STR_CONTACT_VERIFIED = 35,
|
||||
DC_STR_DEVICE_MESSAGES = 68,
|
||||
DC_STR_DEVICE_MESSAGES_HINT = 70,
|
||||
DC_STR_DOWNLOAD_AVAILABILITY = 100,
|
||||
DC_STR_DRAFT = 3,
|
||||
DC_STR_E2E_AVAILABLE = 25,
|
||||
DC_STR_E2E_PREFERRED = 34,
|
||||
DC_STR_ENCRYPTEDMSG = 24,
|
||||
DC_STR_ENCR_NONE = 28,
|
||||
DC_STR_ENCR_TRANSP = 27,
|
||||
DC_STR_EPHEMERAL_DAY = 79,
|
||||
DC_STR_EPHEMERAL_DAYS = 95,
|
||||
DC_STR_EPHEMERAL_DISABLED = 75,
|
||||
DC_STR_EPHEMERAL_FOUR_WEEKS = 81,
|
||||
DC_STR_EPHEMERAL_HOUR = 78,
|
||||
DC_STR_EPHEMERAL_HOURS = 94,
|
||||
DC_STR_EPHEMERAL_MINUTE = 77,
|
||||
DC_STR_EPHEMERAL_MINUTES = 93,
|
||||
DC_STR_EPHEMERAL_SECONDS = 76,
|
||||
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER = 147,
|
||||
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU = 146,
|
||||
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER = 145,
|
||||
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU = 144,
|
||||
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER = 143,
|
||||
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU = 142,
|
||||
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER = 149,
|
||||
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU = 148,
|
||||
DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER = 155,
|
||||
DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU = 154,
|
||||
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER = 139,
|
||||
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU = 138,
|
||||
DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER = 153,
|
||||
DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU = 152,
|
||||
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER = 151,
|
||||
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU = 150,
|
||||
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER = 141,
|
||||
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU = 140,
|
||||
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER = 157,
|
||||
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU = 156,
|
||||
DC_STR_EPHEMERAL_WEEK = 80,
|
||||
DC_STR_EPHEMERAL_WEEKS = 96,
|
||||
DC_STR_ERROR = 112,
|
||||
DC_STR_ERROR_NO_NETWORK = 87,
|
||||
DC_STR_FAILED_SENDING_TO = 74,
|
||||
DC_STR_FILE = 12,
|
||||
DC_STR_FINGERPRINTS = 30,
|
||||
DC_STR_FORWARDED = 97,
|
||||
DC_STR_GIF = 23,
|
||||
DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER = 127,
|
||||
DC_STR_GROUP_IMAGE_CHANGED_BY_YOU = 126,
|
||||
DC_STR_GROUP_IMAGE_DELETED_BY_OTHER = 135,
|
||||
DC_STR_GROUP_IMAGE_DELETED_BY_YOU = 134,
|
||||
DC_STR_GROUP_LEFT_BY_OTHER = 133,
|
||||
DC_STR_GROUP_LEFT_BY_YOU = 132,
|
||||
DC_STR_GROUP_NAME_CHANGED_BY_OTHER = 125,
|
||||
DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124,
|
||||
DC_STR_IMAGE = 9,
|
||||
DC_STR_INCOMING_MESSAGES = 103,
|
||||
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
|
||||
DC_STR_LOCATION = 66,
|
||||
DC_STR_LOCATION_ENABLED_BY_OTHER = 137,
|
||||
DC_STR_LOCATION_ENABLED_BY_YOU = 136,
|
||||
DC_STR_MESSAGES = 114,
|
||||
DC_STR_MSGACTIONBYME = 63,
|
||||
DC_STR_MSGACTIONBYUSER = 62,
|
||||
DC_STR_MSGADDMEMBER = 17,
|
||||
DC_STR_MSGDELMEMBER = 18,
|
||||
DC_STR_MSGGROUPLEFT = 19,
|
||||
DC_STR_MSGGRPIMGCHANGED = 16,
|
||||
DC_STR_MSGGRPIMGDELETED = 33,
|
||||
DC_STR_MSGGRPNAME = 15,
|
||||
DC_STR_MSGLOCATIONDISABLED = 65,
|
||||
DC_STR_MSGLOCATIONENABLED = 64,
|
||||
DC_STR_NOMESSAGES = 1,
|
||||
DC_STR_NOT_CONNECTED = 121,
|
||||
DC_STR_NOT_SUPPORTED_BY_PROVIDER = 113,
|
||||
DC_STR_ONE_MOMENT = 106,
|
||||
DC_STR_OUTGOING_MESSAGES = 104,
|
||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
|
||||
DC_STR_PART_OF_TOTAL_USED = 116,
|
||||
DC_STR_PROTECTION_DISABLED = 89,
|
||||
DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
|
||||
DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
|
||||
DC_STR_PROTECTION_ENABLED = 88,
|
||||
DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
|
||||
DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
|
||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
|
||||
DC_STR_READRCPT = 31,
|
||||
DC_STR_READRCPT_MAILBODY = 32,
|
||||
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
|
||||
DC_STR_REMOVE_MEMBER_BY_YOU = 130,
|
||||
DC_STR_REPLY_NOUN = 90,
|
||||
DC_STR_SAVED_MESSAGES = 69,
|
||||
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
|
||||
DC_STR_SECURE_JOIN_REPLIES = 118,
|
||||
DC_STR_SECURE_JOIN_STARTED = 117,
|
||||
DC_STR_SELF = 2,
|
||||
DC_STR_SELF_DELETED_MSG_BODY = 91,
|
||||
DC_STR_SENDING = 110,
|
||||
DC_STR_SERVER_TURNED_OFF = 92,
|
||||
DC_STR_SETUP_CONTACT_QR_DESC = 119,
|
||||
DC_STR_STICKER = 67,
|
||||
DC_STR_STORAGE_ON_DOMAIN = 105,
|
||||
DC_STR_SUBJECT_FOR_NEW_CONTACT = 73,
|
||||
DC_STR_SYNC_MSG_BODY = 102,
|
||||
DC_STR_SYNC_MSG_SUBJECT = 101,
|
||||
DC_STR_UNKNOWN_SENDER_FOR_CHAT = 72,
|
||||
DC_STR_UPDATE_REMINDER_MSG_BODY = 86,
|
||||
DC_STR_UPDATING = 109,
|
||||
DC_STR_VIDEO = 10,
|
||||
DC_STR_VIDEOCHAT_INVITATION = 82,
|
||||
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
|
||||
DC_STR_VOICEMESSAGE = 7,
|
||||
DC_STR_WELCOME_MESSAGE = 71,
|
||||
DC_TEXT1_DRAFT = 1,
|
||||
DC_TEXT1_SELF = 3,
|
||||
DC_TEXT1_USERNAME = 2,
|
||||
DC_VIDEOCHATTYPE_BASICWEBRTC = 1,
|
||||
DC_VIDEOCHATTYPE_JITSI = 2,
|
||||
DC_VIDEOCHATTYPE_UNKNOWN = 0,
|
||||
}
|
||||
@@ -1,214 +1,3 @@
|
||||
// AUTO-GENERATED by typescript-type-def
|
||||
|
||||
export type U32=number;
|
||||
export type Usize=number;
|
||||
export type Event=(({
|
||||
/**
|
||||
* The library-user may write an informational string to the log.
|
||||
*
|
||||
* This event should *not* be reported to the end-user using a popup or something like
|
||||
* that.
|
||||
*/
|
||||
"type":"Info";}&{"msg":string;})|({
|
||||
/**
|
||||
* Emitted when SMTP connection is established and login was successful.
|
||||
*/
|
||||
"type":"SmtpConnected";}&{"msg":string;})|({
|
||||
/**
|
||||
* Emitted when IMAP connection is established and login was successful.
|
||||
*/
|
||||
"type":"ImapConnected";}&{"msg":string;})|({
|
||||
/**
|
||||
* Emitted when a message was successfully sent to the SMTP server.
|
||||
*/
|
||||
"type":"SmtpMessageSent";}&{"msg":string;})|({
|
||||
/**
|
||||
* Emitted when an IMAP message has been marked as deleted
|
||||
*/
|
||||
"type":"ImapMessageDeleted";}&{"msg":string;})|({
|
||||
/**
|
||||
* Emitted when an IMAP message has been moved
|
||||
*/
|
||||
"type":"ImapMessageMoved";}&{"msg":string;})|({
|
||||
/**
|
||||
* Emitted when an new file in the $BLOBDIR was created
|
||||
*/
|
||||
"type":"NewBlobFile";}&{"file":string;})|({
|
||||
/**
|
||||
* Emitted when an file in the $BLOBDIR was deleted
|
||||
*/
|
||||
"type":"DeletedBlobFile";}&{"file":string;})|({
|
||||
/**
|
||||
* The library-user should write a warning string to the log.
|
||||
*
|
||||
* This event should *not* be reported to the end-user using a popup or something like
|
||||
* that.
|
||||
*/
|
||||
"type":"Warning";}&{"msg":string;})|({
|
||||
/**
|
||||
* The library-user should report an error to the end-user.
|
||||
*
|
||||
* As most things are asynchronous, things may go wrong at any time and the user
|
||||
* should not be disturbed by a dialog or so. Instead, use a bubble or so.
|
||||
*
|
||||
* However, for ongoing processes (eg. configure())
|
||||
* or for functions that are expected to fail (eg. autocryptContinueKeyTransfer())
|
||||
* it might be better to delay showing these events until the function has really
|
||||
* failed (returned false). It should be sufficient to report only the *last* error
|
||||
* in a messasge box then.
|
||||
*/
|
||||
"type":"Error";}&{"msg":string;})|({
|
||||
/**
|
||||
* An action cannot be performed because the user is not in the group.
|
||||
* Reported eg. after a call to
|
||||
* setChatName(), setChatProfileImage(),
|
||||
* addContactToChat(), removeContactFromChat(),
|
||||
* and messages sending functions.
|
||||
*/
|
||||
"type":"ErrorSelfNotInGroup";}&{"msg":string;})|({
|
||||
/**
|
||||
* Messages or chats changed. One or more messages or chats changed for various
|
||||
* reasons in the database:
|
||||
* - Messages sent, received or removed
|
||||
* - Chats created, deleted or archived
|
||||
* - A draft has been set
|
||||
*
|
||||
* `chatId` is set if only a single chat is affected by the changes, otherwise 0.
|
||||
* `msgId` is set if only a single message is affected by the changes, otherwise 0.
|
||||
*/
|
||||
"type":"MsgsChanged";}&{"chatId":U32;"msgId":U32;})|({
|
||||
/**
|
||||
* Reactions for the message changed.
|
||||
*/
|
||||
"type":"ReactionsChanged";}&{"chatId":U32;"msgId":U32;"contactId":U32;})|({
|
||||
/**
|
||||
* There is a fresh message. Typically, the user will show an notification
|
||||
* when receiving this message.
|
||||
*
|
||||
* There is no extra #DC_EVENT_MSGS_CHANGED event send together with this event.
|
||||
*/
|
||||
"type":"IncomingMsg";}&{"chatId":U32;"msgId":U32;})|({
|
||||
/**
|
||||
* Downloading a bunch of messages just finished. This is an experimental
|
||||
* event to allow the UI to only show one notification per message bunch,
|
||||
* instead of cluttering the user with many notifications.
|
||||
*
|
||||
* msg_ids contains the message ids.
|
||||
*/
|
||||
"type":"IncomingMsgBunch";}&{"msgIds":(U32)[];})|({
|
||||
/**
|
||||
* Messages were seen or noticed.
|
||||
* chat id is always set.
|
||||
*/
|
||||
"type":"MsgsNoticed";}&{"chatId":U32;})|({
|
||||
/**
|
||||
* A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
||||
* DC_STATE_OUT_DELIVERED, see `Message.state`.
|
||||
*/
|
||||
"type":"MsgDelivered";}&{"chatId":U32;"msgId":U32;})|({
|
||||
/**
|
||||
* A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
|
||||
* DC_STATE_OUT_FAILED, see `Message.state`.
|
||||
*/
|
||||
"type":"MsgFailed";}&{"chatId":U32;"msgId":U32;})|({
|
||||
/**
|
||||
* A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
||||
* DC_STATE_OUT_MDN_RCVD, see `Message.state`.
|
||||
*/
|
||||
"type":"MsgRead";}&{"chatId":U32;"msgId":U32;})|({
|
||||
/**
|
||||
* Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||
* Or the verify state of a chat has changed.
|
||||
* See setChatName(), setChatProfileImage(), addContactToChat()
|
||||
* and removeContactFromChat().
|
||||
*
|
||||
* This event does not include ephemeral timer modification, which
|
||||
* is a separate event.
|
||||
*/
|
||||
"type":"ChatModified";}&{"chatId":U32;})|({
|
||||
/**
|
||||
* Chat ephemeral timer changed.
|
||||
*/
|
||||
"type":"ChatEphemeralTimerModified";}&{"chatId":U32;"timer":U32;})|({
|
||||
/**
|
||||
* Contact(s) created, renamed, blocked or deleted.
|
||||
*
|
||||
* @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
||||
*/
|
||||
"type":"ContactsChanged";}&{"contactId":(U32|null);})|({
|
||||
/**
|
||||
* Location of one or more contact has changed.
|
||||
*
|
||||
* @param data1 (u32) contact_id of the contact for which the location has changed.
|
||||
* If the locations of several contacts have been changed,
|
||||
* this parameter is set to `None`.
|
||||
*/
|
||||
"type":"LocationChanged";}&{"contactId":(U32|null);})|({
|
||||
/**
|
||||
* Inform about the configuration progress started by configure().
|
||||
*/
|
||||
"type":"ConfigureProgress";}&{
|
||||
/**
|
||||
* Progress.
|
||||
*
|
||||
* 0=error, 1-999=progress in permille, 1000=success and done
|
||||
*/
|
||||
"progress":Usize;
|
||||
/**
|
||||
* Progress comment or error, something to display to the user.
|
||||
*/
|
||||
"comment":(string|null);})|({
|
||||
/**
|
||||
* Inform about the import/export progress started by imex().
|
||||
*
|
||||
* @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
||||
* @param data2 0
|
||||
*/
|
||||
"type":"ImexProgress";}&{"progress":Usize;})|({
|
||||
/**
|
||||
* A file has been exported. A file has been written by imex().
|
||||
* This event may be sent multiple times by a single call to imex().
|
||||
*
|
||||
* A typical purpose for a handler of this event may be to make the file public to some system
|
||||
* services.
|
||||
*
|
||||
* @param data2 0
|
||||
*/
|
||||
"type":"ImexFileWritten";}&{"path":string;})|({
|
||||
/**
|
||||
* Progress information of a secure-join handshake from the view of the inviter
|
||||
* (Alice, the person who shows the QR code).
|
||||
*
|
||||
* These events are typically sent after a joiner has scanned the QR code
|
||||
* generated by getChatSecurejoinQrCodeSvg().
|
||||
*
|
||||
* @param data1 (int) ID of the contact that wants to join.
|
||||
* @param data2 (int) Progress as:
|
||||
* 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
||||
* 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||
* 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
||||
* 1000=Protocol finished for this contact.
|
||||
*/
|
||||
"type":"SecurejoinInviterProgress";}&{"contactId":U32;"progress":Usize;})|({
|
||||
/**
|
||||
* Progress information of a secure-join handshake from the view of the joiner
|
||||
* (Bob, the person who scans the QR code).
|
||||
* The events are typically sent while secureJoin(), which
|
||||
* may take some time, is executed.
|
||||
* @param data1 (int) ID of the inviting contact.
|
||||
* @param data2 (int) Progress as:
|
||||
* 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||
* (Bob has verified alice and waits until Alice does the same for him)
|
||||
*/
|
||||
"type":"SecurejoinJoinerProgress";}&{"contactId":U32;"progress":Usize;})|{
|
||||
/**
|
||||
* The connectivity to the server changed.
|
||||
* This means that you should refresh the connectivity view
|
||||
* and possibly the connectivtiy HTML; see getConnectivity() and
|
||||
* getConnectivityHtml() for details.
|
||||
*/
|
||||
"type":"ConnectivityChanged";}|{"type":"SelfavatarChanged";}|({"type":"WebxdcStatusUpdate";}&{"msgId":U32;"statusUpdateSerial":U32;})|({
|
||||
/**
|
||||
* Inform that a message containing a webxdc instance has been deleted
|
||||
*/
|
||||
"type":"WebxdcInstanceDeleted";}&{"msgId":U32;}));
|
||||
export type EventTypeName=("Info"|"SmtpConnected"|"ImapConnected"|"SmtpMessageSent"|"ImapMessageDeleted"|"ImapMessageMoved"|"NewBlobFile"|"DeletedBlobFile"|"Warning"|"Error"|"ErrorSelfNotInGroup"|"MsgsChanged"|"IncomingMsg"|"MsgsNoticed"|"MsgDelivered"|"MsgFailed"|"MsgRead"|"ChatModified"|"ChatEphemeralTimerModified"|"ContactsChanged"|"LocationChanged"|"ConfigureProgress"|"ImexProgress"|"ImexFileWritten"|"SecurejoinInviterProgress"|"SecurejoinJoinerProgress"|"ConnectivityChanged"|"SelfavatarChanged"|"WebxdcStatusUpdate");
|
||||
|
||||
@@ -2,66 +2,17 @@
|
||||
|
||||
export type U32=number;
|
||||
export type Account=(({"type":"Configured";}&{"id":U32;"displayName":(string|null);"addr":(string|null);"profileImage":(string|null);"color":string;})|({"type":"Unconfigured";}&{"id":U32;}));
|
||||
export type U64=number;
|
||||
export type ProviderInfo={"beforeLoginHint":string;"overviewPage":string;"status":U32;};
|
||||
export type Qr=(({"type":"askVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"askVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"fprOk";}&{"contact_id":U32;})|({"type":"fprMismatch";}&{"contact_id":(U32|null);})|({"type":"fprWithoutAddr";}&{"fingerprint":string;})|({"type":"account";}&{"domain":string;})|({"type":"webrtcInstance";}&{"domain":string;"instance_pattern":string;})|({"type":"addr";}&{"contact_id":U32;"draft":(string|null);})|({"type":"url";}&{"url":string;})|({"type":"text";}&{"text":string;})|({"type":"withdrawVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"withdrawVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyContact";}&{"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"reviveVerifyGroup";}&{"grpname":string;"grpid":string;"contact_id":U32;"fingerprint":string;"invitenumber":string;"authcode":string;})|({"type":"login";}&{"address":string;}));
|
||||
export type Usize=number;
|
||||
export type I64=number;
|
||||
export type ChatListEntry=[U32,U32];
|
||||
export type I64=number;
|
||||
export type ChatListItemFetchResult=(({"type":"ChatListItem";}&{"id":U32;"name":string;"avatarPath":(string|null);"color":string;"lastUpdated":(I64|null);"summaryText1":string;"summaryText2":string;"summaryStatus":U32;"isProtected":boolean;"isGroup":boolean;"freshMessageCounter":Usize;"isSelfTalk":boolean;"isDeviceTalk":boolean;"isSendingLocation":boolean;"isSelfInGroup":boolean;"isArchived":boolean;"isPinned":boolean;"isMuted":boolean;"isContactRequest":boolean;
|
||||
/**
|
||||
* true when chat is a broadcastlist
|
||||
*/
|
||||
"isBroadcast":boolean;
|
||||
/**
|
||||
* contact id if this is a dm chat (for view profile entry in context menu)
|
||||
*/
|
||||
"dmChatContact":(U32|null);"wasSeenRecently":boolean;})|{"type":"ArchiveLink";}|({"type":"Error";}&{"id":U32;"error":string;}));
|
||||
export type Contact={"address":string;"color":string;"authName":string;"status":string;"displayName":string;"id":U32;"name":string;"profileImage":(string|null);"nameAndAddr":string;"isBlocked":boolean;"isVerified":boolean;
|
||||
/**
|
||||
* the contact's last seen timestamp
|
||||
*/
|
||||
"lastSeen":I64;"wasSeenRecently":boolean;};
|
||||
export type FullChat={"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"contacts":(Contact)[];"contactIds":(U32)[];"color":string;"freshMessageCounter":Usize;"isContactRequest":boolean;"isDeviceChat":boolean;"selfInGroup":boolean;"isMuted":boolean;"ephemeralTimer":U32;"canSend":boolean;"wasSeenRecently":boolean;"mailingListAddress":(string|null);};
|
||||
|
||||
/**
|
||||
* cheaper version of fullchat, omits:
|
||||
* - contacts
|
||||
* - contact_ids
|
||||
* - fresh_message_counter
|
||||
* - ephemeral_timer
|
||||
* - self_in_group
|
||||
* - was_seen_recently
|
||||
* - can_send
|
||||
*
|
||||
* used when you only need the basic metadata of a chat like type, name, profile picture
|
||||
*/
|
||||
export type BasicChat=
|
||||
/**
|
||||
* cheaper version of fullchat, omits:
|
||||
* - contacts
|
||||
* - contact_ids
|
||||
* - fresh_message_counter
|
||||
* - ephemeral_timer
|
||||
* - self_in_group
|
||||
* - was_seen_recently
|
||||
* - can_send
|
||||
*
|
||||
* used when you only need the basic metadata of a chat like type, name, profile picture
|
||||
*/
|
||||
{"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"color":string;"isContactRequest":boolean;"isDeviceChat":boolean;"isMuted":boolean;};
|
||||
export type ChatVisibility=("Normal"|"Archived"|"Pinned");
|
||||
export type MuteDuration=("NotMuted"|"Forever"|{"Until":I64;});
|
||||
export type MessageListItem=(({"kind":"message";}&{"msg_id":U32;})|({
|
||||
/**
|
||||
* Day marker, separating messages that correspond to different
|
||||
* days according to local time.
|
||||
*/
|
||||
"kind":"dayMarker";}&{
|
||||
/**
|
||||
* Marker timestamp, for day markers, in unix milliseconds
|
||||
*/
|
||||
"timestamp":I64;}));
|
||||
"dmChatContact":(U32|null);})|{"type":"ArchiveLink";}|({"type":"Error";}&{"id":U32;"error":string;}));
|
||||
export type Contact={"address":string;"color":string;"authName":string;"status":string;"displayName":string;"id":U32;"name":string;"profileImage":(string|null);"nameAndAddr":string;"isBlocked":boolean;"isVerified":boolean;};
|
||||
export type FullChat={"id":U32;"name":string;"isProtected":boolean;"profileImage":(string|null);"archived":boolean;"chatType":U32;"isUnpromoted":boolean;"isSelfTalk":boolean;"contacts":(Contact)[];"contactIds":(U32)[];"color":string;"freshMessageCounter":Usize;"isContactRequest":boolean;"isDeviceChat":boolean;"selfInGroup":boolean;"isMuted":boolean;"ephemeralTimer":U32;"canSend":boolean;};
|
||||
export type Viewtype=("Unknown"|
|
||||
/**
|
||||
* Text message.
|
||||
@@ -107,22 +58,9 @@ export type Viewtype=("Unknown"|
|
||||
* Message is an webxdc instance.
|
||||
*/
|
||||
"Webxdc");
|
||||
export type MessageQuote=(({"kind":"JustText";}&{"text":string;})|({"kind":"WithMessage";}&{"text":string;"messageId":U32;"authorDisplayName":string;"authorDisplayColor":string;"overrideSenderName":(string|null);"image":(string|null);"isForwarded":boolean;"viewType":Viewtype;}));
|
||||
export type SystemMessageType=("Unknown"|"GroupNameChanged"|"GroupImageChanged"|"MemberAddedToGroup"|"MemberRemovedFromGroup"|"AutocryptSetupMessage"|"SecurejoinMessage"|"LocationStreamingEnabled"|"LocationOnly"|
|
||||
/**
|
||||
* Chat ephemeral message timer is changed.
|
||||
*/
|
||||
"EphemeralTimerChanged"|"ChatProtectionEnabled"|"ChatProtectionDisabled"|
|
||||
/**
|
||||
* Self-sent-message that contains only json used for multi-device-sync;
|
||||
* if possible, we attach that to other messages as for locations.
|
||||
*/
|
||||
"MultiDeviceSync"|"WebxdcStatusUpdate"|
|
||||
/**
|
||||
* Webxdc info added with `info` set in `send_webxdc_status_update()`.
|
||||
*/
|
||||
"WebxdcInfoMessage");
|
||||
export type I32=number;
|
||||
export type U64=number;
|
||||
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quotedText":(string|null);"quotedMessageId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);};
|
||||
export type WebxdcMessageInfo={
|
||||
/**
|
||||
* The name of the app.
|
||||
@@ -155,44 +93,5 @@ export type WebxdcMessageInfo={
|
||||
* defaults to an empty string.
|
||||
* Implementations may offer an menu or a button to open this URL.
|
||||
*/
|
||||
"sourceCodeUrl":(string|null);
|
||||
/**
|
||||
* True if full internet access should be granted to the app.
|
||||
*/
|
||||
"internetAccess":boolean;};
|
||||
export type DownloadState=("Done"|"Available"|"Failure"|"InProgress");
|
||||
|
||||
/**
|
||||
* Structure representing all reactions to a particular message.
|
||||
*/
|
||||
export type Reactions=
|
||||
/**
|
||||
* Structure representing all reactions to a particular message.
|
||||
*/
|
||||
{
|
||||
/**
|
||||
* Map from a contact to it's reaction to message.
|
||||
*/
|
||||
"reactionsByContact":Record<U32,(string)[]>;
|
||||
/**
|
||||
* Unique reactions and their count
|
||||
*/
|
||||
"reactions":Record<string,U32>;};
|
||||
export type Message={"id":U32;"chatId":U32;"fromId":U32;"quote":(MessageQuote|null);"parentId":(U32|null);"text":(string|null);"hasLocation":boolean;"hasHtml":boolean;"viewType":Viewtype;"state":U32;"timestamp":I64;"sortTimestamp":I64;"receivedTimestamp":I64;"hasDeviatingTimestamp":boolean;"subject":string;"showPadlock":boolean;"isSetupmessage":boolean;"isInfo":boolean;"isForwarded":boolean;
|
||||
/**
|
||||
* when is_info is true this describes what type of system message it is
|
||||
*/
|
||||
"systemMessageType":SystemMessageType;"duration":I32;"dimensionsHeight":I32;"dimensionsWidth":I32;"videochatType":(U32|null);"videochatUrl":(string|null);"overrideSenderName":(string|null);"sender":Contact;"setupCodeBegin":(string|null);"file":(string|null);"fileMime":(string|null);"fileBytes":U64;"fileName":(string|null);"webxdcInfo":(WebxdcMessageInfo|null);"downloadState":DownloadState;"reactions":(Reactions|null);};
|
||||
export type MessageNotificationInfo={"id":U32;"chatId":U32;"accountId":U32;"image":(string|null);"imageMimeType":(string|null);"chatName":string;"chatProfileImage":(string|null);
|
||||
/**
|
||||
* also known as summary_text1
|
||||
*/
|
||||
"summaryPrefix":(string|null);
|
||||
/**
|
||||
* also known as summary_text2
|
||||
*/
|
||||
"summaryText":string;};
|
||||
export type MessageSearchResult={"id":U32;"authorProfileImage":(string|null);"authorName":string;"authorColor":string;"chatName":(string|null);"message":string;"timestamp":I64;};
|
||||
export type F64=number;
|
||||
export type Location={"locationId":U32;"isIndependent":boolean;"latitude":F64;"longitude":F64;"accuracy":F64;"timestamp":I64;"contactId":U32;"msgId":U32;"chatId":U32;"marker":(string|null);};
|
||||
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],null,null,U32,null,U32,null,U32,Account,U32,U64,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,Qr,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,Record<U32,string>,null,U32,null,U32,null,U32,string,(string|null),null,U32,string,(string|null),null,U32,(U32)[],U32,U32,Usize,U32,boolean,I64,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,BasicChat,U32,U32,null,U32,U32,null,U32,U32,null,U32,U32,string,U32,(U32|null),[string,string],U32,string,U32,U32,U32,null,U32,U32,U32,null,U32,U32,U32,null,U32,U32,(U32)[],U32,string,boolean,U32,U32,U32,U32,U32,string,null,U32,U32,(string|null),null,U32,U32,ChatVisibility,null,U32,U32,U32,null,U32,U32,U32,U32,string,string,U32,U32,U32,null,U32,U32,(U32|null),U32,U32,MuteDuration,null,U32,U32,boolean,U32,(U32)[],null,U32,U32,U32,(U32)[],U32,U32,U32,(MessageListItem)[],U32,U32,Message,U32,U32,(string|null),U32,(U32)[],Record<U32,Message>,U32,U32,MessageNotificationInfo,U32,(U32)[],null,U32,U32,string,U32,U32,null,U32,string,(U32|null),(U32)[],U32,(U32)[],Record<U32,MessageSearchResult>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,boolean,U32,U32,string,null,U32,U32,string,U32,string,(U32|null),U32,(U32|null),Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),[(U32|null),(U32|null)],U32,string,(string|null),null,U32,string,(string|null),null,null,U32,U32,U32,string,U32,(U32|null),(U32|null),I64,I64,(Location)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,(U32)[],U32,null,U32,U32,string,U32,U32,U32,(string)[],U32,U32,U32,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,U32,string,null,U32,Record<string,(string)[]>,U32,U32,string,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];
|
||||
"sourceCodeUrl":(string|null);};
|
||||
export type __AllTyps=[string,boolean,Record<string,string>,U32,U32,null,(U32)[],U32,null,(U32|null),(Account)[],U32,Account,U32,string,(ProviderInfo|null),U32,boolean,U32,Record<string,string>,U32,string,(string|null),null,U32,Record<string,(string|null)>,null,U32,string,null,U32,string,(string|null),U32,(string)[],Record<string,(string|null)>,U32,null,U32,null,U32,(U32)[],U32,U32,Usize,U32,string,U32,U32,string,null,U32,(U32|null),(string|null),(U32|null),(ChatListEntry)[],U32,(ChatListEntry)[],Record<U32,ChatListItemFetchResult>,U32,U32,FullChat,U32,U32,null,U32,U32,null,U32,string,string,U32,U32,U32,U32,(U32)[],U32,U32,Message,U32,(U32)[],Record<U32,Message>,U32,U32,Contact,U32,string,(string|null),U32,U32,U32,U32,U32,U32,null,U32,U32,null,U32,(Contact)[],U32,U32,(string|null),(U32)[],U32,U32,(string|null),(Contact)[],U32,(U32)[],Record<U32,Contact>,U32,U32,Viewtype,(Viewtype|null),(Viewtype|null),(U32)[],U32,U32,string,string,null,U32,U32,U32,string,U32,U32,WebxdcMessageInfo,U32,string,U32,U32];
|
||||
|
||||
@@ -1,8 +1,32 @@
|
||||
{
|
||||
"name": "deltachat-jsonrpc-client",
|
||||
"version": "0.1.0",
|
||||
"main": "dist/deltachat.js",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"type": "module",
|
||||
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
|
||||
"license": "MPL-2.0",
|
||||
"scripts": {
|
||||
"prettier:check": "prettier --check **.ts",
|
||||
"prettier:fix": "prettier --write **.ts",
|
||||
"generate-bindings": "cargo test",
|
||||
"build": "run-s generate-bindings build:tsc build:bundle",
|
||||
"build:tsc": "tsc",
|
||||
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
|
||||
"example": "run-s build example:build example:start",
|
||||
"example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
|
||||
"example:start": "http-server .",
|
||||
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
|
||||
"test": "run-s test:prepare test:run-coverage test:report-coverage",
|
||||
"test:prepare": "cargo build --features webserver --bin deltachat-jsonrpc-server",
|
||||
"test:run": "mocha dist/test",
|
||||
"test:run-coverage": "COVERAGE=1 NODE_OPTIONS=--enable-source-maps c8 --include 'dist/*' -r text -r html -r json mocha dist/test",
|
||||
"test:report-coverage": "node report_api_coverage.mjs",
|
||||
"docs": "typedoc --out docs deltachat.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@deltachat/tiny-emitter": "3.0.0",
|
||||
"isomorphic-ws": "^4.0.1",
|
||||
"tiny-emitter": "git+https://github.com/Simon-Laux/tiny-emitter.git",
|
||||
"yerpc": "^0.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
@@ -23,30 +47,5 @@
|
||||
"typedoc": "^0.23.2",
|
||||
"typescript": "^4.5.5",
|
||||
"ws": "^8.5.0"
|
||||
},
|
||||
"license": "MPL-2.0",
|
||||
"main": "dist/deltachat.js",
|
||||
"name": "@deltachat/jsonrpc-client",
|
||||
"scripts": {
|
||||
"build": "run-s generate-bindings extract-constants build:tsc build:bundle",
|
||||
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
|
||||
"build:tsc": "tsc",
|
||||
"docs": "typedoc --out docs deltachat.ts",
|
||||
"example": "run-s build example:build example:start",
|
||||
"example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
|
||||
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
|
||||
"example:start": "http-server .",
|
||||
"extract-constants": "node ./scripts/generate-constants.js",
|
||||
"generate-bindings": "cargo test",
|
||||
"prettier:check": "prettier --check **.ts",
|
||||
"prettier:fix": "prettier --write **.ts",
|
||||
"test": "run-s test:prepare test:run-coverage test:report-coverage",
|
||||
"test:prepare": "cargo build --package deltachat-rpc-server --bin deltachat-rpc-server",
|
||||
"test:report-coverage": "node report_api_coverage.mjs",
|
||||
"test:run": "mocha dist/test",
|
||||
"test:run-coverage": "COVERAGE=1 NODE_OPTIONS=--enable-source-maps c8 --include 'dist/*' -r text -r html -r json mocha dist/test"
|
||||
},
|
||||
"type": "module",
|
||||
"types": "dist/deltachat.d.ts",
|
||||
"version": "1.101.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
import { readFileSync, writeFileSync } from "fs";
|
||||
import { resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { dirname } from "path";
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
|
||||
const data = [];
|
||||
const header = resolve(__dirname, "../../../deltachat-ffi/deltachat.h");
|
||||
|
||||
console.log("Generating constants...");
|
||||
|
||||
const header_data = readFileSync(header, "UTF-8");
|
||||
const regex = /^#define\s+(\w+)\s+(\w+)/gm;
|
||||
let match;
|
||||
while (null != (match = regex.exec(header_data))) {
|
||||
const key = match[1];
|
||||
const value = parseInt(match[2]);
|
||||
if (!isNaN(value)) {
|
||||
data.push({ key, value });
|
||||
}
|
||||
}
|
||||
|
||||
const constants = data
|
||||
.filter(
|
||||
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
|
||||
)
|
||||
.sort((lhs, rhs) => {
|
||||
if (lhs.key < rhs.key) return -1;
|
||||
else if (lhs.key > rhs.key) return 1;
|
||||
return 0;
|
||||
})
|
||||
.filter(({ key }) => {
|
||||
// filter out what we don't need it
|
||||
return !(
|
||||
key.startsWith("DC_EVENT_") ||
|
||||
key.startsWith("DC_IMEX_") ||
|
||||
key.startsWith("DC_CHAT_VISIBILITY") ||
|
||||
key.startsWith("DC_DOWNLOAD") ||
|
||||
key.startsWith("DC_INFO_") ||
|
||||
(key.startsWith("DC_MSG") && !key.startsWith("DC_MSG_ID")) ||
|
||||
key.startsWith("DC_QR_")
|
||||
);
|
||||
})
|
||||
.map((row) => {
|
||||
return ` ${row.key}: ${row.value}`;
|
||||
})
|
||||
.join(",\n");
|
||||
|
||||
writeFileSync(
|
||||
resolve(__dirname, "../generated/constants.ts"),
|
||||
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`
|
||||
);
|
||||
@@ -1,59 +1,40 @@
|
||||
import * as T from "../generated/types.js";
|
||||
import * as RPC from "../generated/jsonrpc.js";
|
||||
import { RawClient } from "../generated/client.js";
|
||||
import { Event } from "../generated/events.js";
|
||||
import { EventTypeName } from "../generated/events.js";
|
||||
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
|
||||
import { TinyEmitter } from "@deltachat/tiny-emitter";
|
||||
import { TinyEmitter } from "tiny-emitter";
|
||||
|
||||
type DCWireEvent<T extends Event> = {
|
||||
event: T;
|
||||
export type DeltaChatEvent = {
|
||||
id: EventTypeName;
|
||||
contextId: number;
|
||||
field1: any;
|
||||
field2: any;
|
||||
};
|
||||
// export type Events = Record<
|
||||
// Event["type"] | "ALL",
|
||||
// (event: DeltaChatEvent<Event>) => void
|
||||
// >;
|
||||
|
||||
type Events = { ALL: (accountId: number, event: Event) => void } & {
|
||||
[Property in Event["type"]]: (
|
||||
accountId: number,
|
||||
event: Extract<Event, { type: Property }>
|
||||
) => void;
|
||||
};
|
||||
|
||||
type ContextEvents = { ALL: (event: Event) => void } & {
|
||||
[Property in Event["type"]]: (
|
||||
event: Extract<Event, { type: Property }>
|
||||
) => void;
|
||||
};
|
||||
|
||||
export type DcEvent = Event;
|
||||
export type DcEventType<T extends Event["type"]> = Extract<Event, { type: T }>;
|
||||
export type Events = Record<
|
||||
EventTypeName | "ALL",
|
||||
(event: DeltaChatEvent) => void
|
||||
>;
|
||||
|
||||
export class BaseDeltaChat<
|
||||
Transport extends BaseTransport<any>
|
||||
> extends TinyEmitter<Events> {
|
||||
rpc: RawClient;
|
||||
account?: T.Account;
|
||||
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
|
||||
private contextEmitters: TinyEmitter<Events>[] = [];
|
||||
constructor(public transport: Transport) {
|
||||
super();
|
||||
this.rpc = new RawClient(this.transport);
|
||||
this.transport.on("request", (request: Request) => {
|
||||
const method = request.method;
|
||||
if (method === "event") {
|
||||
const event = request.params! as DCWireEvent<Event>;
|
||||
//@ts-ignore
|
||||
this.emit(event.event.type, event.contextId, event.event as any);
|
||||
this.emit("ALL", event.contextId, event.event as any);
|
||||
const event = request.params! as DeltaChatEvent;
|
||||
this.emit(event.id, event);
|
||||
this.emit("ALL", event);
|
||||
|
||||
if (this.contextEmitters[event.contextId]) {
|
||||
this.contextEmitters[event.contextId].emit(
|
||||
event.event.type,
|
||||
//@ts-ignore
|
||||
event.event as any
|
||||
);
|
||||
this.contextEmitters[event.contextId].emit("ALL", event.event);
|
||||
this.contextEmitters[event.contextId].emit(event.id, event);
|
||||
this.contextEmitters[event.contextId].emit("ALL", event);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -89,39 +70,8 @@ export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
|
||||
if (typeof opts === "string") opts = { url: opts };
|
||||
if (opts) opts = { ...DEFAULT_OPTS, ...opts };
|
||||
else opts = { ...DEFAULT_OPTS };
|
||||
const transport = new WebsocketTransport(opts.url);
|
||||
const transport = new WebsocketTransport(opts.url)
|
||||
super(transport);
|
||||
this.opts = opts;
|
||||
}
|
||||
}
|
||||
|
||||
export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
|
||||
close() {}
|
||||
constructor(input: any, output: any) {
|
||||
const transport = new StdioTransport(input, output);
|
||||
super(transport);
|
||||
}
|
||||
}
|
||||
|
||||
export class StdioTransport extends BaseTransport {
|
||||
constructor(public input: any, public output: any) {
|
||||
super();
|
||||
|
||||
var buffer = "";
|
||||
this.output.on("data", (data: any) => {
|
||||
buffer += data.toString();
|
||||
while (buffer.includes("\n")) {
|
||||
const n = buffer.indexOf("\n");
|
||||
const line = buffer.substring(0, n);
|
||||
const message = JSON.parse(line);
|
||||
this._onmessage(message);
|
||||
buffer = buffer.substring(n + 1);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
_send(message: RPC.Message): void {
|
||||
const serialized = JSON.stringify(message);
|
||||
this.input.write(serialized + "\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,4 +4,3 @@ export * from "../generated/events.js";
|
||||
export { RawClient } from "../generated/client.js";
|
||||
export * from "./client.js";
|
||||
export * as yerpc from "yerpc";
|
||||
export { C } from "../generated/constants.js";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { strictEqual } from "assert";
|
||||
import chai, { assert, expect } from "chai";
|
||||
import chaiAsPromised from "chai-as-promised";
|
||||
chai.use(chaiAsPromised);
|
||||
import { StdioDeltaChat as DeltaChat } from "../deltachat.js";
|
||||
import { DeltaChat } from "../deltachat.js";
|
||||
|
||||
import {
|
||||
RpcServerHandle,
|
||||
@@ -15,7 +15,9 @@ describe("basic tests", () => {
|
||||
|
||||
before(async () => {
|
||||
serverHandle = await startServer();
|
||||
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout)
|
||||
// make sure server is up by the time we continue
|
||||
await new Promise((res) => setTimeout(res, 100));
|
||||
dc = new DeltaChat(serverHandle.url)
|
||||
// dc.on("ALL", (event) => {
|
||||
//console.log("event", event);
|
||||
// });
|
||||
@@ -82,21 +84,21 @@ describe("basic tests", () => {
|
||||
accountId = await dc.rpc.addAccount();
|
||||
});
|
||||
it("should block and unblock contact", async function () {
|
||||
const contactId = await dc.rpc.createContact(
|
||||
const contactId = await dc.rpc.contactsCreateContact(
|
||||
accountId,
|
||||
"example@delta.chat",
|
||||
null
|
||||
);
|
||||
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
|
||||
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
|
||||
.false;
|
||||
await dc.rpc.blockContact(accountId, contactId);
|
||||
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
|
||||
await dc.rpc.contactsBlock(accountId, contactId);
|
||||
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
|
||||
.true;
|
||||
expect(await dc.rpc.getBlockedContacts(accountId)).to.have.length(1);
|
||||
await dc.rpc.unblockContact(accountId, contactId);
|
||||
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
|
||||
expect(await dc.rpc.contactsGetBlocked(accountId)).to.have.length(1);
|
||||
await dc.rpc.contactsUnblock(accountId, contactId);
|
||||
expect((await dc.rpc.contactsGetContact(accountId, contactId)).isBlocked).to.be
|
||||
.false;
|
||||
expect(await dc.rpc.getBlockedContacts(accountId)).to.have.length(0);
|
||||
expect(await dc.rpc.contactsGetBlocked(accountId)).to.have.length(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -1,8 +1,12 @@
|
||||
import { assert, expect } from "chai";
|
||||
import { StdioDeltaChat as DeltaChat, DcEvent } from "../deltachat.js";
|
||||
import { RpcServerHandle, createTempUser, startServer } from "./test_base.js";
|
||||
import { DeltaChat, DeltaChatEvent, EventTypeName } from "../deltachat.js";
|
||||
import {
|
||||
RpcServerHandle,
|
||||
createTempUser,
|
||||
startServer,
|
||||
} from "./test_base.js";
|
||||
|
||||
const EVENT_TIMEOUT = 20000;
|
||||
const EVENT_TIMEOUT = 20000
|
||||
|
||||
describe("online tests", function () {
|
||||
let serverHandle: RpcServerHandle;
|
||||
@@ -12,7 +16,7 @@ describe("online tests", function () {
|
||||
let accountId1: number, accountId2: number;
|
||||
|
||||
before(async function () {
|
||||
this.timeout(12000);
|
||||
this.timeout(12000)
|
||||
if (!process.env.DCC_NEW_TMP_EMAIL) {
|
||||
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
|
||||
console.error(
|
||||
@@ -27,10 +31,10 @@ describe("online tests", function () {
|
||||
this.skip();
|
||||
}
|
||||
serverHandle = await startServer();
|
||||
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
|
||||
dc = new DeltaChat(serverHandle.url)
|
||||
|
||||
dc.on("ALL", (contextId, { type }) => {
|
||||
if (type !== "Info") console.log(contextId, type);
|
||||
dc.on("ALL", ({ id, contextId }) => {
|
||||
if (id !== "Info") console.log(contextId, id);
|
||||
});
|
||||
|
||||
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
|
||||
@@ -70,7 +74,7 @@ describe("online tests", function () {
|
||||
addr: account2.email,
|
||||
mail_pw: account2.password,
|
||||
});
|
||||
await dc.rpc.configure(accountId2);
|
||||
await dc.rpc.configure(accountId2)
|
||||
accountsConfigured = true;
|
||||
});
|
||||
|
||||
@@ -80,28 +84,28 @@ describe("online tests", function () {
|
||||
}
|
||||
this.timeout(15000);
|
||||
|
||||
const contactId = await dc.rpc.createContact(
|
||||
const contactId = await dc.rpc.contactsCreateContact(
|
||||
accountId1,
|
||||
account2.email,
|
||||
null
|
||||
);
|
||||
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
|
||||
const chatId = await dc.rpc.contactsCreateChatByContactId(accountId1, contactId);
|
||||
const eventPromise = Promise.race([
|
||||
waitForEvent(dc, "MsgsChanged", accountId2),
|
||||
waitForEvent(dc, "IncomingMsg", accountId2),
|
||||
]);
|
||||
|
||||
await dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello");
|
||||
const { chatId: chatIdOnAccountB } = await eventPromise;
|
||||
await dc.rpc.miscSendTextMessage(accountId1, "Hello", chatId);
|
||||
const { field1: chatIdOnAccountB } = await eventPromise;
|
||||
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
|
||||
const messageList = await dc.rpc.getMessageIds(
|
||||
const messageList = await dc.rpc.messageListGetMessageIds(
|
||||
accountId2,
|
||||
chatIdOnAccountB,
|
||||
0
|
||||
);
|
||||
|
||||
expect(messageList).have.length(1);
|
||||
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
|
||||
const message = await dc.rpc.messageGetMessage(accountId2, messageList[0]);
|
||||
expect(message.text).equal("Hello");
|
||||
});
|
||||
|
||||
@@ -112,30 +116,30 @@ describe("online tests", function () {
|
||||
this.timeout(10000);
|
||||
|
||||
// send message from A to B
|
||||
const contactId = await dc.rpc.createContact(
|
||||
const contactId = await dc.rpc.contactsCreateContact(
|
||||
accountId1,
|
||||
account2.email,
|
||||
null
|
||||
);
|
||||
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
|
||||
const chatId = await dc.rpc.contactsCreateChatByContactId(accountId1, contactId);
|
||||
const eventPromise = Promise.race([
|
||||
waitForEvent(dc, "MsgsChanged", accountId2),
|
||||
waitForEvent(dc, "IncomingMsg", accountId2),
|
||||
]);
|
||||
dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello2");
|
||||
dc.rpc.miscSendTextMessage(accountId1, "Hello2", chatId);
|
||||
// wait for message from A
|
||||
console.log("wait for message from A");
|
||||
|
||||
const event = await eventPromise;
|
||||
const { chatId: chatIdOnAccountB } = event;
|
||||
const { field1: chatIdOnAccountB } = event;
|
||||
|
||||
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
|
||||
const messageList = await dc.rpc.getMessageIds(
|
||||
const messageList = await dc.rpc.messageListGetMessageIds(
|
||||
accountId2,
|
||||
chatIdOnAccountB,
|
||||
0
|
||||
);
|
||||
const message = await dc.rpc.getMessage(
|
||||
const message = await dc.rpc.messageGetMessage(
|
||||
accountId2,
|
||||
messageList.reverse()[0]
|
||||
);
|
||||
@@ -145,14 +149,14 @@ describe("online tests", function () {
|
||||
waitForEvent(dc, "MsgsChanged", accountId1),
|
||||
waitForEvent(dc, "IncomingMsg", accountId1),
|
||||
]);
|
||||
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
|
||||
dc.rpc.miscSendTextMessage(accountId2, "super secret message", chatId);
|
||||
// Check if answer arives at A and if it is encrypted
|
||||
await eventPromise2;
|
||||
|
||||
const messageId = (
|
||||
await dc.rpc.getMessageIds(accountId1, chatId, 0)
|
||||
await dc.rpc.messageListGetMessageIds(accountId1, chatId, 0)
|
||||
).reverse()[0];
|
||||
const message2 = await dc.rpc.getMessage(accountId1, messageId);
|
||||
const message2 = await dc.rpc.messageGetMessage(accountId1, messageId);
|
||||
expect(message2.text).equal("super secret message");
|
||||
expect(message2.showPadlock).equal(true);
|
||||
});
|
||||
@@ -175,22 +179,22 @@ describe("online tests", function () {
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForEvent<T extends DcEvent["type"]>(
|
||||
async function waitForEvent(
|
||||
dc: DeltaChat,
|
||||
eventType: T,
|
||||
eventType: EventTypeName,
|
||||
accountId: number,
|
||||
timeout: number = EVENT_TIMEOUT
|
||||
): Promise<Extract<DcEvent, { type: T }>> {
|
||||
): Promise<DeltaChatEvent> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const rejectTimeout = setTimeout(
|
||||
() => reject(new Error("Timeout reached before event came in")),
|
||||
() => reject(new Error('Timeout reached before event came in')),
|
||||
timeout
|
||||
);
|
||||
const callback = (contextId: number, event: DcEvent) => {
|
||||
if (contextId == accountId) {
|
||||
)
|
||||
const callback = (event: DeltaChatEvent) => {
|
||||
if (event.contextId == accountId) {
|
||||
dc.off(eventType, callback);
|
||||
clearTimeout(rejectTimeout);
|
||||
resolve(event as any);
|
||||
clearTimeout(rejectTimeout)
|
||||
resolve(event);
|
||||
}
|
||||
};
|
||||
dc.on(eventType, callback);
|
||||
|
||||
@@ -1,38 +1,38 @@
|
||||
import { tmpdir } from "os";
|
||||
import { join, resolve } from "path";
|
||||
import { mkdtemp, rm } from "fs/promises";
|
||||
import { existsSync } from "fs";
|
||||
import { spawn, exec } from "child_process";
|
||||
import fetch from "node-fetch";
|
||||
import { Readable, Writable } from "node:stream";
|
||||
|
||||
export const RPC_SERVER_PORT = 20808;
|
||||
|
||||
export type RpcServerHandle = {
|
||||
stdin: Writable;
|
||||
stdout: Readable;
|
||||
close: () => Promise<void>;
|
||||
};
|
||||
url: string,
|
||||
close: () => Promise<void>
|
||||
}
|
||||
|
||||
export async function startServer(): Promise<RpcServerHandle> {
|
||||
export async function startServer(port: number = RPC_SERVER_PORT): Promise<RpcServerHandle> {
|
||||
const tmpDir = await mkdtemp(join(tmpdir(), "deltachat-jsonrpc-test"));
|
||||
|
||||
const pathToServerBinary = resolve(
|
||||
join(await getTargetDir(), "debug/deltachat-rpc-server")
|
||||
);
|
||||
const pathToServerBinary = resolve(join(await getTargetDir(), "debug/deltachat-jsonrpc-server"));
|
||||
console.log('using server binary: ' + pathToServerBinary);
|
||||
|
||||
if (!existsSync(pathToServerBinary)) {
|
||||
throw new Error(
|
||||
"server executable does not exist, you need to build it first" +
|
||||
"\nserver executable not found at " +
|
||||
pathToServerBinary
|
||||
);
|
||||
}
|
||||
|
||||
const server = spawn(pathToServerBinary, {
|
||||
cwd: tmpDir,
|
||||
env: {
|
||||
RUST_LOG: process.env.RUST_LOG || "info",
|
||||
RUST_MIN_STACK: "8388608",
|
||||
DC_PORT: '' + port
|
||||
},
|
||||
});
|
||||
|
||||
server.on("error", (err) => {
|
||||
throw new Error(
|
||||
"Failed to start server executable " +
|
||||
pathToServerBinary +
|
||||
", make sure you built it first."
|
||||
);
|
||||
});
|
||||
let shouldClose = false;
|
||||
|
||||
server.on("exit", () => {
|
||||
@@ -43,10 +43,12 @@ export async function startServer(): Promise<RpcServerHandle> {
|
||||
});
|
||||
|
||||
server.stderr.pipe(process.stderr);
|
||||
server.stdout.pipe(process.stdout)
|
||||
|
||||
const url = `ws://localhost:${port}/ws`
|
||||
|
||||
return {
|
||||
stdin: server.stdin,
|
||||
stdout: server.stdout,
|
||||
url,
|
||||
close: async () => {
|
||||
shouldClose = true;
|
||||
if (!server.kill()) {
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
[package]
|
||||
name = "deltachat-rpc-server"
|
||||
version = "1.101.0"
|
||||
description = "DeltaChat JSON-RPC server"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
readme = "README.md"
|
||||
license = "MPL-2.0"
|
||||
|
||||
keywords = ["deltachat", "chat", "openpgp", "email", "encryption"]
|
||||
categories = ["cryptography", "std", "email"]
|
||||
|
||||
[[bin]]
|
||||
name = "deltachat-rpc-server"
|
||||
|
||||
[dependencies]
|
||||
deltachat-jsonrpc = { path = "../deltachat-jsonrpc" }
|
||||
|
||||
anyhow = "1"
|
||||
env_logger = { version = "0.9.1" }
|
||||
futures-lite = "1.12.0"
|
||||
log = "0.4"
|
||||
serde_json = "1.0.85"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
tokio = { version = "1.21.2", features = ["io-std"] }
|
||||
yerpc = { version = "0.3.1", features = ["anyhow_expose"] }
|
||||
@@ -1,68 +0,0 @@
|
||||
///! Delta Chat core RPC server.
|
||||
///!
|
||||
///! It speaks JSON Lines over stdio.
|
||||
use std::path::PathBuf;
|
||||
|
||||
use anyhow::Result;
|
||||
use deltachat_jsonrpc::api::events::event_to_json_rpc_notification;
|
||||
use deltachat_jsonrpc::api::{Accounts, CommandApi};
|
||||
use futures_lite::stream::StreamExt;
|
||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||
use tokio::task::JoinHandle;
|
||||
use yerpc::{RpcClient, RpcSession};
|
||||
|
||||
#[tokio::main(flavor = "multi_thread")]
|
||||
async fn main() -> Result<()> {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||
|
||||
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
|
||||
log::info!("Starting with accounts directory `{}`.", path);
|
||||
let accounts = Accounts::new(PathBuf::from(&path)).await?;
|
||||
let events = accounts.get_event_emitter();
|
||||
|
||||
log::info!("Creating JSON-RPC API.");
|
||||
let state = CommandApi::new(accounts);
|
||||
|
||||
let (client, mut out_receiver) = RpcClient::new();
|
||||
let session = RpcSession::new(client.clone(), state);
|
||||
|
||||
// Events task converts core events to JSON-RPC notifications.
|
||||
let events_task: JoinHandle<Result<()>> = tokio::spawn(async move {
|
||||
while let Some(event) = events.recv().await {
|
||||
let event = event_to_json_rpc_notification(event);
|
||||
client.send_notification("event", Some(event)).await?;
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// Send task prints JSON responses to stdout.
|
||||
let send_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
|
||||
while let Some(message) = out_receiver.next().await {
|
||||
let message = serde_json::to_string(&message)?;
|
||||
log::trace!("RPC send {}", message);
|
||||
println!("{}", message);
|
||||
}
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// Receiver task reads JSON requests from stdin.
|
||||
let recv_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
|
||||
let stdin = io::stdin();
|
||||
let mut lines = BufReader::new(stdin).lines();
|
||||
while let Some(message) = lines.next_line().await? {
|
||||
log::trace!("RPC recv {}", message);
|
||||
session.handle_incoming(&message).await;
|
||||
}
|
||||
log::info!("EOF reached on stdin");
|
||||
Ok(())
|
||||
});
|
||||
|
||||
// Wait for the end of stdin.
|
||||
recv_task.await??;
|
||||
|
||||
// Shutdown the server.
|
||||
send_task.abort();
|
||||
events_task.abort();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
#![allow(clippy::format_push_string)]
|
||||
extern crate dirs;
|
||||
|
||||
use std::path::Path;
|
||||
@@ -20,7 +19,6 @@ use deltachat::log::LogExt;
|
||||
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||
use deltachat::peerstate::*;
|
||||
use deltachat::qr::*;
|
||||
use deltachat::reaction::send_reaction;
|
||||
use deltachat::receive_imf::*;
|
||||
use deltachat::sql;
|
||||
use deltachat::tools::*;
|
||||
@@ -408,7 +406,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
resend <msg-id>\n\
|
||||
markseen <msg-id>\n\
|
||||
delmsg <msg-id>\n\
|
||||
react <msg-id> [<reaction>]\n\
|
||||
===========================Contact commands==\n\
|
||||
listcontacts [<query>]\n\
|
||||
listverified [<query>]\n\
|
||||
@@ -554,11 +551,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
sql::housekeeping(&context).await.ok_or_log(&context);
|
||||
}
|
||||
"listchats" | "listarchived" | "chats" => {
|
||||
let listflags = if arg0 == "listarchived" {
|
||||
DC_GCL_ARCHIVED_ONLY
|
||||
} else {
|
||||
0
|
||||
};
|
||||
let listflags = if arg0 == "listarchived" { 0x01 } else { 0 };
|
||||
let time_start = std::time::SystemTime::now();
|
||||
let chatlist = Chatlist::try_load(
|
||||
&context,
|
||||
@@ -989,9 +982,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
|
||||
}
|
||||
"listmedia" => {
|
||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||
|
||||
let images = chat::get_chat_media(
|
||||
&context,
|
||||
sel_chat.map(|c| c.id),
|
||||
sel_chat.as_ref().unwrap().get_id(),
|
||||
Viewtype::Image,
|
||||
Viewtype::Gif,
|
||||
Viewtype::Video,
|
||||
@@ -1127,12 +1122,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
ids[0] = MsgId::new(arg1.parse()?);
|
||||
message::delete_msgs(&context, &ids).await?;
|
||||
}
|
||||
"react" => {
|
||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||
let msg_id = MsgId::new(arg1.parse()?);
|
||||
let reaction = arg2;
|
||||
send_reaction(&context, msg_id, reaction).await?;
|
||||
}
|
||||
"listcontacts" | "contacts" | "listverified" => {
|
||||
let contacts = Contact::get_all(
|
||||
&context,
|
||||
|
||||
@@ -20,10 +20,10 @@ use deltachat::context::*;
|
||||
use deltachat::oauth2::*;
|
||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
||||
use deltachat::securejoin::*;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::{EventType, Events};
|
||||
use log::{error, info, warn};
|
||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||
use rustyline::config::OutputStreamType;
|
||||
use rustyline::error::ReadlineError;
|
||||
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
|
||||
use rustyline::hint::{Hinter, HistoryHinter};
|
||||
@@ -72,19 +72,6 @@ fn receive_event(event: EventType) {
|
||||
))
|
||||
);
|
||||
}
|
||||
EventType::ReactionsChanged {
|
||||
chat_id,
|
||||
msg_id,
|
||||
contact_id,
|
||||
} => {
|
||||
info!(
|
||||
"{}",
|
||||
yellow.paint(format!(
|
||||
"Received REACTIONS_CHANGED(chat_id={}, msg_id={}, contact_id={})",
|
||||
chat_id, msg_id, contact_id
|
||||
))
|
||||
);
|
||||
}
|
||||
EventType::ContactsChanged(_) => {
|
||||
info!("{}", yellow.paint("Received CONTACTS_CHANGED()"));
|
||||
}
|
||||
@@ -221,7 +208,7 @@ const CHAT_COMMANDS: [&str; 36] = [
|
||||
"accept",
|
||||
"blockchat",
|
||||
];
|
||||
const MESSAGE_COMMANDS: [&str; 9] = [
|
||||
const MESSAGE_COMMANDS: [&str; 8] = [
|
||||
"listmsgs",
|
||||
"msginfo",
|
||||
"listfresh",
|
||||
@@ -230,7 +217,6 @@ const MESSAGE_COMMANDS: [&str; 9] = [
|
||||
"markseen",
|
||||
"delmsg",
|
||||
"download",
|
||||
"react",
|
||||
];
|
||||
const CONTACT_COMMANDS: [&str; 9] = [
|
||||
"listcontacts",
|
||||
@@ -313,7 +299,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
println!("Error: Bad arguments, expected [db-name].");
|
||||
bail!("No db-name specified");
|
||||
}
|
||||
let context = Context::new(Path::new(&args[1]), 0, Events::new(), StockStrings::new()).await?;
|
||||
let context = Context::new(Path::new(&args[1]), 0, Events::new()).await?;
|
||||
|
||||
let events = context.get_event_emitter();
|
||||
tokio::task::spawn(async move {
|
||||
@@ -328,6 +314,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
.history_ignore_space(true)
|
||||
.completion_type(CompletionType::List)
|
||||
.edit_mode(EditMode::Emacs)
|
||||
.output_stream(OutputStreamType::Stdout)
|
||||
.build();
|
||||
let mut selected_chat = ChatId::default();
|
||||
|
||||
@@ -338,7 +325,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||
highlighter: MatchingBracketHighlighter::new(),
|
||||
hinter: HistoryHinter {},
|
||||
};
|
||||
let mut rl = Editor::with_config(config)?;
|
||||
let mut rl = Editor::with_config(config);
|
||||
rl.set_helper(Some(h));
|
||||
rl.bind_sequence(KeyEvent::alt('N'), Cmd::HistorySearchForward);
|
||||
rl.bind_sequence(KeyEvent::alt('P'), Cmd::HistorySearchBackward);
|
||||
@@ -446,7 +433,7 @@ async fn handle_cmd(
|
||||
}
|
||||
println!("{}", qr);
|
||||
let output = Command::new("qrencode")
|
||||
.args(["-t", "ansiutf8", qr.as_str(), "-o", "-"])
|
||||
.args(&["-t", "ansiutf8", qr.as_str(), "-o", "-"])
|
||||
.output()
|
||||
.expect("failed to execute process");
|
||||
io::stdout().write_all(&output.stdout).unwrap();
|
||||
|
||||
@@ -6,7 +6,6 @@ use deltachat::config;
|
||||
use deltachat::contact::*;
|
||||
use deltachat::context::*;
|
||||
use deltachat::message::Message;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::{EventType, Events};
|
||||
|
||||
fn cb(event: EventType) {
|
||||
@@ -37,7 +36,7 @@ async fn main() {
|
||||
let dir = tempdir().unwrap();
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
log::info!("creating database {:?}", dbfile);
|
||||
let ctx = Context::new(&dbfile, 0, Events::new(), StockStrings::new())
|
||||
let ctx = Context::new(&dbfile, 0, Events::new())
|
||||
.await
|
||||
.expect("Failed to create context");
|
||||
let info = ctx.get_info().await;
|
||||
|
||||
@@ -234,7 +234,7 @@ We have the following scripts for building, testing and coverage:
|
||||
The following steps are needed to make a release:
|
||||
|
||||
1. Wait until `pack-module` github action is completed
|
||||
2. Run `npm publish https://download.delta.chat/node/deltachat-node-1.x.x.tar.gz` to publish it to npm. You probably need write rights to npm.
|
||||
2. Run `npm publish https://download.delta.chat/node/deltachat-node-v1.x.x.tar.gz` to publish it to npm. You probably need write rights to npm.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ module.exports = {
|
||||
DC_EVENT_IMEX_FILE_WRITTEN: 2052,
|
||||
DC_EVENT_IMEX_PROGRESS: 2051,
|
||||
DC_EVENT_INCOMING_MSG: 2005,
|
||||
DC_EVENT_INCOMING_MSG_BUNCH: 2006,
|
||||
DC_EVENT_INFO: 100,
|
||||
DC_EVENT_LOCATION_CHANGED: 2035,
|
||||
DC_EVENT_MSGS_CHANGED: 2000,
|
||||
@@ -51,14 +50,12 @@ module.exports = {
|
||||
DC_EVENT_MSG_FAILED: 2012,
|
||||
DC_EVENT_MSG_READ: 2015,
|
||||
DC_EVENT_NEW_BLOB_FILE: 150,
|
||||
DC_EVENT_REACTIONS_CHANGED: 2001,
|
||||
DC_EVENT_SECUREJOIN_INVITER_PROGRESS: 2060,
|
||||
DC_EVENT_SECUREJOIN_JOINER_PROGRESS: 2061,
|
||||
DC_EVENT_SELFAVATAR_CHANGED: 2110,
|
||||
DC_EVENT_SMTP_CONNECTED: 101,
|
||||
DC_EVENT_SMTP_MESSAGE_SENT: 103,
|
||||
DC_EVENT_WARNING: 300,
|
||||
DC_EVENT_WEBXDC_INSTANCE_DELETED: 2121,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE: 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT: 4,
|
||||
DC_GCL_ADD_SELF: 2,
|
||||
@@ -72,19 +69,8 @@ module.exports = {
|
||||
DC_IMEX_EXPORT_SELF_KEYS: 1,
|
||||
DC_IMEX_IMPORT_BACKUP: 12,
|
||||
DC_IMEX_IMPORT_SELF_KEYS: 2,
|
||||
DC_INFO_AUTOCRYPT_SETUP_MESSAGE: 6,
|
||||
DC_INFO_EPHEMERAL_TIMER_CHANGED: 10,
|
||||
DC_INFO_GROUP_IMAGE_CHANGED: 3,
|
||||
DC_INFO_GROUP_NAME_CHANGED: 2,
|
||||
DC_INFO_LOCATIONSTREAMING_ENABLED: 8,
|
||||
DC_INFO_LOCATION_ONLY: 9,
|
||||
DC_INFO_MEMBER_ADDED_TO_GROUP: 4,
|
||||
DC_INFO_MEMBER_REMOVED_FROM_GROUP: 5,
|
||||
DC_INFO_PROTECTION_DISABLED: 12,
|
||||
DC_INFO_PROTECTION_ENABLED: 11,
|
||||
DC_INFO_SECURE_JOIN_MESSAGE: 7,
|
||||
DC_INFO_UNKNOWN: 0,
|
||||
DC_INFO_WEBXDC_INFO_MESSAGE: 32,
|
||||
DC_KEY_GEN_DEFAULT: 0,
|
||||
DC_KEY_GEN_ED25519: 2,
|
||||
DC_KEY_GEN_RSA2048: 1,
|
||||
@@ -116,7 +102,6 @@ module.exports = {
|
||||
DC_QR_FPR_MISMATCH: 220,
|
||||
DC_QR_FPR_OK: 210,
|
||||
DC_QR_FPR_WITHOUT_ADDR: 230,
|
||||
DC_QR_LOGIN: 520,
|
||||
DC_QR_REVIVE_VERIFYCONTACT: 510,
|
||||
DC_QR_REVIVE_VERIFYGROUP: 512,
|
||||
DC_QR_TEXT: 330,
|
||||
@@ -143,10 +128,6 @@ module.exports = {
|
||||
DC_STATE_UNDEFINED: 0,
|
||||
DC_STR_AC_SETUP_MSG_BODY: 43,
|
||||
DC_STR_AC_SETUP_MSG_SUBJECT: 42,
|
||||
DC_STR_ADD_MEMBER_BY_OTHER: 129,
|
||||
DC_STR_ADD_MEMBER_BY_YOU: 128,
|
||||
DC_STR_AEAP_ADDR_CHANGED: 122,
|
||||
DC_STR_AEAP_EXPLANATION_AND_LINK: 123,
|
||||
DC_STR_ARCHIVEDCHATS: 40,
|
||||
DC_STR_AUDIO: 11,
|
||||
DC_STR_BAD_TIME_MSG_BODY: 85,
|
||||
@@ -177,26 +158,6 @@ module.exports = {
|
||||
DC_STR_EPHEMERAL_MINUTE: 77,
|
||||
DC_STR_EPHEMERAL_MINUTES: 93,
|
||||
DC_STR_EPHEMERAL_SECONDS: 76,
|
||||
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER: 147,
|
||||
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU: 146,
|
||||
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER: 145,
|
||||
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU: 144,
|
||||
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER: 143,
|
||||
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU: 142,
|
||||
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER: 149,
|
||||
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU: 148,
|
||||
DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER: 155,
|
||||
DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU: 154,
|
||||
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER: 139,
|
||||
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU: 138,
|
||||
DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER: 153,
|
||||
DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU: 152,
|
||||
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER: 151,
|
||||
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU: 150,
|
||||
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER: 141,
|
||||
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU: 140,
|
||||
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER: 157,
|
||||
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU: 156,
|
||||
DC_STR_EPHEMERAL_WEEK: 80,
|
||||
DC_STR_EPHEMERAL_WEEKS: 96,
|
||||
DC_STR_ERROR: 112,
|
||||
@@ -206,20 +167,10 @@ module.exports = {
|
||||
DC_STR_FINGERPRINTS: 30,
|
||||
DC_STR_FORWARDED: 97,
|
||||
DC_STR_GIF: 23,
|
||||
DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER: 127,
|
||||
DC_STR_GROUP_IMAGE_CHANGED_BY_YOU: 126,
|
||||
DC_STR_GROUP_IMAGE_DELETED_BY_OTHER: 135,
|
||||
DC_STR_GROUP_IMAGE_DELETED_BY_YOU: 134,
|
||||
DC_STR_GROUP_LEFT_BY_OTHER: 133,
|
||||
DC_STR_GROUP_LEFT_BY_YOU: 132,
|
||||
DC_STR_GROUP_NAME_CHANGED_BY_OTHER: 125,
|
||||
DC_STR_GROUP_NAME_CHANGED_BY_YOU: 124,
|
||||
DC_STR_IMAGE: 9,
|
||||
DC_STR_INCOMING_MESSAGES: 103,
|
||||
DC_STR_LAST_MSG_SENT_SUCCESSFULLY: 111,
|
||||
DC_STR_LOCATION: 66,
|
||||
DC_STR_LOCATION_ENABLED_BY_OTHER: 137,
|
||||
DC_STR_LOCATION_ENABLED_BY_YOU: 136,
|
||||
DC_STR_MESSAGES: 114,
|
||||
DC_STR_MSGACTIONBYME: 63,
|
||||
DC_STR_MSGACTIONBYUSER: 62,
|
||||
@@ -239,16 +190,10 @@ module.exports = {
|
||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
|
||||
DC_STR_PART_OF_TOTAL_USED: 116,
|
||||
DC_STR_PROTECTION_DISABLED: 89,
|
||||
DC_STR_PROTECTION_DISABLED_BY_OTHER: 161,
|
||||
DC_STR_PROTECTION_DISABLED_BY_YOU: 160,
|
||||
DC_STR_PROTECTION_ENABLED: 88,
|
||||
DC_STR_PROTECTION_ENABLED_BY_OTHER: 159,
|
||||
DC_STR_PROTECTION_ENABLED_BY_YOU: 158,
|
||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
|
||||
DC_STR_READRCPT: 31,
|
||||
DC_STR_READRCPT_MAILBODY: 32,
|
||||
DC_STR_REMOVE_MEMBER_BY_OTHER: 131,
|
||||
DC_STR_REMOVE_MEMBER_BY_YOU: 130,
|
||||
DC_STR_REPLY_NOUN: 90,
|
||||
DC_STR_SAVED_MESSAGES: 69,
|
||||
DC_STR_SECURE_JOIN_GROUP_QR_DESC: 120,
|
||||
|
||||
@@ -14,9 +14,7 @@ module.exports = {
|
||||
400: 'DC_EVENT_ERROR',
|
||||
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||
2001: 'DC_EVENT_REACTIONS_CHANGED',
|
||||
2005: 'DC_EVENT_INCOMING_MSG',
|
||||
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
|
||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||
2010: 'DC_EVENT_MSG_DELIVERED',
|
||||
2012: 'DC_EVENT_MSG_FAILED',
|
||||
@@ -32,6 +30,5 @@ module.exports = {
|
||||
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
|
||||
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
|
||||
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED'
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE'
|
||||
}
|
||||
|
||||
@@ -39,10 +39,6 @@ export class Chat {
|
||||
return binding.dcn_chat_get_name(this.dc_chat)
|
||||
}
|
||||
|
||||
getMailinglistAddr(): string {
|
||||
return binding.dcn_chat_get_mailinglist_addr(this.dc_chat)
|
||||
}
|
||||
|
||||
getProfileImage(): string {
|
||||
return binding.dcn_chat_get_profile_image(this.dc_chat)
|
||||
}
|
||||
@@ -96,7 +92,6 @@ export class Chat {
|
||||
color: this.color,
|
||||
id: this.getId(),
|
||||
name: this.getName(),
|
||||
mailinglistAddr: this.getMailinglistAddr(),
|
||||
profileImage: this.getProfileImage(),
|
||||
type: this.getType(),
|
||||
isSelfTalk: this.isSelfTalk(),
|
||||
|
||||
@@ -42,7 +42,6 @@ export enum C {
|
||||
DC_EVENT_IMEX_FILE_WRITTEN = 2052,
|
||||
DC_EVENT_IMEX_PROGRESS = 2051,
|
||||
DC_EVENT_INCOMING_MSG = 2005,
|
||||
DC_EVENT_INCOMING_MSG_BUNCH = 2006,
|
||||
DC_EVENT_INFO = 100,
|
||||
DC_EVENT_LOCATION_CHANGED = 2035,
|
||||
DC_EVENT_MSGS_CHANGED = 2000,
|
||||
@@ -51,14 +50,12 @@ export enum C {
|
||||
DC_EVENT_MSG_FAILED = 2012,
|
||||
DC_EVENT_MSG_READ = 2015,
|
||||
DC_EVENT_NEW_BLOB_FILE = 150,
|
||||
DC_EVENT_REACTIONS_CHANGED = 2001,
|
||||
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060,
|
||||
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061,
|
||||
DC_EVENT_SELFAVATAR_CHANGED = 2110,
|
||||
DC_EVENT_SMTP_CONNECTED = 101,
|
||||
DC_EVENT_SMTP_MESSAGE_SENT = 103,
|
||||
DC_EVENT_WARNING = 300,
|
||||
DC_EVENT_WEBXDC_INSTANCE_DELETED = 2121,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT = 4,
|
||||
DC_GCL_ADD_SELF = 2,
|
||||
@@ -72,19 +69,8 @@ export enum C {
|
||||
DC_IMEX_EXPORT_SELF_KEYS = 1,
|
||||
DC_IMEX_IMPORT_BACKUP = 12,
|
||||
DC_IMEX_IMPORT_SELF_KEYS = 2,
|
||||
DC_INFO_AUTOCRYPT_SETUP_MESSAGE = 6,
|
||||
DC_INFO_EPHEMERAL_TIMER_CHANGED = 10,
|
||||
DC_INFO_GROUP_IMAGE_CHANGED = 3,
|
||||
DC_INFO_GROUP_NAME_CHANGED = 2,
|
||||
DC_INFO_LOCATIONSTREAMING_ENABLED = 8,
|
||||
DC_INFO_LOCATION_ONLY = 9,
|
||||
DC_INFO_MEMBER_ADDED_TO_GROUP = 4,
|
||||
DC_INFO_MEMBER_REMOVED_FROM_GROUP = 5,
|
||||
DC_INFO_PROTECTION_DISABLED = 12,
|
||||
DC_INFO_PROTECTION_ENABLED = 11,
|
||||
DC_INFO_SECURE_JOIN_MESSAGE = 7,
|
||||
DC_INFO_UNKNOWN = 0,
|
||||
DC_INFO_WEBXDC_INFO_MESSAGE = 32,
|
||||
DC_KEY_GEN_DEFAULT = 0,
|
||||
DC_KEY_GEN_ED25519 = 2,
|
||||
DC_KEY_GEN_RSA2048 = 1,
|
||||
@@ -116,7 +102,6 @@ export enum C {
|
||||
DC_QR_FPR_MISMATCH = 220,
|
||||
DC_QR_FPR_OK = 210,
|
||||
DC_QR_FPR_WITHOUT_ADDR = 230,
|
||||
DC_QR_LOGIN = 520,
|
||||
DC_QR_REVIVE_VERIFYCONTACT = 510,
|
||||
DC_QR_REVIVE_VERIFYGROUP = 512,
|
||||
DC_QR_TEXT = 330,
|
||||
@@ -143,10 +128,6 @@ export enum C {
|
||||
DC_STATE_UNDEFINED = 0,
|
||||
DC_STR_AC_SETUP_MSG_BODY = 43,
|
||||
DC_STR_AC_SETUP_MSG_SUBJECT = 42,
|
||||
DC_STR_ADD_MEMBER_BY_OTHER = 129,
|
||||
DC_STR_ADD_MEMBER_BY_YOU = 128,
|
||||
DC_STR_AEAP_ADDR_CHANGED = 122,
|
||||
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
|
||||
DC_STR_ARCHIVEDCHATS = 40,
|
||||
DC_STR_AUDIO = 11,
|
||||
DC_STR_BAD_TIME_MSG_BODY = 85,
|
||||
@@ -177,26 +158,6 @@ export enum C {
|
||||
DC_STR_EPHEMERAL_MINUTE = 77,
|
||||
DC_STR_EPHEMERAL_MINUTES = 93,
|
||||
DC_STR_EPHEMERAL_SECONDS = 76,
|
||||
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_OTHER = 147,
|
||||
DC_STR_EPHEMERAL_TIMER_1_DAY_BY_YOU = 146,
|
||||
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_OTHER = 145,
|
||||
DC_STR_EPHEMERAL_TIMER_1_HOUR_BY_YOU = 144,
|
||||
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_OTHER = 143,
|
||||
DC_STR_EPHEMERAL_TIMER_1_MINUTE_BY_YOU = 142,
|
||||
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_OTHER = 149,
|
||||
DC_STR_EPHEMERAL_TIMER_1_WEEK_BY_YOU = 148,
|
||||
DC_STR_EPHEMERAL_TIMER_DAYS_BY_OTHER = 155,
|
||||
DC_STR_EPHEMERAL_TIMER_DAYS_BY_YOU = 154,
|
||||
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_OTHER = 139,
|
||||
DC_STR_EPHEMERAL_TIMER_DISABLED_BY_YOU = 138,
|
||||
DC_STR_EPHEMERAL_TIMER_HOURS_BY_OTHER = 153,
|
||||
DC_STR_EPHEMERAL_TIMER_HOURS_BY_YOU = 152,
|
||||
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_OTHER = 151,
|
||||
DC_STR_EPHEMERAL_TIMER_MINUTES_BY_YOU = 150,
|
||||
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_OTHER = 141,
|
||||
DC_STR_EPHEMERAL_TIMER_SECONDS_BY_YOU = 140,
|
||||
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER = 157,
|
||||
DC_STR_EPHEMERAL_TIMER_WEEKS_BY_YOU = 156,
|
||||
DC_STR_EPHEMERAL_WEEK = 80,
|
||||
DC_STR_EPHEMERAL_WEEKS = 96,
|
||||
DC_STR_ERROR = 112,
|
||||
@@ -206,20 +167,10 @@ export enum C {
|
||||
DC_STR_FINGERPRINTS = 30,
|
||||
DC_STR_FORWARDED = 97,
|
||||
DC_STR_GIF = 23,
|
||||
DC_STR_GROUP_IMAGE_CHANGED_BY_OTHER = 127,
|
||||
DC_STR_GROUP_IMAGE_CHANGED_BY_YOU = 126,
|
||||
DC_STR_GROUP_IMAGE_DELETED_BY_OTHER = 135,
|
||||
DC_STR_GROUP_IMAGE_DELETED_BY_YOU = 134,
|
||||
DC_STR_GROUP_LEFT_BY_OTHER = 133,
|
||||
DC_STR_GROUP_LEFT_BY_YOU = 132,
|
||||
DC_STR_GROUP_NAME_CHANGED_BY_OTHER = 125,
|
||||
DC_STR_GROUP_NAME_CHANGED_BY_YOU = 124,
|
||||
DC_STR_IMAGE = 9,
|
||||
DC_STR_INCOMING_MESSAGES = 103,
|
||||
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
|
||||
DC_STR_LOCATION = 66,
|
||||
DC_STR_LOCATION_ENABLED_BY_OTHER = 137,
|
||||
DC_STR_LOCATION_ENABLED_BY_YOU = 136,
|
||||
DC_STR_MESSAGES = 114,
|
||||
DC_STR_MSGACTIONBYME = 63,
|
||||
DC_STR_MSGACTIONBYUSER = 62,
|
||||
@@ -239,16 +190,10 @@ export enum C {
|
||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
|
||||
DC_STR_PART_OF_TOTAL_USED = 116,
|
||||
DC_STR_PROTECTION_DISABLED = 89,
|
||||
DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
|
||||
DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
|
||||
DC_STR_PROTECTION_ENABLED = 88,
|
||||
DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
|
||||
DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
|
||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
|
||||
DC_STR_READRCPT = 31,
|
||||
DC_STR_READRCPT_MAILBODY = 32,
|
||||
DC_STR_REMOVE_MEMBER_BY_OTHER = 131,
|
||||
DC_STR_REMOVE_MEMBER_BY_YOU = 130,
|
||||
DC_STR_REPLY_NOUN = 90,
|
||||
DC_STR_SAVED_MESSAGES = 69,
|
||||
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
|
||||
@@ -295,9 +240,7 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
400: 'DC_EVENT_ERROR',
|
||||
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||
2001: 'DC_EVENT_REACTIONS_CHANGED',
|
||||
2005: 'DC_EVENT_INCOMING_MSG',
|
||||
2006: 'DC_EVENT_INCOMING_MSG_BUNCH',
|
||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||
2010: 'DC_EVENT_MSG_DELIVERED',
|
||||
2012: 'DC_EVENT_MSG_FAILED',
|
||||
@@ -314,5 +257,4 @@ export const EventId2EventName: { [key: number]: string } = {
|
||||
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
|
||||
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED',
|
||||
}
|
||||
|
||||
@@ -72,10 +72,6 @@ export class Contact {
|
||||
return binding.dcn_contact_get_last_seen(this.dc_contact)
|
||||
}
|
||||
|
||||
wasSeenRecently() {
|
||||
return Boolean(binding.dcn_contact_was_seen_recently(this.dc_contact))
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return binding.dcn_contact_get_name(this.dc_contact)
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ export class AccountManager extends EventEmitter {
|
||||
debug('Started event handler')
|
||||
}
|
||||
|
||||
startJsonRpcHandler(callback: ((response: string) => void) | null) {
|
||||
startJsonRpcHandler(callback: ((response: string) => void) | null, apiVersion: string = "v0") {
|
||||
if (this.dcn_accounts === null) {
|
||||
throw new Error('dcn_account is null')
|
||||
}
|
||||
@@ -126,7 +126,7 @@ export class AccountManager extends EventEmitter {
|
||||
throw new Error('jsonrpc was started already')
|
||||
}
|
||||
|
||||
binding.dcn_accounts_start_jsonrpc(this.dcn_accounts, callback.bind(this))
|
||||
binding.dcn_accounts_start_jsonrpc(this.dcn_accounts, apiVersion, callback.bind(this))
|
||||
debug('Started JSON-RPC handler')
|
||||
this.jsonRpcStarted = true
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ export interface ChatJSON {
|
||||
color: string
|
||||
id: number
|
||||
name: string
|
||||
mailinglistAddr: string
|
||||
profileImage: string
|
||||
type: number
|
||||
isSelfTalk: boolean
|
||||
|
||||
@@ -12,12 +12,11 @@ const GITHUB_API_URL =
|
||||
|
||||
const file_url = process.env['URL']
|
||||
const GITHUB_TOKEN = process.env['GITHUB_TOKEN']
|
||||
const context = process.env['MSG_CONTEXT']
|
||||
|
||||
const STATUS_DATA = {
|
||||
state: 'success',
|
||||
description: '⏩ Click on "Details" to download →',
|
||||
context: context || 'Download the node-bindings.tar.gz',
|
||||
context: 'Download the node-bindings.tar.gz',
|
||||
target_url: base_url + file_url,
|
||||
}
|
||||
|
||||
|
||||
28
node/segfault.js
Normal file
28
node/segfault.js
Normal file
@@ -0,0 +1,28 @@
|
||||
const { default: dc } = require('./dist')
|
||||
|
||||
const ac = new dc('test1233490')
|
||||
|
||||
console.log("[1]");
|
||||
|
||||
ac.startJsonRpcHandler(console.log)
|
||||
|
||||
console.log("[2]");
|
||||
console.log(
|
||||
ac.jsonRpcRequest(
|
||||
JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'get_all_account_ids',
|
||||
params: [],
|
||||
id: 2,
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
console.log("[3]");
|
||||
|
||||
setTimeout(() => {
|
||||
console.log("[4]");
|
||||
ac.close() // This segfaults -> TODO Findout why?
|
||||
|
||||
console.log('still living')
|
||||
}, 1000)
|
||||
50
node/segfault2.js
Normal file
50
node/segfault2.js
Normal file
@@ -0,0 +1,50 @@
|
||||
const { default: dc } = require('./dist')
|
||||
|
||||
const ac = new dc('test1233490')
|
||||
|
||||
console.log('[1]')
|
||||
|
||||
ac.startJsonRpcHandler(console.log)
|
||||
|
||||
console.log('[2]')
|
||||
console.log(
|
||||
ac.jsonRpcRequest(
|
||||
JSON.stringify({
|
||||
jsonrpc: '2.0',
|
||||
method: 'batch_set_config',
|
||||
id: 3,
|
||||
params: [
|
||||
69,
|
||||
{
|
||||
addr: '',
|
||||
mail_user: '',
|
||||
mail_pw: '',
|
||||
mail_server: '',
|
||||
mail_port: '',
|
||||
mail_security: '',
|
||||
imap_certificate_checks: '',
|
||||
send_user: '',
|
||||
send_pw: '',
|
||||
send_server: '',
|
||||
send_port: '',
|
||||
send_security: '',
|
||||
smtp_certificate_checks: '',
|
||||
socks5_enabled: '0',
|
||||
socks5_host: '',
|
||||
socks5_port: '',
|
||||
socks5_user: '',
|
||||
socks5_password: '',
|
||||
},
|
||||
],
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
console.log('[3]')
|
||||
|
||||
setTimeout(() => {
|
||||
console.log('[4]')
|
||||
ac.close() // This segfaults -> TODO Findout why?
|
||||
|
||||
console.log('still living')
|
||||
}, 1000)
|
||||
@@ -1628,18 +1628,6 @@ NAPI_METHOD(dcn_chat_get_name) {
|
||||
NAPI_RETURN_AND_UNREF_STRING(name);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_chat_get_mailinglist_addr) {
|
||||
NAPI_ARGV(1);
|
||||
NAPI_DC_CHAT();
|
||||
|
||||
//TRACE("calling..");
|
||||
char* addr = dc_chat_get_mailinglist_addr(dc_chat);
|
||||
//TRACE("result %s", name);
|
||||
|
||||
NAPI_RETURN_AND_UNREF_STRING(addr);
|
||||
}
|
||||
|
||||
|
||||
NAPI_METHOD(dcn_chat_get_profile_image) {
|
||||
NAPI_ARGV(1);
|
||||
NAPI_DC_CHAT();
|
||||
@@ -1930,13 +1918,6 @@ NAPI_METHOD(dcn_contact_get_last_seen) {
|
||||
NAPI_RETURN_INT64(timestamp);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_contact_was_seen_recently) {
|
||||
NAPI_ARGV(1);
|
||||
NAPI_DC_CONTACT();
|
||||
int seen_recently = dc_contact_was_seen_recently(dc_contact);
|
||||
NAPI_RETURN_UINT32(seen_recently);
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_contact_is_blocked) {
|
||||
NAPI_ARGV(1);
|
||||
NAPI_DC_CONTACT();
|
||||
@@ -3112,14 +3093,14 @@ static void accounts_event_handler_thread_func(void* arg)
|
||||
|
||||
TRACE("event_handler_thread_func starting");
|
||||
|
||||
dc_event_emitter_t * dc_event_emitter = dc_accounts_get_event_emitter(dcn_accounts->dc_accounts);
|
||||
dc_accounts_event_emitter_t * dc_accounts_event_emitter = dc_accounts_get_event_emitter(dcn_accounts->dc_accounts);
|
||||
dc_event_t* event;
|
||||
while (true) {
|
||||
if (dc_event_emitter == NULL) {
|
||||
if (dc_accounts_event_emitter == NULL) {
|
||||
TRACE("event emitter is null, bailing");
|
||||
break;
|
||||
}
|
||||
event = dc_get_next_event(dc_event_emitter);
|
||||
event = dc_accounts_get_next_event(dc_accounts_event_emitter);
|
||||
if (event == NULL) {
|
||||
TRACE("no more events");
|
||||
break;
|
||||
@@ -3145,7 +3126,7 @@ static void accounts_event_handler_thread_func(void* arg)
|
||||
}
|
||||
}
|
||||
|
||||
dc_event_emitter_unref(dc_event_emitter);
|
||||
dc_accounts_event_emitter_unref(dc_accounts_event_emitter);
|
||||
|
||||
TRACE("event_handler_thread_func ended");
|
||||
|
||||
@@ -3332,9 +3313,10 @@ static void call_accounts_js_jsonrpc_handler(napi_env env, napi_value js_callbac
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_accounts_start_jsonrpc) {
|
||||
NAPI_ARGV(2);
|
||||
NAPI_ARGV(3);
|
||||
NAPI_DCN_ACCOUNTS();
|
||||
napi_value callback = argv[1];
|
||||
NAPI_ARGV_UTF8_MALLOC(api_version, 1);
|
||||
napi_value callback = argv[2];
|
||||
|
||||
TRACE("calling..");
|
||||
napi_value async_resource_name;
|
||||
@@ -3357,7 +3339,7 @@ NAPI_METHOD(dcn_accounts_start_jsonrpc) {
|
||||
TRACE("done");
|
||||
|
||||
dcn_accounts->gc = 0;
|
||||
dcn_accounts->jsonrpc_instance = dc_jsonrpc_init(dcn_accounts->dc_accounts);
|
||||
dcn_accounts->jsonrpc_instance = dc_jsonrpc_init(dcn_accounts->dc_accounts, api_version);
|
||||
|
||||
TRACE("creating uv thread..");
|
||||
uv_thread_create(&dcn_accounts->jsonrpc_thread, accounts_jsonrpc_thread_func, dcn_accounts);
|
||||
@@ -3514,7 +3496,6 @@ NAPI_INIT() {
|
||||
NAPI_EXPORT_FUNCTION(dcn_chat_get_visibility);
|
||||
NAPI_EXPORT_FUNCTION(dcn_chat_get_id);
|
||||
NAPI_EXPORT_FUNCTION(dcn_chat_get_name);
|
||||
NAPI_EXPORT_FUNCTION(dcn_chat_get_mailinglist_addr);
|
||||
NAPI_EXPORT_FUNCTION(dcn_chat_get_profile_image);
|
||||
NAPI_EXPORT_FUNCTION(dcn_chat_get_type);
|
||||
NAPI_EXPORT_FUNCTION(dcn_chat_is_self_talk);
|
||||
|
||||
@@ -89,11 +89,7 @@ describe('JSON RPC', function () {
|
||||
const { dc } = DeltaChat.newTemporary()
|
||||
let promise_resolve
|
||||
const promise = new Promise((res, _rej) => {
|
||||
promise_resolve = (response) => {
|
||||
// ignore events
|
||||
const answer = JSON.parse(response)
|
||||
if (answer['method'] !== 'event') res(answer)
|
||||
}
|
||||
promise_resolve = res
|
||||
})
|
||||
dc.startJsonRpcHandler(promise_resolve)
|
||||
dc.jsonRpcRequest(
|
||||
@@ -110,7 +106,7 @@ describe('JSON RPC', function () {
|
||||
id: 2,
|
||||
result: [1],
|
||||
},
|
||||
await promise
|
||||
JSON.parse(await promise)
|
||||
)
|
||||
dc.close()
|
||||
})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@deltachat/jsonrpc-client": "file:deltachat-jsonrpc/typescript",
|
||||
"debug": "^4.1.1",
|
||||
"napi-macros": "^2.0.0",
|
||||
"node-gyp-build": "^4.1.0"
|
||||
@@ -57,8 +58,8 @@
|
||||
"prebuildify": "cd node && prebuildify -t 16.13.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
|
||||
"test": "npm run test:lint && npm run test:mocha",
|
||||
"test:lint": "npm run lint",
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.101.0"
|
||||
"version": "1.87.0"
|
||||
}
|
||||
@@ -1,77 +1,84 @@
|
||||
=========================
|
||||
DeltaChat Python bindings
|
||||
deltachat python bindings
|
||||
=========================
|
||||
|
||||
This package provides `Python bindings`_ to the `deltachat-core library`_
|
||||
which implements IMAP/SMTP/MIME/OpenPGP e-mail standards and offers
|
||||
This package provides bindings to the deltachat-core_ Rust -library
|
||||
which implements IMAP/SMTP/MIME/PGP e-mail standards and offers
|
||||
a low-level Chat/Contact/Message API to user interfaces and bots.
|
||||
|
||||
.. _`deltachat-core library`: https://github.com/deltachat/deltachat-core-rust
|
||||
.. _`Python bindings`: https://py.delta.chat/
|
||||
|
||||
Installing pre-built packages (Linux-only)
|
||||
==========================================
|
||||
========================================================
|
||||
|
||||
If you have a Linux system you may try to install the ``deltachat`` binary "wheel" packages
|
||||
without any "build-from-source" steps.
|
||||
Otherwise you need to `compile the Delta Chat bindings yourself`__.
|
||||
|
||||
__ sourceinstall_
|
||||
Otherwise you need to `compile the Delta Chat bindings yourself <#sourceinstall>`_.
|
||||
|
||||
We recommend to first `install virtualenv <https://virtualenv.pypa.io/en/stable/installation.html>`_,
|
||||
then create a fresh Python virtual environment and activate it in your shell::
|
||||
|
||||
virtualenv env # or: python -m venv
|
||||
source env/bin/activate
|
||||
virtualenv venv # or: python -m venv
|
||||
source venv/bin/activate
|
||||
|
||||
Afterwards, invoking ``python`` or ``pip install`` only
|
||||
modifies files in your ``env`` directory and leaves
|
||||
modifies files in your ``venv`` directory and leaves
|
||||
your system installation alone.
|
||||
|
||||
For Linux we build wheels for all releases and push them to a python package
|
||||
index. To install the latest release::
|
||||
For Linux, we automatically build wheels for all github PR branches
|
||||
and push them to a python package index. To install the latest
|
||||
github ``master`` branch::
|
||||
|
||||
pip install deltachat
|
||||
pip install --pre -i https://m.devpi.net/dc/master deltachat
|
||||
|
||||
To verify it worked::
|
||||
|
||||
python -c "import deltachat"
|
||||
|
||||
.. note::
|
||||
|
||||
If you can help to automate the building of wheels for Mac or Windows,
|
||||
that'd be much appreciated! please then get
|
||||
`in contact with us <https://delta.chat/en/contribute>`_.
|
||||
|
||||
|
||||
Running tests
|
||||
=============
|
||||
|
||||
Recommended way to run tests is using `tox <https://tox.wiki>`_.
|
||||
After successful binding installation you can install tox
|
||||
and run the tests::
|
||||
After successful binding installation you can install a few more
|
||||
Python packages before running the tests::
|
||||
|
||||
pip install tox
|
||||
tox -e py3
|
||||
python -m pip install pytest pytest-xdist pytest-timeout pytest-rerunfailures requests
|
||||
pytest -v tests
|
||||
|
||||
This will run all "offline" tests and skip all functional
|
||||
end-to-end tests that require accounts on real e-mail servers.
|
||||
|
||||
.. _livetests:
|
||||
|
||||
Running "live" tests with temporary accounts
|
||||
--------------------------------------------
|
||||
running "live" tests with temporary accounts
|
||||
---------------------------------------------
|
||||
|
||||
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL`` to a URL that creates e-mail accounts. Most developers use https://testrun.org URLs created and managed by `mailadm <https://mailadm.readthedocs.io/>`_.
|
||||
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL`` to a URL that creates e-mail accounts. Most developers use https://testrun.org URLS created and managed by [mailadm](https://mailadm.readthedocs.io/en/latest/).
|
||||
|
||||
Please feel free to contact us through a github issue or by e-mail and we'll send you a URL that you can then use for functional tests like this::
|
||||
Please feel free to contact us through a github issue or by e-mail and we'll send you a URL that you can then use for functional tests like this:
|
||||
|
||||
export DCC_NEW_TMP_EMAIL=<URL you got from us>
|
||||
|
||||
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the http://testrun.org server. These accounts exists only for one hour and then are removed completely.
|
||||
One hour is enough to invoke pytest and run all offline and online tests::
|
||||
One hour is enough to invoke pytest and run all offline and online tests:
|
||||
|
||||
tox -e py3
|
||||
pytest
|
||||
|
||||
# or if you have installed pytest-xdist for parallel test execution
|
||||
pytest -n6
|
||||
|
||||
Each test run creates new accounts.
|
||||
|
||||
|
||||
.. _sourceinstall:
|
||||
|
||||
Installing bindings from source
|
||||
===============================
|
||||
Installing bindings from source (Updated: July 2020)
|
||||
=========================================================
|
||||
|
||||
Install Rust and Cargo first.
|
||||
The easiest is probably to use `rustup <https://rustup.rs/>`_.
|
||||
@@ -90,42 +97,74 @@ E.g. on Debian-based systems `apt install python3 python3-pip
|
||||
python3-venv` should give you a usable python installation.
|
||||
|
||||
Ensure you are in the deltachat-core-rust/python directory, create the
|
||||
virtual environment with dependencies using tox
|
||||
and activate it in your shell::
|
||||
virtual environment and activate it in your shell::
|
||||
|
||||
cd python
|
||||
tox --devenv env
|
||||
source env/bin/activate
|
||||
python3 -m venv venv # or: virtualenv venv
|
||||
source venv/bin/activate
|
||||
|
||||
You should now be able to build the python bindings using the supplied script::
|
||||
|
||||
python3 install_python_bindings.py
|
||||
python install_python_bindings.py
|
||||
|
||||
The core compilation and bindings building might take a while,
|
||||
depending on the speed of your machine.
|
||||
The bindings will be installed in release mode but with debug symbols.
|
||||
The release mode is currently necessary because some tests generate RSA keys
|
||||
which is prohibitively slow in non-release mode.
|
||||
|
||||
|
||||
Code examples
|
||||
=============
|
||||
|
||||
You may look at `examples <https://py.delta.chat/examples.html>`_.
|
||||
|
||||
|
||||
.. _`deltachat-core-rust github repository`: https://github.com/deltachat/deltachat-core-rust
|
||||
.. _`deltachat-core`: https://github.com/deltachat/deltachat-core-rust
|
||||
|
||||
|
||||
Building manylinux based wheels
|
||||
===============================
|
||||
====================================
|
||||
|
||||
Building portable manylinux wheels which come with libdeltachat.so
|
||||
can be done with Docker_ or Podman_.
|
||||
can be done with docker-tooling.
|
||||
|
||||
.. _Docker: https://www.docker.com/
|
||||
.. _Podman: https://podman.io/
|
||||
using docker pull / premade images
|
||||
------------------------------------
|
||||
|
||||
If you want to build your own wheels, build container image first::
|
||||
We publish a build environment under the ``deltachat/coredeps`` tag so
|
||||
that you can pull it from the ``hub.docker.com`` site's "deltachat"
|
||||
organization::
|
||||
|
||||
$ cd deltachat-core-rust # cd to deltachat-core-rust working tree
|
||||
$ docker build -t deltachat/coredeps scripts/coredeps
|
||||
|
||||
This will use the ``scripts/coredeps/Dockerfile`` to build
|
||||
container image called ``deltachat/coredeps``. You can afterwards
|
||||
find it with::
|
||||
|
||||
$ docker images
|
||||
$ docker pull deltachat/coredeps
|
||||
|
||||
This docker image can be used to run tests and build Python wheels for all interpreters::
|
||||
|
||||
$ docker run -e DCC_NEW_TMP_EMAIL \
|
||||
--rm -it -v $(pwd):/mnt -w /mnt \
|
||||
--rm -it -v \$(pwd):/mnt -w /mnt \
|
||||
deltachat/coredeps scripts/run_all.sh
|
||||
|
||||
|
||||
Optionally build your own docker image
|
||||
--------------------------------------
|
||||
|
||||
If you want to build your own custom docker image you can do this::
|
||||
|
||||
$ cd deltachat-core # cd to deltachat-core checkout directory
|
||||
$ docker build -t deltachat/coredeps scripts/docker_coredeps
|
||||
|
||||
This will use the ``scripts/docker_coredeps/Dockerfile`` to build
|
||||
up docker image called ``deltachat/coredeps``. You can afterwards
|
||||
find it with::
|
||||
|
||||
$ docker images
|
||||
|
||||
|
||||
Troubleshooting
|
||||
---------------
|
||||
|
||||
On more recent systems running the docker image may crash. You can
|
||||
fix this by adding ``vsyscall=emulate`` to the Linux kernel boot
|
||||
arguments commandline. E.g. on Debian you'd add this to
|
||||
``GRUB_CMDLINE_LINUX_DEFAULT`` in ``/etc/default/grub``.
|
||||
|
||||
7
python/fail_test.py
Normal file
7
python/fail_test.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from __future__ import print_function
|
||||
from deltachat import capi
|
||||
from deltachat.capi import ffi, lib
|
||||
|
||||
if __name__ == "__main__":
|
||||
ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL)
|
||||
lib.dc_stop_io(ctx)
|
||||
@@ -16,7 +16,7 @@ if __name__ == "__main__":
|
||||
dn = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
os.environ["DCC_RS_DEV"] = dn
|
||||
|
||||
cmd = ["cargo", "build", "-p", "deltachat_ffi", "--features", "jsonrpc"]
|
||||
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
|
||||
|
||||
if target == "release":
|
||||
os.environ["CARGO_PROFILE_RELEASE_LTO"] = "on"
|
||||
|
||||
@@ -1,41 +1,7 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2", "cffi>=1.0.0", "pkgconfig"]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0", "pkgconfig"]
|
||||
build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "deltachat"
|
||||
description = "Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat"
|
||||
readme = "README.rst"
|
||||
requires-python = ">=3.7"
|
||||
authors = [
|
||||
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
|
||||
]
|
||||
classifiers = [
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Communications :: Email",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
]
|
||||
dependencies = [
|
||||
"cffi>=1.0.0",
|
||||
"imap-tools",
|
||||
"pluggy",
|
||||
"requests",
|
||||
]
|
||||
dynamic = [
|
||||
"version"
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
|
||||
"Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues"
|
||||
"Documentation" = "https://py.delta.chat/"
|
||||
|
||||
[project.entry-points.pytest11]
|
||||
"deltachat.testplugin" = "deltachat.testplugin"
|
||||
|
||||
[tool.setuptools_scm]
|
||||
root = ".."
|
||||
tag_regex = '^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
|
||||
|
||||
4
python/setup.cfg
Normal file
4
python/setup.cfg
Normal file
@@ -0,0 +1,4 @@
|
||||
[devpi:upload]
|
||||
formats = sdist.tgz
|
||||
no-vcs = 1
|
||||
|
||||
@@ -1,4 +1,40 @@
|
||||
from setuptools import setup
|
||||
import os
|
||||
import re
|
||||
|
||||
import setuptools
|
||||
|
||||
|
||||
def main():
|
||||
with open("README.rst") as f:
|
||||
long_description = f.read()
|
||||
setuptools.setup(
|
||||
name="deltachat",
|
||||
description="Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat",
|
||||
long_description=long_description,
|
||||
author="holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors",
|
||||
install_requires=["cffi>=1.0.0", "pluggy", "imap-tools", "requests"],
|
||||
setup_requires=[
|
||||
"setuptools_scm", # required for compatibility with `python3 setup.py sdist`
|
||||
"pkgconfig",
|
||||
],
|
||||
packages=setuptools.find_packages("src"),
|
||||
package_dir={"": "src"},
|
||||
cffi_modules=["src/deltachat/_build.py:ffibuilder"],
|
||||
entry_points={
|
||||
"pytest11": [
|
||||
"deltachat.testplugin = deltachat.testplugin",
|
||||
],
|
||||
},
|
||||
classifiers=[
|
||||
"Development Status :: 4 - Beta",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Topic :: Communications :: Email",
|
||||
"Topic :: Software Development :: Libraries",
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
setup(cffi_modules=["src/deltachat/_build.py:ffibuilder"])
|
||||
main()
|
||||
|
||||
@@ -2,7 +2,7 @@ import sys
|
||||
|
||||
from pkg_resources import DistributionNotFound, get_distribution
|
||||
|
||||
from . import capi, events, hookspec # noqa
|
||||
from . import capi, const, events, hookspec # noqa
|
||||
from .account import Account, get_core_info # noqa
|
||||
from .capi import ffi # noqa
|
||||
from .chat import Chat # noqa
|
||||
@@ -17,6 +17,14 @@ except DistributionNotFound:
|
||||
__version__ = "0.0.0.dev0-unknown"
|
||||
|
||||
|
||||
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
||||
if not _DC_EVENTNAME_MAP:
|
||||
for name in dir(const):
|
||||
if name.startswith("DC_EVENT_"):
|
||||
_DC_EVENTNAME_MAP[getattr(const, name)] = name
|
||||
return _DC_EVENTNAME_MAP[integer]
|
||||
|
||||
|
||||
def register_global_plugin(plugin):
|
||||
"""Register a global plugin which implements one or more
|
||||
of the :class:`deltachat.hookspec.Global` hooks.
|
||||
@@ -52,7 +60,29 @@ def run_cmdline(argv=None, account_plugins=None):
|
||||
|
||||
ac = Account(args.db)
|
||||
|
||||
ac.run_account(addr=args.email, password=args.password, account_plugins=account_plugins, show_ffi=args.show_ffi)
|
||||
if args.show_ffi:
|
||||
ac.set_config("displayname", "bot")
|
||||
log = events.FFIEventLogger(ac)
|
||||
ac.add_account_plugin(log)
|
||||
|
||||
for plugin in account_plugins or []:
|
||||
print("adding plugin", plugin)
|
||||
ac.add_account_plugin(plugin)
|
||||
|
||||
if not ac.is_configured():
|
||||
assert (
|
||||
args.email and args.password
|
||||
), "you must specify --email and --password once to configure this database/account"
|
||||
ac.set_config("addr", args.email)
|
||||
ac.set_config("mail_pw", args.password)
|
||||
ac.set_config("mvbox_move", "0")
|
||||
ac.set_config("sentbox_watch", "0")
|
||||
ac.set_config("bot", "1")
|
||||
configtracker = ac.configure()
|
||||
configtracker.wait_finish()
|
||||
|
||||
# start IO threads and configure if neccessary
|
||||
ac.start_io()
|
||||
|
||||
print("{}: waiting for message".format(ac.get_config("addr")))
|
||||
|
||||
|
||||
@@ -20,7 +20,7 @@ from .cutil import (
|
||||
from_optional_dc_charpointer,
|
||||
iter_array,
|
||||
)
|
||||
from .events import EventThread, FFIEventLogger
|
||||
from .events import EventThread
|
||||
from .message import Message
|
||||
from .tracker import ConfigureTracker, ImexTracker
|
||||
|
||||
@@ -169,6 +169,8 @@ class Account(object):
|
||||
"""
|
||||
self._check_config_key(name)
|
||||
namebytes = name.encode("utf8")
|
||||
if namebytes == b"addr" and self.is_configured():
|
||||
raise ValueError("can not change 'addr' after account is configured.")
|
||||
if isinstance(value, (int, bool)):
|
||||
value = str(int(value))
|
||||
if value is not None:
|
||||
@@ -596,36 +598,6 @@ class Account(object):
|
||||
# meta API for start/stop and event based processing
|
||||
#
|
||||
|
||||
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False):
|
||||
"""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
|
||||
"""
|
||||
if show_ffi:
|
||||
self.set_config("displayname", "bot")
|
||||
log = FFIEventLogger(self)
|
||||
self.add_account_plugin(log)
|
||||
|
||||
for plugin in account_plugins or []:
|
||||
print("adding plugin", plugin)
|
||||
self.add_account_plugin(plugin)
|
||||
|
||||
if not self.is_configured():
|
||||
assert addr and password, "you must specify email and password once to configure this database/account"
|
||||
self.set_config("addr", addr)
|
||||
self.set_config("mail_pw", password)
|
||||
self.set_config("mvbox_move", "0")
|
||||
self.set_config("sentbox_watch", "0")
|
||||
self.set_config("bot", "1")
|
||||
configtracker = self.configure()
|
||||
configtracker.wait_finish()
|
||||
|
||||
# start IO threads and configure if neccessary
|
||||
self.start_io()
|
||||
|
||||
def add_account_plugin(self, plugin, name=None):
|
||||
"""add an account plugin which implements one or more of
|
||||
the :class:`deltachat.hookspec.PerAccount` hooks.
|
||||
@@ -708,9 +680,8 @@ class Account(object):
|
||||
"""Start configuration process and return a Configtracker instance
|
||||
on which you can block with wait_finish() to get a True/False success
|
||||
value for the configuration process.
|
||||
|
||||
:param reconfigure: deprecated, doesn't need to be checked anymore.
|
||||
"""
|
||||
assert self.is_configured() == reconfigure
|
||||
if not self.get_config("addr") or not self.get_config("mail_pw"):
|
||||
raise MissingCredentials("addr or mail_pwd not set in config")
|
||||
configtracker = ConfigureTracker(self)
|
||||
|
||||
@@ -8,21 +8,14 @@ import traceback
|
||||
from contextlib import contextmanager
|
||||
from queue import Empty, Queue
|
||||
|
||||
from . import const
|
||||
import deltachat
|
||||
|
||||
from .capi import ffi, lib
|
||||
from .cutil import from_optional_dc_charpointer
|
||||
from .hookspec import account_hookimpl
|
||||
from .message import map_system_message
|
||||
|
||||
|
||||
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
||||
if not _DC_EVENTNAME_MAP:
|
||||
for name in dir(const):
|
||||
if name.startswith("DC_EVENT_"):
|
||||
_DC_EVENTNAME_MAP[getattr(const, name)] = name
|
||||
return _DC_EVENTNAME_MAP[integer]
|
||||
|
||||
|
||||
class FFIEvent:
|
||||
def __init__(self, name: str, data1, data2):
|
||||
self.name = name
|
||||
@@ -105,7 +98,7 @@ class FFIEventTracker:
|
||||
yield self.get(timeout=timeout, check_error=check_error)
|
||||
|
||||
def get_matching(self, event_name_regex, check_error=True, timeout=None):
|
||||
rex = re.compile("^(?:{})$".format(event_name_regex))
|
||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||
for ev in self.iter_events(timeout=timeout, check_error=check_error):
|
||||
if rex.match(ev.name):
|
||||
return ev
|
||||
@@ -246,7 +239,7 @@ class EventThread(threading.Thread):
|
||||
data1 = lib.dc_event_get_data1_int(event)
|
||||
# the following code relates to the deltachat/_build.py's helper
|
||||
# function which provides us signature info of an event call
|
||||
evt_name = get_dc_event_name(evt)
|
||||
evt_name = deltachat.get_dc_event_name(evt)
|
||||
if lib.dc_event_has_string_data(evt):
|
||||
data2 = from_optional_dc_charpointer(lib.dc_event_get_data2_str(event))
|
||||
else:
|
||||
|
||||
@@ -507,8 +507,6 @@ def parse_system_add_remove(text):
|
||||
|
||||
returns a (action, affected, actor) triple"""
|
||||
|
||||
# You removed member a@b.
|
||||
# You added member a@b.
|
||||
# Member Me (x@y) removed by a@b.
|
||||
# Member x@y added by a@b
|
||||
# Member With space (tmp1@x.org) removed by tmp2@x.org.
|
||||
@@ -520,10 +518,6 @@ def parse_system_add_remove(text):
|
||||
if m:
|
||||
affected, action, actor = m.groups()
|
||||
return action, extract_addr(affected), extract_addr(actor)
|
||||
m = re.match(r"you (removed|added) member (.+)", text)
|
||||
if m:
|
||||
action, affected = m.groups()
|
||||
return action, extract_addr(affected), "me"
|
||||
if text.startswith("group left by "):
|
||||
addr = extract_addr(text[13:])
|
||||
if addr:
|
||||
|
||||
@@ -497,8 +497,6 @@ class ACFactory:
|
||||
configdict = dict(
|
||||
addr=cloned_from.get_config("addr"),
|
||||
mail_pw=cloned_from.get_config("mail_pw"),
|
||||
imap_certificate_checks=cloned_from.get_config("imap_certificate_checks"),
|
||||
smtp_certificate_checks=cloned_from.get_config("smtp_certificate_checks"),
|
||||
)
|
||||
configdict.update(kwargs)
|
||||
ac = self._get_cached_account(addr=configdict["addr"]) if cache else None
|
||||
|
||||
13
python/tests/package_wheels.py
Normal file
13
python/tests/package_wheels.py
Normal file
@@ -0,0 +1,13 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
if __name__ == "__main__":
|
||||
assert len(sys.argv) == 2
|
||||
wheelhousedir = sys.argv[1]
|
||||
# pip wheel will build in an isolated tmp dir that does not have git
|
||||
# history so setuptools_scm can not automatically determine a
|
||||
# version there. So pass in the version through an env var.
|
||||
version = subprocess.check_output(["python", "setup.py", "--version"]).strip().split(b"\n")[-1]
|
||||
os.environ["SETUPTOOLS_SCM_PRETEND_VERSION"] = version.decode("ascii")
|
||||
subprocess.check_call(("pip wheel . -w %s" % wheelhousedir).split())
|
||||
@@ -1581,9 +1581,8 @@ def test_set_get_group_image(acfactory, data, lp):
|
||||
|
||||
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.text == "hi" or msg2.text == "hi"
|
||||
assert msg1.chat.id == msg2.chat.id
|
||||
|
||||
lp.sec("ac2: see if chat now has got the profile image")
|
||||
@@ -1597,8 +1596,6 @@ def test_set_get_group_image(acfactory, data, lp):
|
||||
lp.sec("ac2: delete profile image from chat")
|
||||
msg1.chat.remove_profile_image()
|
||||
msg_back = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg_back.text == "Group image deleted by {}.".format(ac2.get_config("addr"))
|
||||
assert msg_back.is_system_message()
|
||||
assert msg_back.chat == chat
|
||||
assert chat.get_profile_image() is None
|
||||
|
||||
@@ -1857,7 +1854,7 @@ def test_configure_error_msgs_invalid_server(acfactory):
|
||||
# Can't connect so it probably should say something about "internet"
|
||||
# again, should not repeat itself
|
||||
# If this fails then probably `e.msg.to_lowercase().contains("could not resolve")`
|
||||
# in configure.rs returned false because the error message was changed
|
||||
# in configure/mod.rs returned false because the error message was changed
|
||||
# (i.e. did not contain "could not resolve" anymore)
|
||||
assert (ev.data2.count("internet") + ev.data2.count("network")) == 1
|
||||
# Should mention that it can't connect:
|
||||
@@ -2055,47 +2052,6 @@ def test_delete_deltachat_folder(acfactory):
|
||||
assert "DeltaChat" in ac1.direct_imap.list_folders()
|
||||
|
||||
|
||||
def test_aeap_flow_verified(acfactory, lp):
|
||||
"""Test that a new address is added to a contact when it changes its address."""
|
||||
ac1, ac2, ac1new = acfactory.get_online_accounts(3)
|
||||
|
||||
lp.sec("ac1: create verified-group QR, ac2 scans and joins")
|
||||
chat = ac1.create_group_chat("hello", verified=True)
|
||||
assert chat.is_protected()
|
||||
qr = chat.get_join_qr()
|
||||
lp.sec("ac2: start QR-code based join-group protocol")
|
||||
chat2 = ac2.qr_join_chat(qr)
|
||||
assert chat2.id >= 10
|
||||
ac1._evtracker.wait_securejoin_inviter_progress(1000)
|
||||
|
||||
lp.sec("sending first message")
|
||||
msg_out = chat.send_text("old address")
|
||||
|
||||
lp.sec("receiving first message")
|
||||
ac2._evtracker.wait_next_incoming_message() # member added message
|
||||
msg_in_1 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in_1.text == msg_out.text
|
||||
|
||||
lp.sec("changing email account")
|
||||
ac1.set_config("addr", ac1new.get_config("addr"))
|
||||
ac1.set_config("mail_pw", ac1new.get_config("mail_pw"))
|
||||
ac1.stop_io()
|
||||
configtracker = ac1.configure()
|
||||
configtracker.wait_finish()
|
||||
ac1.start_io()
|
||||
|
||||
lp.sec("sending second message")
|
||||
msg_out = chat.send_text("changed address")
|
||||
|
||||
lp.sec("receiving second message")
|
||||
msg_in_2 = ac2._evtracker.wait_next_incoming_message()
|
||||
assert msg_in_2.text == msg_out.text
|
||||
assert msg_in_2.chat.id == msg_in_1.chat.id
|
||||
assert msg_in_2.get_sender_contact().addr == ac1new.get_config("addr")
|
||||
assert len(msg_in_2.chat.get_contacts()) == 2
|
||||
assert ac1new.get_config("addr") in [contact.addr for contact in msg_in_2.chat.get_contacts()]
|
||||
|
||||
|
||||
class TestOnlineConfigureFails:
|
||||
def test_invalid_password(self, acfactory):
|
||||
configdict = acfactory.get_next_liveconfig()
|
||||
|
||||
@@ -80,7 +80,7 @@ class TestOfflineAccountBasic:
|
||||
d = ac1.get_info()
|
||||
assert d["arch"]
|
||||
assert d["number_of_chats"] == "0"
|
||||
assert d["bcc_self"] == "1"
|
||||
assert d["bcc_self"] == "0"
|
||||
|
||||
def test_is_not_configured(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
@@ -118,7 +118,7 @@ class TestOfflineAccountBasic:
|
||||
def test_has_bccself(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
assert "bcc_self" in ac1.get_config("sys.config_keys").split()
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
assert ac1.get_config("bcc_self") == "0"
|
||||
|
||||
def test_selfcontact_if_unconfigured(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
@@ -200,11 +200,11 @@ class TestOfflineContact:
|
||||
assert ac1.delete_contact(contact1)
|
||||
assert contact1 not in ac1.get_contacts()
|
||||
|
||||
def test_delete_referenced_contact_hides_contact(self, acfactory):
|
||||
def test_get_contacts_and_delete_fails(self, acfactory):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
contact1 = ac1.create_contact("some1@example.com", name="some1")
|
||||
msg = contact1.create_chat().send_text("one message")
|
||||
assert ac1.delete_contact(contact1)
|
||||
assert not ac1.delete_contact(contact1)
|
||||
assert not msg.filemime
|
||||
|
||||
def test_create_chat_flexibility(self, acfactory):
|
||||
@@ -301,7 +301,7 @@ class TestOfflineChat:
|
||||
assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft()
|
||||
|
||||
def test_group_chat_creation_with_translation(self, ac1):
|
||||
ac1.set_stock_translation(const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s")
|
||||
ac1.set_stock_translation(const.DC_STR_MSGGRPNAME, "abc %1$s xyz %2$s")
|
||||
ac1._evtracker.consume_events()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(const.DC_STR_FILE, "xyz %1$s")
|
||||
@@ -317,7 +317,7 @@ class TestOfflineChat:
|
||||
chat.send_text("Now we have a group for homework")
|
||||
assert chat.is_promoted()
|
||||
chat.set_name("Homework")
|
||||
assert chat.get_messages()[-1].text == "abc homework xyz Homework"
|
||||
assert chat.get_messages()[-1].text == "abc homework xyz Homework by me."
|
||||
|
||||
@pytest.mark.parametrize("verified", [True, False])
|
||||
def test_group_chat_qr(self, acfactory, ac1, verified):
|
||||
@@ -497,6 +497,12 @@ class TestOfflineChat:
|
||||
contact = msg.get_sender_contact()
|
||||
assert contact == ac1.get_self_contact()
|
||||
|
||||
def test_set_config_after_configure_is_forbidden(self, ac1):
|
||||
assert ac1.get_config("mail_pw")
|
||||
assert ac1.is_configured()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_config("addr", "123@example.org")
|
||||
|
||||
def test_import_export_on_unencrypted_acct(self, acfactory, tmpdir):
|
||||
backupdir = tmpdir.mkdir("backup")
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
|
||||
@@ -9,11 +9,9 @@ envlist =
|
||||
[testenv]
|
||||
commands =
|
||||
pytest -n6 --extra-info --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
|
||||
pip wheel . -w {toxworkdir}/wheelhouse --no-deps
|
||||
setenv =
|
||||
# Avoid stack overflow when Rust core is built without optimizations.
|
||||
RUST_MIN_STACK=8388608
|
||||
python tests/package_wheels.py {toxworkdir}/wheelhouse
|
||||
passenv =
|
||||
TRAVIS
|
||||
DCC_RS_DEV
|
||||
DCC_RS_TARGET
|
||||
DCC_NEW_TMP_EMAIL
|
||||
|
||||
@@ -14,15 +14,18 @@ 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.
|
||||
|
||||
- `run-doxygen.sh` generates C-docs which are then uploaded to https://c.delta.chat/
|
||||
- `doxygen/Dockerfile` specifies an image that contains
|
||||
the doxygen tool which is used by `run-doxygen.sh`
|
||||
to generate C-docs which are then uploaded
|
||||
via `ci_upload.sh` to `https://c.delta.chat/_unofficial_unreleased_docs/<BRANCH>`
|
||||
(and the master branch is linked to https://c.delta.chat proper).
|
||||
|
||||
- `run_all.sh` builds Python wheels
|
||||
|
||||
## Triggering runs on the build machine locally (fast!)
|
||||
|
||||
There is experimental support for triggering a remote Python or Rust test run
|
||||
from your local checkout/branch. You will need to be authorized to login to
|
||||
the build machine (ask your friendly sysadmin on #deltachat Libera Chat) to type:
|
||||
the build machine (ask your friendly sysadmin on #deltachat Libera Chat) to type::
|
||||
|
||||
scripts/manual_remote_tests.sh rust
|
||||
scripts/manual_remote_tests.sh python
|
||||
@@ -30,18 +33,19 @@ the build machine (ask your friendly sysadmin on #deltachat Libera Chat) to type
|
||||
This will **rsync** your current checkout to the remote build machine
|
||||
(no need to commit before) and then run either rust or python tests.
|
||||
|
||||
# coredeps Dockerfile
|
||||
# Outdated files (for later re-use)
|
||||
|
||||
`coredeps/Dockerfile` specifies an image that contains all
|
||||
of Delta Chat's core dependencies. It is used to
|
||||
build python wheels (binary packages for Python).
|
||||
of Delta Chat's core dependencies. It used to run
|
||||
python tests and build wheels (binary packages for Python)
|
||||
|
||||
You can build the docker images yourself locally
|
||||
to avoid the relatively large download:
|
||||
to avoid the relatively large download::
|
||||
|
||||
cd scripts # where all CI things are
|
||||
docker build -t deltachat/coredeps coredeps
|
||||
docker build -t deltachat/coredeps docker-coredeps
|
||||
docker build -t deltachat/doxygen docker-doxygen
|
||||
|
||||
Additionally, you can install qemu and build arm64 docker image on x86\_64 machine:
|
||||
Additionally, you can install qemu and build arm64 docker image:
|
||||
apt-get install qemu binfmt-support qemu-user-static
|
||||
docker build -t deltachat/coredeps-arm64 --build-arg BASEIMAGE=quay.io/pypa/manylinux2014_aarch64 coredeps
|
||||
docker build -t deltachat/coredeps-arm64 docker-coredeps-arm64
|
||||
|
||||
@@ -29,19 +29,18 @@ jobs:
|
||||
- name: c-docs
|
||||
image_resource:
|
||||
source:
|
||||
repository: alpine
|
||||
repository: hrektts/doxygen
|
||||
type: registry-image
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
path: bash
|
||||
args:
|
||||
- -ec
|
||||
- -exc
|
||||
- |
|
||||
apk add --no-cache doxygen git
|
||||
cd deltachat-core-rust
|
||||
scripts/run-doxygen.sh
|
||||
bash scripts/run-doxygen.sh
|
||||
cd ..
|
||||
cp -av deltachat-core-rust/deltachat-ffi/html deltachat-core-rust/deltachat-ffi/xml c-docs/
|
||||
cp -av deltachat-core-rust/deltachat-ffi/{html,xml} c-docs/
|
||||
|
||||
- task: upload-c-docs
|
||||
config:
|
||||
@@ -303,73 +302,3 @@ jobs:
|
||||
devpi use https://m.devpi.net/dc/master
|
||||
devpi login ((devpi.login)) --password ((devpi.password))
|
||||
devpi upload py-wheels/*musllinux_1_1_x86_64*
|
||||
|
||||
- name: python-musl-aarch64
|
||||
plan:
|
||||
- get: deltachat-core-rust
|
||||
- get: deltachat-core-rust-release
|
||||
trigger: true
|
||||
|
||||
# Build manylinux image with additional dependencies
|
||||
- task: build-coredeps
|
||||
privileged: true
|
||||
config:
|
||||
inputs:
|
||||
# Building the latest, not tagged coredeps
|
||||
- name: deltachat-core-rust
|
||||
image_resource:
|
||||
source:
|
||||
repository: concourse/oci-build-task
|
||||
type: registry-image
|
||||
outputs:
|
||||
- name: coredeps-image
|
||||
path: image
|
||||
params:
|
||||
CONTEXT: deltachat-core-rust/scripts/coredeps
|
||||
UNPACK_ROOTFS: "true"
|
||||
BUILD_ARG_BASEIMAGE: quay.io/pypa/musllinux_1_1_aarch64
|
||||
platform: linux
|
||||
caches:
|
||||
- path: cache
|
||||
run:
|
||||
path: build
|
||||
|
||||
# Use built image to build python wheels
|
||||
- task: build-wheels
|
||||
image: coredeps-image
|
||||
config:
|
||||
inputs:
|
||||
- name: deltachat-core-rust-release
|
||||
path: .
|
||||
outputs:
|
||||
- name: py-wheels
|
||||
path: ./python/.docker-tox/wheelhouse/
|
||||
platform: linux
|
||||
run:
|
||||
path: bash
|
||||
args:
|
||||
- -exc
|
||||
- |
|
||||
scripts/run_all.sh
|
||||
|
||||
# Upload musl aarch64 wheels
|
||||
- task: upload-wheels
|
||||
config:
|
||||
inputs:
|
||||
- name: py-wheels
|
||||
image_resource:
|
||||
type: registry-image
|
||||
source:
|
||||
repository: debian
|
||||
platform: linux
|
||||
run:
|
||||
path: sh
|
||||
args:
|
||||
- -ec
|
||||
- |
|
||||
apt-get update -y
|
||||
apt-get install -y --no-install-recommends python3-pip python3-setuptools
|
||||
pip3 install devpi
|
||||
devpi use https://m.devpi.net/dc/master
|
||||
devpi login ((devpi.login)) --password ((devpi.password))
|
||||
devpi upload py-wheels/*musllinux_1_1_aarch64*
|
||||
|
||||
@@ -17,7 +17,7 @@ export RUSTC_BOOTSTRAP=1
|
||||
# [2] https://github.com/mozilla/grcov/issues/595
|
||||
|
||||
export CARGO_INCREMENTAL=0
|
||||
export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort -Cdebuginfo=2"
|
||||
export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort"
|
||||
export RUSTDOCFLAGS="-Cpanic=abort"
|
||||
cargo clean
|
||||
cargo build
|
||||
|
||||
5
scripts/docker-doxygen/Dockerfile
Normal file
5
scripts/docker-doxygen/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
||||
FROM debian:stable
|
||||
|
||||
# this is tagged as deltachat/doxygen
|
||||
|
||||
RUN apt-get update && apt-get install -y doxygen
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/bin/sh
|
||||
set -e
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
|
||||
cd deltachat-ffi
|
||||
PROJECT_NUMBER=$(git log -1 --format="%h (%cd)") doxygen
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/sh
|
||||
#!/bin/bash
|
||||
#
|
||||
# Build the Delta Chat Core Rust library, Python wheels and docs
|
||||
|
||||
@@ -8,16 +8,21 @@ set -e -x
|
||||
|
||||
# compile core lib
|
||||
|
||||
cargo build --release -p deltachat_ffi --features jsonrpc
|
||||
export PATH=/root/.cargo/bin:$PATH
|
||||
cargo build --release -p deltachat_ffi
|
||||
# cargo test --all --all-features
|
||||
|
||||
# Statically link against libdeltachat.a.
|
||||
export DCC_RS_DEV="$PWD"
|
||||
export DCC_RS_DEV=$(pwd)
|
||||
export DCC_RS_TARGET=release
|
||||
|
||||
# Configure access to a base python and to several python interpreters
|
||||
# needed by tox below.
|
||||
export PATH=$PATH:/opt/python/cp37-cp37m/bin
|
||||
export PYTHONDONTWRITEBYTECODE=1
|
||||
cd python
|
||||
|
||||
TOXWORKDIR=.docker-tox
|
||||
pushd python
|
||||
# prepare a clean tox run
|
||||
rm -rf tests/__pycache__
|
||||
rm -rf src/deltachat/__pycache__
|
||||
@@ -28,13 +33,11 @@ mkdir -p $TOXWORKDIR
|
||||
# Note that the independent remote_tests_python step does all kinds of
|
||||
# live-testing already.
|
||||
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,pypy37,pypy38,pypy39,auditwheels --skip-missing-interpreters true
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,pypy37,pypy38,pypy39,auditwheels
|
||||
popd
|
||||
|
||||
|
||||
echo -----------------------
|
||||
echo generating python docs
|
||||
echo -----------------------
|
||||
tox --workdir "$TOXWORKDIR" -e doc
|
||||
(cd python && tox --workdir "$TOXWORKDIR" -e doc)
|
||||
|
||||
26
scripts/set_core_version.py
Executable file → Normal file
26
scripts/set_core_version.py
Executable file → Normal file
@@ -42,15 +42,15 @@ def replace_toml_version(relpath, newversion):
|
||||
|
||||
|
||||
def read_json_version(relpath):
|
||||
p = pathlib.Path(relpath)
|
||||
p = pathlib.Path("package.json")
|
||||
assert p.exists()
|
||||
with open(p, "r") as f:
|
||||
json_data = json.loads(f.read())
|
||||
return json_data["version"]
|
||||
|
||||
|
||||
def update_package_json(relpath, newversion):
|
||||
p = pathlib.Path(relpath)
|
||||
def update_package_json(newversion):
|
||||
p = pathlib.Path("package.json")
|
||||
assert p.exists()
|
||||
with open(p, "r") as f:
|
||||
json_data = json.loads(f.read())
|
||||
@@ -63,21 +63,14 @@ def main():
|
||||
parser = ArgumentParser(prog="set_core_version")
|
||||
parser.add_argument("newversion")
|
||||
|
||||
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
|
||||
toml_list = [
|
||||
"Cargo.toml",
|
||||
"deltachat-ffi/Cargo.toml",
|
||||
"deltachat-jsonrpc/Cargo.toml",
|
||||
"deltachat-rpc-server/Cargo.toml",
|
||||
]
|
||||
toml_list = ["Cargo.toml", "deltachat-ffi/Cargo.toml", "deltachat-jsonrpc/Cargo.toml"]
|
||||
try:
|
||||
opts = parser.parse_args()
|
||||
except SystemExit:
|
||||
print()
|
||||
for x in toml_list:
|
||||
print("{}: {}".format(x, read_toml_version(x)))
|
||||
for x in json_list:
|
||||
print("{}: {}".format(x, read_json_version(x)))
|
||||
print("package.json:", str(read_json_version("package.json")))
|
||||
print()
|
||||
raise SystemExit("need argument: new version, example: 1.25.0")
|
||||
|
||||
@@ -98,12 +91,9 @@ def main():
|
||||
else:
|
||||
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
|
||||
|
||||
for toml_filename in toml_list:
|
||||
replace_toml_version(toml_filename, newversion)
|
||||
|
||||
for json_filename in json_list:
|
||||
update_package_json(json_filename, newversion)
|
||||
|
||||
replace_toml_version("Cargo.toml", newversion)
|
||||
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
|
||||
update_package_json(newversion)
|
||||
|
||||
print("running cargo check")
|
||||
subprocess.call(["cargo", "check"])
|
||||
|
||||
10
spec.md
10
spec.md
@@ -450,16 +450,6 @@ This allows the receiver to show the time without knowing the file format.
|
||||
Chat-Duration: 10000
|
||||
|
||||
|
||||
# Reactions
|
||||
|
||||
Messengers MAY implement [RFC 9078](https://tools.ietf.org/html/rfc9078) reactions.
|
||||
Received reaction should be interpreted as overwriting all previous reactions
|
||||
received from the same contact.
|
||||
This semantics is compatible to [XEP-0444](https://xmpp.org/extensions/xep-0444.html).
|
||||
As an extension to RFC 9078, it is allowed to send empty reaction message,
|
||||
in which case all previously sent reactions are retracted.
|
||||
|
||||
|
||||
# Miscellaneous
|
||||
|
||||
Messengers SHOULD use the header `In-Reply-To` as usual.
|
||||
|
||||
162
src/accounts.rs
162
src/accounts.rs
@@ -10,7 +10,6 @@ use uuid::Uuid;
|
||||
|
||||
use crate::context::Context;
|
||||
use crate::events::{Event, EventEmitter, EventType, Events};
|
||||
use crate::stock_str::StockStrings;
|
||||
|
||||
/// Account manager, that can handle multiple accounts in a single place.
|
||||
#[derive(Debug)]
|
||||
@@ -21,12 +20,6 @@ pub struct Accounts {
|
||||
|
||||
/// Event channel to emit account manager errors.
|
||||
events: Events,
|
||||
|
||||
/// Stock string translations shared by all created contexts.
|
||||
///
|
||||
/// This way changing a translation for one context automatically
|
||||
/// changes it for all other contexts.
|
||||
pub(crate) stockstrings: StockStrings,
|
||||
}
|
||||
|
||||
impl Accounts {
|
||||
@@ -40,7 +33,7 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Creates a new default structure.
|
||||
pub async fn create(dir: &Path) -> Result<()> {
|
||||
pub async fn create(dir: &PathBuf) -> Result<()> {
|
||||
fs::create_dir_all(dir)
|
||||
.await
|
||||
.context("failed to create folder")?;
|
||||
@@ -62,9 +55,8 @@ impl Accounts {
|
||||
.await
|
||||
.context("failed to load accounts config")?;
|
||||
let events = Events::new();
|
||||
let stockstrings = StockStrings::new();
|
||||
let accounts = config
|
||||
.load_accounts(&events, &stockstrings)
|
||||
.load_accounts(&events)
|
||||
.await
|
||||
.context("failed to load accounts")?;
|
||||
|
||||
@@ -73,24 +65,23 @@ impl Accounts {
|
||||
config,
|
||||
accounts,
|
||||
events,
|
||||
stockstrings,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get an account by its `id`:
|
||||
pub fn get_account(&self, id: u32) -> Option<Context> {
|
||||
pub async fn get_account(&self, id: u32) -> Option<Context> {
|
||||
self.accounts.get(&id).cloned()
|
||||
}
|
||||
|
||||
/// Get the currently selected account.
|
||||
pub fn get_selected_account(&self) -> Option<Context> {
|
||||
let id = self.config.get_selected_account();
|
||||
pub async fn get_selected_account(&self) -> Option<Context> {
|
||||
let id = self.config.get_selected_account().await;
|
||||
self.accounts.get(&id).cloned()
|
||||
}
|
||||
|
||||
/// Returns the currently selected account's id or None if no account is selected.
|
||||
pub fn get_selected_account_id(&self) -> Option<u32> {
|
||||
match self.config.get_selected_account() {
|
||||
pub async fn get_selected_account_id(&self) -> Option<u32> {
|
||||
match self.config.get_selected_account().await {
|
||||
0 => None,
|
||||
id => Some(id),
|
||||
}
|
||||
@@ -113,7 +104,6 @@ impl Accounts {
|
||||
&account_config.dbfile(),
|
||||
account_config.id,
|
||||
self.events.clone(),
|
||||
self.stockstrings.clone(),
|
||||
)
|
||||
.await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
@@ -129,7 +119,6 @@ impl Accounts {
|
||||
&account_config.dbfile(),
|
||||
account_config.id,
|
||||
self.events.clone(),
|
||||
self.stockstrings.clone(),
|
||||
)
|
||||
.await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
@@ -146,7 +135,7 @@ impl Accounts {
|
||||
ctx.stop_io().await;
|
||||
drop(ctx);
|
||||
|
||||
if let Some(cfg) = self.config.get_account(id) {
|
||||
if let Some(cfg) = self.config.get_account(id).await {
|
||||
// Spend up to 1 minute trying to remove the files.
|
||||
// Files may remain locked up to 30 seconds due to r2d2 bug:
|
||||
// https://github.com/sfackler/r2d2/issues/99
|
||||
@@ -182,7 +171,7 @@ impl Accounts {
|
||||
ensure!(dbfile.exists(), "no database found: {}", dbfile.display());
|
||||
ensure!(blobdir.exists(), "no blobdir found: {}", blobdir.display());
|
||||
|
||||
let old_id = self.config.get_selected_account();
|
||||
let old_id = self.config.get_selected_account().await;
|
||||
|
||||
// create new account
|
||||
let account_config = self
|
||||
@@ -215,13 +204,7 @@ impl Accounts {
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
let ctx = Context::new(
|
||||
&new_dbfile,
|
||||
account_config.id,
|
||||
self.events.clone(),
|
||||
self.stockstrings.clone(),
|
||||
)
|
||||
.await?;
|
||||
let ctx = Context::new(&new_dbfile, account_config.id, self.events.clone()).await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
Ok(account_config.id)
|
||||
}
|
||||
@@ -242,7 +225,7 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Get a list of all account ids.
|
||||
pub fn get_all(&self) -> Vec<u32> {
|
||||
pub async fn get_all(&self) -> Vec<u32> {
|
||||
self.accounts.keys().copied().collect()
|
||||
}
|
||||
|
||||
@@ -298,7 +281,7 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Returns event emitter.
|
||||
pub fn get_event_emitter(&self) -> EventEmitter {
|
||||
pub async fn get_event_emitter(&self) -> EventEmitter {
|
||||
self.events.get_emitter()
|
||||
}
|
||||
}
|
||||
@@ -356,31 +339,17 @@ impl Config {
|
||||
Ok(Config { file, inner })
|
||||
}
|
||||
|
||||
/// Loads all accounts defined in the configuration file.
|
||||
///
|
||||
/// Created contexts share the same event channel and stock string
|
||||
/// translations.
|
||||
pub async fn load_accounts(
|
||||
&self,
|
||||
events: &Events,
|
||||
stockstrings: &StockStrings,
|
||||
) -> Result<BTreeMap<u32, Context>> {
|
||||
pub async fn load_accounts(&self, events: &Events) -> Result<BTreeMap<u32, Context>> {
|
||||
let mut accounts = BTreeMap::new();
|
||||
|
||||
for account_config in &self.inner.accounts {
|
||||
let ctx = Context::new(
|
||||
&account_config.dbfile(),
|
||||
account_config.id,
|
||||
events.clone(),
|
||||
stockstrings.clone(),
|
||||
)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to create context from file {:?}",
|
||||
account_config.dbfile()
|
||||
)
|
||||
})?;
|
||||
let ctx = Context::new(&account_config.dbfile(), account_config.id, events.clone())
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"failed to create context from file {:?}",
|
||||
account_config.dbfile()
|
||||
)
|
||||
})?;
|
||||
|
||||
accounts.insert(account_config.id, ctx);
|
||||
}
|
||||
@@ -411,6 +380,7 @@ impl Config {
|
||||
.context("failed to select just added account")?;
|
||||
let cfg = self
|
||||
.get_account(id)
|
||||
.await
|
||||
.context("failed to get just added account")?;
|
||||
Ok(cfg)
|
||||
}
|
||||
@@ -432,11 +402,11 @@ impl Config {
|
||||
self.sync().await
|
||||
}
|
||||
|
||||
fn get_account(&self, id: u32) -> Option<AccountConfig> {
|
||||
async fn get_account(&self, id: u32) -> Option<AccountConfig> {
|
||||
self.inner.accounts.iter().find(|e| e.id == id).cloned()
|
||||
}
|
||||
|
||||
pub fn get_selected_account(&self) -> u32 {
|
||||
pub async fn get_selected_account(&self) -> u32 {
|
||||
self.inner.selected_account
|
||||
}
|
||||
|
||||
@@ -477,8 +447,6 @@ impl AccountConfig {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
use crate::stock_str::{self, StockMessage};
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_account_new_open() {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
@@ -490,7 +458,7 @@ mod tests {
|
||||
let accounts2 = Accounts::open(p).await.unwrap();
|
||||
|
||||
assert_eq!(accounts1.accounts.len(), 1);
|
||||
assert_eq!(accounts1.config.get_selected_account(), 1);
|
||||
assert_eq!(accounts1.config.get_selected_account().await, 1);
|
||||
|
||||
assert_eq!(accounts1.dir, accounts2.dir);
|
||||
assert_eq!(accounts1.config, accounts2.config,);
|
||||
@@ -504,23 +472,23 @@ mod tests {
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, 1);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, 2);
|
||||
assert_eq!(accounts.config.get_selected_account(), id);
|
||||
assert_eq!(accounts.config.get_selected_account().await, id);
|
||||
assert_eq!(accounts.accounts.len(), 2);
|
||||
|
||||
accounts.select_account(1).await.unwrap();
|
||||
assert_eq!(accounts.config.get_selected_account(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
|
||||
accounts.remove_account(1).await.unwrap();
|
||||
assert_eq!(accounts.config.get_selected_account(), 2);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 2);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
}
|
||||
|
||||
@@ -530,17 +498,17 @@ mod tests {
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
assert!(accounts.get_selected_account().is_none());
|
||||
assert_eq!(accounts.config.get_selected_account(), 0);
|
||||
assert!(accounts.get_selected_account().await.is_none());
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
let id = accounts.add_account().await?;
|
||||
assert!(accounts.get_selected_account().is_some());
|
||||
assert!(accounts.get_selected_account().await.is_some());
|
||||
assert_eq!(id, 1);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account(), id);
|
||||
assert_eq!(accounts.config.get_selected_account().await, id);
|
||||
|
||||
accounts.remove_account(id).await?;
|
||||
assert!(accounts.get_selected_account().is_none());
|
||||
assert!(accounts.get_selected_account().await.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -552,10 +520,10 @@ mod tests {
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account(), 0);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
|
||||
let extern_dbfile: PathBuf = dir.path().join("other");
|
||||
let ctx = Context::new(&extern_dbfile, 0, Events::new(), StockStrings::new())
|
||||
let ctx = Context::new(&extern_dbfile, 0, Events::new())
|
||||
.await
|
||||
.unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
|
||||
@@ -569,9 +537,9 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
|
||||
let ctx = accounts.get_selected_account().unwrap();
|
||||
let ctx = accounts.get_selected_account().await.unwrap();
|
||||
assert_eq!(
|
||||
"me@mail.com",
|
||||
ctx.get_config(crate::config::Config::Addr)
|
||||
@@ -594,7 +562,7 @@ mod tests {
|
||||
assert_eq!(id, expected_id);
|
||||
}
|
||||
|
||||
let ids = accounts.get_all();
|
||||
let ids = accounts.get_all().await;
|
||||
for (i, expected_id) in (1..10).enumerate() {
|
||||
assert_eq!(ids.get(i), Some(&expected_id));
|
||||
}
|
||||
@@ -609,16 +577,16 @@ mod tests {
|
||||
let (id0, id1, id2) = {
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
accounts.add_account().await?;
|
||||
let ids = accounts.get_all();
|
||||
let ids = accounts.get_all().await;
|
||||
assert_eq!(ids.len(), 1);
|
||||
|
||||
let id0 = *ids.first().unwrap();
|
||||
let ctx = accounts.get_account(id0).unwrap();
|
||||
let id0 = *ids.get(0).unwrap();
|
||||
let ctx = accounts.get_account(id0).await.unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("one@example.org"))
|
||||
.await?;
|
||||
|
||||
let id1 = accounts.add_account().await?;
|
||||
let ctx = accounts.get_account(id1).unwrap();
|
||||
let ctx = accounts.get_account(id1).await.unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
|
||||
.await?;
|
||||
|
||||
@@ -629,7 +597,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let id2 = accounts.add_account().await?;
|
||||
let ctx = accounts.get_account(id2).unwrap();
|
||||
let ctx = accounts.get_account(id2).await.unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
|
||||
.await?;
|
||||
|
||||
@@ -643,31 +611,31 @@ mod tests {
|
||||
|
||||
let (id0_reopened, id1_reopened, id2_reopened) = {
|
||||
let accounts = Accounts::new(p.clone()).await?;
|
||||
let ctx = accounts.get_selected_account().unwrap();
|
||||
let ctx = accounts.get_selected_account().await.unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
Some("two@example.org".to_string())
|
||||
);
|
||||
|
||||
let ids = accounts.get_all();
|
||||
let ids = accounts.get_all().await;
|
||||
assert_eq!(ids.len(), 3);
|
||||
|
||||
let id0 = *ids.first().unwrap();
|
||||
let ctx = accounts.get_account(id0).unwrap();
|
||||
let id0 = *ids.get(0).unwrap();
|
||||
let ctx = accounts.get_account(id0).await.unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
Some("one@example.org".to_string())
|
||||
);
|
||||
|
||||
let id1 = *ids.get(1).unwrap();
|
||||
let t = accounts.get_account(id1).unwrap();
|
||||
let t = accounts.get_account(id1).await.unwrap();
|
||||
assert_eq!(
|
||||
t.get_config(crate::config::Config::Addr).await?,
|
||||
Some("two@example.org".to_string())
|
||||
);
|
||||
|
||||
let id2 = *ids.get(2).unwrap();
|
||||
let ctx = accounts.get_account(id2).unwrap();
|
||||
let ctx = accounts.get_account(id2).await.unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
Some("three@example.org".to_string())
|
||||
@@ -693,7 +661,7 @@ mod tests {
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
|
||||
// Create event emitter.
|
||||
let event_emitter = accounts.get_event_emitter();
|
||||
let event_emitter = accounts.get_event_emitter().await;
|
||||
|
||||
// Test that event emitter does not return `None` immediately.
|
||||
let duration = std::time::Duration::from_millis(1);
|
||||
@@ -724,6 +692,7 @@ mod tests {
|
||||
.context("failed to add closed account")?;
|
||||
let account = accounts
|
||||
.get_selected_account()
|
||||
.await
|
||||
.context("failed to get account")?;
|
||||
assert_eq!(account.id, account_id);
|
||||
let passphrase_set_success = account
|
||||
@@ -738,6 +707,7 @@ mod tests {
|
||||
.context("failed to create second accounts manager")?;
|
||||
let account = accounts
|
||||
.get_selected_account()
|
||||
.await
|
||||
.context("failed to get account")?;
|
||||
assert_eq!(account.is_open().await, false);
|
||||
|
||||
@@ -750,28 +720,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Tests that accounts share stock string translations.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_accounts_share_translations() -> Result<()> {
|
||||
let dir = tempfile::tempdir().unwrap();
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
accounts.add_account().await?;
|
||||
accounts.add_account().await?;
|
||||
|
||||
let account1 = accounts.get_account(1).context("failed to get account 1")?;
|
||||
let account2 = accounts.get_account(2).context("failed to get account 2")?;
|
||||
|
||||
assert_eq!(stock_str::no_messages(&account1).await, "No messages.");
|
||||
assert_eq!(stock_str::no_messages(&account2).await, "No messages.");
|
||||
account1
|
||||
.set_stock_translation(StockMessage::NoMessages, "foobar".to_string())
|
||||
.await?;
|
||||
assert_eq!(stock_str::no_messages(&account1).await, "foobar");
|
||||
assert_eq!(stock_str::no_messages(&account2).await, "foobar");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
753
src/authres.rs
753
src/authres.rs
@@ -1,753 +0,0 @@
|
||||
//! Parsing and handling of the Authentication-Results header.
|
||||
//! See the comment on [`handle_authres`] for more.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::collections::BTreeSet;
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::Result;
|
||||
use mailparse::MailHeaderMap;
|
||||
use mailparse::ParsedMail;
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::tools::time;
|
||||
use crate::tools::EmailAddress;
|
||||
|
||||
/// `authres` is short for the Authentication-Results header, defined in
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc8601>, which contains info
|
||||
/// about whether DKIM and SPF passed.
|
||||
///
|
||||
/// To mitigate From forgery, we remember for each sending domain whether it is known
|
||||
/// to have valid DKIM. If an email from such a domain comes with invalid DKIM,
|
||||
/// we don't allow changing the autocrypt key.
|
||||
///
|
||||
/// See <https://github.com/deltachat/deltachat-core-rust/issues/3507>.
|
||||
pub(crate) async fn handle_authres(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
from: &str,
|
||||
message_time: i64,
|
||||
) -> Result<DkimResults> {
|
||||
let from_domain = match EmailAddress::new(from) {
|
||||
Ok(email) => email.domain,
|
||||
Err(e) => {
|
||||
warn!(context, "invalid email {:#}", e);
|
||||
// This email is invalid, but don't return an error, we still want to
|
||||
// add a stub to the database so that it's not downloaded again
|
||||
return Ok(DkimResults::default());
|
||||
}
|
||||
};
|
||||
|
||||
let authres = parse_authres_headers(&mail.get_headers(), &from_domain);
|
||||
update_authservid_candidates(context, &authres).await?;
|
||||
compute_dkim_results(context, authres, &from_domain, message_time).await
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub(crate) struct DkimResults {
|
||||
/// Whether DKIM passed for this particular e-mail.
|
||||
pub dkim_passed: bool,
|
||||
/// Whether DKIM is known to work for e-mails coming from the sender's domain,
|
||||
/// i.e. whether we expect DKIM to work.
|
||||
pub dkim_should_work: bool,
|
||||
/// Whether changing the public Autocrypt key should be allowed.
|
||||
/// This is false if we expected DKIM to work (dkim_works=true),
|
||||
/// but it failed now (dkim_passed=false).
|
||||
pub allow_keychange: bool,
|
||||
}
|
||||
|
||||
impl fmt::Display for DkimResults {
|
||||
fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(
|
||||
fmt,
|
||||
"DKIM Results: Passed={}, Works={}, Allow_Keychange={}",
|
||||
self.dkim_passed, self.dkim_should_work, self.allow_keychange
|
||||
)?;
|
||||
if !self.allow_keychange {
|
||||
write!(fmt, " KEYCHANGES NOT ALLOWED!!!!")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
type AuthservId = String;
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum DkimResult {
|
||||
/// The header explicitly said that DKIM passed
|
||||
Passed,
|
||||
/// The header explicitly said that DKIM failed
|
||||
Failed,
|
||||
/// The header didn't say anything about DKIM; this might mean that it wasn't
|
||||
/// checked, but it might also mean that it failed. This is because some providers
|
||||
/// (e.g. ik.me, mail.ru, posteo.de) don't add `dkim=none` to their
|
||||
/// Authentication-Results if there was no DKIM.
|
||||
Nothing,
|
||||
}
|
||||
|
||||
type ParsedAuthresHeaders = Vec<(AuthservId, DkimResult)>;
|
||||
|
||||
fn parse_authres_headers(
|
||||
headers: &mailparse::headers::Headers<'_>,
|
||||
from_domain: &str,
|
||||
) -> ParsedAuthresHeaders {
|
||||
let mut res = Vec::new();
|
||||
for header_value in headers.get_all_values(HeaderDef::AuthenticationResults.into()) {
|
||||
let header_value = remove_comments(&header_value);
|
||||
|
||||
if let Some(mut authserv_id) = header_value.split(';').next() {
|
||||
if authserv_id.contains(char::is_whitespace) || authserv_id.is_empty() {
|
||||
// Outlook violates the RFC by not adding an authserv-id at all, which we notice
|
||||
// because there is whitespace in the first identifier before the ';'.
|
||||
// Authentication-Results-parsing still works securely because they remove incoming
|
||||
// Authentication-Results headers.
|
||||
// We just use an arbitrary authserv-id, it will work for Outlook, and in general,
|
||||
// with providers not implementing the RFC correctly, someone can trick us
|
||||
// into thinking that an incoming email is DKIM-correct, anyway.
|
||||
// The most important thing here is that we have some valid `authserv_id`.
|
||||
authserv_id = "invalidAuthservId";
|
||||
}
|
||||
let dkim_passed = parse_one_authres_header(&header_value, from_domain);
|
||||
res.push((authserv_id.to_string(), dkim_passed));
|
||||
}
|
||||
}
|
||||
|
||||
res
|
||||
}
|
||||
|
||||
/// The headers can contain comments that look like this:
|
||||
/// ```text
|
||||
/// Authentication-Results: (this is a comment) gmx.net; (another; comment) dkim=pass;
|
||||
/// ```
|
||||
fn remove_comments(header: &str) -> Cow<'_, str> {
|
||||
// In Pomsky, this is:
|
||||
// "(" Codepoint* lazy ")"
|
||||
// See https://playground.pomsky-lang.org/?text=%22(%22%20Codepoint*%20lazy%20%22)%22
|
||||
static RE: Lazy<regex::Regex> = Lazy::new(|| regex::Regex::new(r"\([\s\S]*?\)").unwrap());
|
||||
|
||||
RE.replace_all(header, " ")
|
||||
}
|
||||
|
||||
/// Parses a single Authentication-Results header, like:
|
||||
///
|
||||
/// ```text
|
||||
/// Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
|
||||
/// ```
|
||||
fn parse_one_authres_header(header_value: &str, from_domain: &str) -> DkimResult {
|
||||
if let Some((before_dkim_part, dkim_to_end)) = header_value.split_once("dkim=") {
|
||||
// Check that the character right before `dkim=` is a space or a tab
|
||||
// so that we wouldn't e.g. mistake `notdkim=pass` for `dkim=pass`
|
||||
if before_dkim_part.ends_with(' ') || before_dkim_part.ends_with('\t') {
|
||||
let dkim_part = dkim_to_end.split(';').next().unwrap_or_default();
|
||||
let dkim_parts: Vec<_> = dkim_part.split_whitespace().collect();
|
||||
if let Some(&"pass") = dkim_parts.first() {
|
||||
// DKIM headers contain a header.d or header.i field
|
||||
// that says which domain signed. We have to check ourselves
|
||||
// that this is the same domain as in the From header.
|
||||
let header_d: &str = &format!("header.d={}", &from_domain);
|
||||
let header_i: &str = &format!("header.i=@{}", &from_domain);
|
||||
|
||||
if dkim_parts.contains(&header_d) || dkim_parts.contains(&header_i) {
|
||||
// We have found a `dkim=pass` header!
|
||||
return DkimResult::Passed;
|
||||
}
|
||||
} else {
|
||||
// dkim=fail, dkim=none, ...
|
||||
return DkimResult::Failed;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DkimResult::Nothing
|
||||
}
|
||||
|
||||
/// ## About authserv-ids
|
||||
///
|
||||
/// After having checked DKIM, our email server adds an Authentication-Results header.
|
||||
///
|
||||
/// Now, an attacker could just add an Authentication-Results header that says dkim=pass
|
||||
/// in order to make us think that DKIM was correct in their From-forged email.
|
||||
///
|
||||
/// In order to prevent this, each email server adds its authserv-id to the
|
||||
/// Authentication-Results header, e.g. Testrun's authserv-id is `testrun.org`, Gmail's
|
||||
/// is `mx.google.com`. When Testrun gets a mail delivered from outside, it will then
|
||||
/// remove any Authentication-Results headers whose authserv-id is also `testrun.org`.
|
||||
///
|
||||
/// We need to somehow find out the authserv-id(s) of our email server, so that
|
||||
/// we can use the Authentication-Results with the right authserv-id.
|
||||
///
|
||||
/// ## What this function does
|
||||
///
|
||||
/// When receiving an email, this function is called and updates the candidates for
|
||||
/// our server's authserv-id, i.e. what we think our server's authserv-id is.
|
||||
///
|
||||
/// Usually, every incoming email has Authentication-Results with our server's
|
||||
/// authserv-id, so, the intersection of the existing authserv-ids and the incoming
|
||||
/// authserv-ids for our server's authserv-id is a good guess for our server's
|
||||
/// authserv-id. When this intersection is empty, we assume that the authserv-id has
|
||||
/// changed and start over with the new authserv-ids.
|
||||
///
|
||||
/// See [`handle_authres`].
|
||||
async fn update_authservid_candidates(
|
||||
context: &Context,
|
||||
authres: &ParsedAuthresHeaders,
|
||||
) -> Result<()> {
|
||||
let mut new_ids: BTreeSet<&str> = authres
|
||||
.iter()
|
||||
.map(|(authserv_id, _dkim_passed)| authserv_id.as_str())
|
||||
.collect();
|
||||
if new_ids.is_empty() {
|
||||
// The incoming message doesn't contain any authentication results, maybe it's a
|
||||
// self-sent or a mailer-daemon message
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let old_config = context.get_config(Config::AuthservIdCandidates).await?;
|
||||
let old_ids = parse_authservid_candidates_config(&old_config);
|
||||
let intersection: BTreeSet<&str> = old_ids.intersection(&new_ids).copied().collect();
|
||||
if !intersection.is_empty() {
|
||||
new_ids = intersection;
|
||||
}
|
||||
// If there were no AuthservIdCandidates previously, just start with
|
||||
// the ones from the incoming email
|
||||
|
||||
if old_ids != new_ids {
|
||||
let new_config = new_ids.into_iter().collect::<Vec<_>>().join(" ");
|
||||
context
|
||||
.set_config(Config::AuthservIdCandidates, Some(&new_config))
|
||||
.await?;
|
||||
// Updating the authservid candidates may mean that we now consider
|
||||
// emails as "failed" which "passed" previously, so we need to
|
||||
// reset our expectation which DKIMs work.
|
||||
clear_dkim_works(context).await?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Use the parsed authres and the authservid candidates to compute whether DKIM passed
|
||||
/// and whether a keychange should be allowed.
|
||||
///
|
||||
/// We track in the `sending_domains` table whether we get positive Authentication-Results
|
||||
/// for mails from a contact (meaning that their provider properly authenticates against
|
||||
/// our provider).
|
||||
///
|
||||
/// Once a contact is known to come with positive Authentication-Resutls (dkim: pass),
|
||||
/// we don't accept Autocrypt key changes if they come with negative Authentication-Results.
|
||||
async fn compute_dkim_results(
|
||||
context: &Context,
|
||||
mut authres: ParsedAuthresHeaders,
|
||||
from_domain: &str,
|
||||
message_time: i64,
|
||||
) -> Result<DkimResults> {
|
||||
let mut dkim_passed = false;
|
||||
|
||||
let ids_config = context.get_config(Config::AuthservIdCandidates).await?;
|
||||
let ids = parse_authservid_candidates_config(&ids_config);
|
||||
|
||||
// Remove all foreign authentication results
|
||||
authres.retain(|(authserv_id, _dkim_passed)| ids.contains(authserv_id.as_str()));
|
||||
|
||||
if authres.is_empty() {
|
||||
// If the authentication results are empty, then our provider doesn't add them
|
||||
// and an attacker could just add their own Authentication-Results, making us
|
||||
// think that DKIM passed. So, in this case, we can as well assume that DKIM passed.
|
||||
dkim_passed = true;
|
||||
} else {
|
||||
for (_authserv_id, current_dkim_passed) in authres {
|
||||
match current_dkim_passed {
|
||||
DkimResult::Passed => {
|
||||
dkim_passed = true;
|
||||
break;
|
||||
}
|
||||
DkimResult::Failed => {
|
||||
dkim_passed = false;
|
||||
break;
|
||||
}
|
||||
DkimResult::Nothing => {
|
||||
// Continue looking for an Authentication-Results header
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let last_working_timestamp = dkim_works_timestamp(context, from_domain).await?;
|
||||
let mut dkim_should_work = dkim_should_work(last_working_timestamp)?;
|
||||
if message_time > last_working_timestamp && dkim_passed {
|
||||
set_dkim_works_timestamp(context, from_domain, message_time).await?;
|
||||
dkim_should_work = true;
|
||||
}
|
||||
|
||||
Ok(DkimResults {
|
||||
dkim_passed,
|
||||
dkim_should_work,
|
||||
allow_keychange: dkim_passed || !dkim_should_work,
|
||||
})
|
||||
}
|
||||
|
||||
/// Whether DKIM in emails from this domain should be considered to work.
|
||||
fn dkim_should_work(last_working_timestamp: i64) -> Result<bool> {
|
||||
// When we get an email with valid DKIM-Authentication-Results,
|
||||
// then we assume that DKIM works for 30 days from this time on.
|
||||
let should_work_until = last_working_timestamp + 3600 * 24 * 30;
|
||||
|
||||
let dkim_ever_worked = last_working_timestamp > 0;
|
||||
|
||||
// We're using time() here and not the time when the message
|
||||
// claims to have been sent (passed around as `message_time`)
|
||||
// because otherwise an attacker could just put a time way
|
||||
// in the future into the `Date` header and then we would
|
||||
// assume that DKIM doesn't have to be valid anymore.
|
||||
let dkim_should_work_now = should_work_until > time();
|
||||
Ok(dkim_ever_worked && dkim_should_work_now)
|
||||
}
|
||||
|
||||
async fn dkim_works_timestamp(context: &Context, from_domain: &str) -> Result<i64, anyhow::Error> {
|
||||
let last_working_timestamp: i64 = context
|
||||
.sql
|
||||
.query_get_value(
|
||||
"SELECT dkim_works FROM sending_domains WHERE domain=?",
|
||||
paramsv![from_domain],
|
||||
)
|
||||
.await?
|
||||
.unwrap_or(0);
|
||||
Ok(last_working_timestamp)
|
||||
}
|
||||
|
||||
async fn set_dkim_works_timestamp(
|
||||
context: &Context,
|
||||
from_domain: &str,
|
||||
timestamp: i64,
|
||||
) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO sending_domains (domain, dkim_works) VALUES (?,?)
|
||||
ON CONFLICT(domain) DO UPDATE SET dkim_works=excluded.dkim_works",
|
||||
paramsv![from_domain, timestamp],
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn clear_dkim_works(context: &Context) -> Result<()> {
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM sending_domains", paramsv![])
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_authservid_candidates_config(config: &Option<String>) -> BTreeSet<&str> {
|
||||
config
|
||||
.as_deref()
|
||||
.map(|c| c.split_whitespace().collect())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::indexing_slicing)]
|
||||
use std::time::Duration;
|
||||
|
||||
use tokio::fs;
|
||||
use tokio::io::AsyncReadExt;
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::e2ee;
|
||||
use crate::mimeparser;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::securejoin::get_securejoin_qr;
|
||||
use crate::securejoin::join_securejoin;
|
||||
use crate::test_utils;
|
||||
use crate::test_utils::TestContext;
|
||||
use crate::test_utils::TestContextManager;
|
||||
use crate::tools;
|
||||
|
||||
#[test]
|
||||
fn test_remove_comments() {
|
||||
let header = "Authentication-Results: mx3.messagingengine.com;
|
||||
dkim=pass (1024-bit rsa key sha256) header.d=riseup.net;"
|
||||
.to_string();
|
||||
assert_eq!(
|
||||
remove_comments(&header),
|
||||
"Authentication-Results: mx3.messagingengine.com;
|
||||
dkim=pass header.d=riseup.net;"
|
||||
);
|
||||
|
||||
let header = ") aaa (".to_string();
|
||||
assert_eq!(remove_comments(&header), ") aaa (");
|
||||
|
||||
let header = "((something weird) no comment".to_string();
|
||||
assert_eq!(remove_comments(&header), " no comment");
|
||||
|
||||
let header = "🎉(🎉(🎉))🎉(".to_string();
|
||||
assert_eq!(remove_comments(&header), "🎉 )🎉(");
|
||||
|
||||
// Comments are allowed to include whitespace
|
||||
let header = "(com\n\t\r\nment) no comment (comment)".to_string();
|
||||
assert_eq!(remove_comments(&header), " no comment ");
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_parse_authentication_results() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
t.configure_addr("alice@gmx.net").await;
|
||||
let bytes = b"Authentication-Results: gmx.net; dkim=pass header.i=@slack.com
|
||||
Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
("gmx.net".to_string(), DkimResult::Passed),
|
||||
("gmx.net".to_string(), DkimResult::Nothing)
|
||||
]
|
||||
);
|
||||
|
||||
let bytes = b"Authentication-Results: gmx.net; notdkim=pass header.i=@slack.com
|
||||
Authentication-Results: gmx.net; notdkim=pass header.i=@amazonses.com";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
("gmx.net".to_string(), DkimResult::Nothing),
|
||||
("gmx.net".to_string(), DkimResult::Nothing)
|
||||
]
|
||||
);
|
||||
|
||||
let bytes = b"Authentication-Results: gmx.net; dkim=pass header.i=@amazonses.com";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
|
||||
assert_eq!(actual, vec![("gmx.net".to_string(), DkimResult::Nothing)],);
|
||||
|
||||
// Weird Authentication-Results from Outlook without an authserv-id
|
||||
let bytes = b"Authentication-Results: spf=pass (sender IP is 40.92.73.85)
|
||||
smtp.mailfrom=hotmail.com; dkim=pass (signature was verified)
|
||||
header.d=hotmail.com;dmarc=pass action=none
|
||||
header.from=hotmail.com;compauth=pass reason=100";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "hotmail.com");
|
||||
// At this point, the most important thing to test is that there are no
|
||||
// authserv-ids with whitespace in them.
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![("invalidAuthservId".to_string(), DkimResult::Passed)]
|
||||
);
|
||||
|
||||
let bytes = b"Authentication-Results: gmx.net; dkim=none header.i=@slack.com
|
||||
Authentication-Results: gmx.net; dkim=pass header.i=@slack.com";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "slack.com");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
("gmx.net".to_string(), DkimResult::Failed),
|
||||
("gmx.net".to_string(), DkimResult::Passed)
|
||||
]
|
||||
);
|
||||
|
||||
// ';' in comments
|
||||
let bytes = b"Authentication-Results: mx1.riseup.net;
|
||||
dkim=pass (1024-bit key; unprotected) header.d=yandex.ru header.i=@yandex.ru header.a=rsa-sha256 header.s=mail header.b=avNJu6sw;
|
||||
dkim-atps=neutral";
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "yandex.ru");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![("mx1.riseup.net".to_string(), DkimResult::Passed)]
|
||||
);
|
||||
|
||||
let bytes = br#"Authentication-Results: box.hispanilandia.net;
|
||||
dkim=fail reason="signature verification failed" (2048-bit key; secure) header.d=disroot.org header.i=@disroot.org header.b="kqh3WUKq";
|
||||
dkim-atps=neutral
|
||||
Authentication-Results: box.hispanilandia.net; dmarc=pass (p=quarantine dis=none) header.from=disroot.org
|
||||
Authentication-Results: box.hispanilandia.net; spf=pass smtp.mailfrom=adbenitez@disroot.org"#;
|
||||
let mail = mailparse::parse_mail(bytes)?;
|
||||
let actual = parse_authres_headers(&mail.get_headers(), "disroot.org");
|
||||
assert_eq!(
|
||||
actual,
|
||||
vec![
|
||||
("box.hispanilandia.net".to_string(), DkimResult::Failed),
|
||||
("box.hispanilandia.net".to_string(), DkimResult::Nothing),
|
||||
("box.hispanilandia.net".to_string(), DkimResult::Nothing),
|
||||
]
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_update_authservid_candidates() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
update_authservid_candidates_test(&t, &["mx3.messagingengine.com"]).await;
|
||||
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
|
||||
assert_eq!(candidates, "mx3.messagingengine.com");
|
||||
|
||||
// "mx4.messagingengine.com" seems to be the new authserv-id, DC should accept it
|
||||
update_authservid_candidates_test(&t, &["mx4.messagingengine.com"]).await;
|
||||
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
|
||||
assert_eq!(candidates, "mx4.messagingengine.com");
|
||||
|
||||
// A message without any Authentication-Results headers shouldn't remove all
|
||||
// candidates since it could be a mailer-daemon message or so
|
||||
update_authservid_candidates_test(&t, &[]).await;
|
||||
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
|
||||
assert_eq!(candidates, "mx4.messagingengine.com");
|
||||
|
||||
update_authservid_candidates_test(&t, &["mx4.messagingengine.com", "someotherdomain.com"])
|
||||
.await;
|
||||
let candidates = t.get_config(Config::AuthservIdCandidates).await?.unwrap();
|
||||
assert_eq!(candidates, "mx4.messagingengine.com");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Calls update_authservid_candidates(), meant for using in a test.
|
||||
///
|
||||
/// update_authservid_candidates() only looks at the keys of its
|
||||
/// `authentication_results` parameter. So, this function takes `incoming_ids`
|
||||
/// and adds some AuthenticationResults to get the HashMap we need.
|
||||
async fn update_authservid_candidates_test(context: &Context, incoming_ids: &[&str]) {
|
||||
let v = incoming_ids
|
||||
.iter()
|
||||
.map(|id| (id.to_string(), DkimResult::Passed))
|
||||
.collect();
|
||||
update_authservid_candidates(context, &v).await.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_realworld_authentication_results() -> Result<()> {
|
||||
let mut test_failed = false;
|
||||
|
||||
let dir = tools::read_dir("test-data/message/dkimchecks-2022-09-28/".as_ref())
|
||||
.await
|
||||
.unwrap();
|
||||
let mut bytes = Vec::new();
|
||||
for entry in dir {
|
||||
if !entry.file_type().await.unwrap().is_dir() {
|
||||
continue;
|
||||
}
|
||||
let self_addr = entry.file_name().into_string().unwrap();
|
||||
let self_domain = EmailAddress::new(&self_addr).unwrap().domain;
|
||||
let authres_parsing_works = [
|
||||
"ik.me",
|
||||
"web.de",
|
||||
"posteo.de",
|
||||
"gmail.com",
|
||||
"hotmail.com",
|
||||
"mail.ru",
|
||||
"aol.com",
|
||||
"yahoo.com",
|
||||
"icloud.com",
|
||||
"fastmail.com",
|
||||
"mail.de",
|
||||
"outlook.com",
|
||||
"gmx.de",
|
||||
"testrun.org",
|
||||
]
|
||||
.contains(&self_domain.as_str());
|
||||
|
||||
let t = TestContext::new().await;
|
||||
t.configure_addr(&self_addr).await;
|
||||
if !authres_parsing_works {
|
||||
println!("========= Receiving as {} =========", &self_addr);
|
||||
}
|
||||
|
||||
// Simulate receiving all emails once, so that we have the correct authserv-ids
|
||||
let mut dir = tools::read_dir(&entry.path()).await.unwrap();
|
||||
|
||||
// The ordering in which the emails are received can matter;
|
||||
// the test _should_ pass for every ordering.
|
||||
dir.sort_by_key(|d| d.file_name());
|
||||
//rand::seq::SliceRandom::shuffle(&mut dir[..], &mut rand::thread_rng());
|
||||
|
||||
for entry in &dir {
|
||||
let mut file = fs::File::open(entry.path()).await?;
|
||||
bytes.clear();
|
||||
file.read_to_end(&mut bytes).await.unwrap();
|
||||
|
||||
let mail = mailparse::parse_mail(&bytes)?;
|
||||
let from = &mimeparser::get_from(&mail.headers)[0].addr;
|
||||
|
||||
let res = handle_authres(&t, &mail, from, time()).await?;
|
||||
assert!(res.allow_keychange);
|
||||
}
|
||||
|
||||
for entry in &dir {
|
||||
let mut file = fs::File::open(entry.path()).await?;
|
||||
bytes.clear();
|
||||
file.read_to_end(&mut bytes).await.unwrap();
|
||||
|
||||
let mail = mailparse::parse_mail(&bytes)?;
|
||||
let from = &mimeparser::get_from(&mail.headers)[0].addr;
|
||||
|
||||
let res = handle_authres(&t, &mail, from, time()).await?;
|
||||
if !res.allow_keychange {
|
||||
println!(
|
||||
"!!!!!! FAILURE Receiving {:?}, keychange is not allowed !!!!!!",
|
||||
entry.path()
|
||||
);
|
||||
test_failed = true;
|
||||
}
|
||||
|
||||
let from_domain = EmailAddress::new(from).unwrap().domain;
|
||||
assert_eq!(
|
||||
res.dkim_should_work,
|
||||
dkim_should_work(dkim_works_timestamp(&t, &from_domain).await?)?
|
||||
);
|
||||
assert_eq!(res.dkim_passed, res.dkim_should_work);
|
||||
|
||||
// delta.blinzeln.de and gmx.de have invalid DKIM, so the DKIM check should fail
|
||||
let expected_result = (from_domain != "delta.blinzeln.de") && (from_domain != "gmx.de")
|
||||
// These are (fictional) forged emails where the attacker added a fake
|
||||
// Authentication-Results before sending the email
|
||||
&& from != "forged-authres-added@example.com"
|
||||
// Other forged emails
|
||||
&& !from.starts_with("forged");
|
||||
|
||||
if res.dkim_passed != expected_result {
|
||||
if authres_parsing_works {
|
||||
println!(
|
||||
"!!!!!! FAILURE Receiving {:?}, order {:#?} wrong result: !!!!!!",
|
||||
entry.path(),
|
||||
dir.iter().map(|e| e.file_name()).collect::<Vec<_>>()
|
||||
);
|
||||
test_failed = true;
|
||||
}
|
||||
println!("From {}: {}", from_domain, res.dkim_passed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert!(!test_failed);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_handle_authres() {
|
||||
let t = TestContext::new().await;
|
||||
|
||||
// Even if the format is wrong and parsing fails, handle_authres() shouldn't
|
||||
// return an Err because this would prevent the message from being added
|
||||
// to the database and downloaded again and again
|
||||
let bytes = b"Authentication-Results: dkim=";
|
||||
let mail = mailparse::parse_mail(bytes).unwrap();
|
||||
handle_authres(&t, &mail, "invalidfrom.com", time())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[ignore = "Disallowing keychanges is disabled for now"]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_handle_authres_fails() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
// Bob sends Alice a message, so she gets his key
|
||||
tcm.send_recv_accept(&bob, &alice, "Hi").await;
|
||||
|
||||
// We don't need bob anymore, let's make sure it's not accidentally used
|
||||
drop(bob);
|
||||
|
||||
// Assume Alice receives an email from bob@example.net with
|
||||
// correct DKIM -> `set_dkim_works()` was called
|
||||
set_dkim_works_timestamp(&alice, "example.net", time()).await?;
|
||||
// And Alice knows her server's authserv-id
|
||||
alice
|
||||
.set_config(Config::AuthservIdCandidates, Some("example.org"))
|
||||
.await?;
|
||||
|
||||
tcm.section("An attacker, bob2, sends a from-forged email to Alice!");
|
||||
|
||||
// Sleep to make sure key reset is ignored because of DKIM failure
|
||||
// and not because reordering is suspected.
|
||||
tokio::time::sleep(std::time::Duration::from_millis(1100)).await;
|
||||
|
||||
let bob2 = tcm.unconfigured().await;
|
||||
bob2.configure_addr("bob@example.net").await;
|
||||
e2ee::ensure_secret_key_exists(&bob2).await?;
|
||||
|
||||
let chat = bob2.create_chat(&alice).await;
|
||||
let mut sent = bob2
|
||||
.send_text(chat.id, "Please send me lots of money")
|
||||
.await;
|
||||
|
||||
sent.payload
|
||||
.insert_str(0, "Authentication-Results: example.org; dkim=fail");
|
||||
|
||||
let received = alice.recv_msg(&sent).await;
|
||||
|
||||
// Assert that the error tells the user about the problem
|
||||
assert!(received.error.unwrap().contains("DKIM failed"));
|
||||
|
||||
let bob_state = Peerstate::from_addr(&alice, "bob@example.net")
|
||||
.await?
|
||||
.unwrap();
|
||||
|
||||
// Encryption preference is still mutual.
|
||||
assert_eq!(bob_state.prefer_encrypt, EncryptPreference::Mutual);
|
||||
|
||||
// Also check that the keypair was not changed
|
||||
assert_eq!(
|
||||
bob_state.public_key.unwrap(),
|
||||
test_utils::bob_keypair().public
|
||||
);
|
||||
|
||||
// Since Alice didn't change the key, Bob can't read her message
|
||||
let received = tcm
|
||||
.try_send_recv(&alice, &bob2, "My credit card number is 1234")
|
||||
.await;
|
||||
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.");
|
||||
tcm.section("To fix the key problems, Bob scans Alice's QR code.");
|
||||
|
||||
let qr = get_securejoin_qr(&alice.ctx, None).await.unwrap();
|
||||
join_securejoin(&bob2.ctx, &qr).await.unwrap();
|
||||
|
||||
loop {
|
||||
if let Some(mut sent) = bob2.pop_sent_msg_opt(Duration::ZERO).await {
|
||||
sent.payload
|
||||
.insert_str(0, "Authentication-Results: example.org; dkim=fail");
|
||||
alice.recv_msg(&sent).await;
|
||||
} else if let Some(sent) = alice.pop_sent_msg_opt(Duration::ZERO).await {
|
||||
bob2.recv_msg(&sent).await;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Unfortunately, securejoin currently doesn't work with authres-checking,
|
||||
// so these checks would fail:
|
||||
|
||||
// let contact_bob = alice.add_or_lookup_contact(&bob2).await;
|
||||
// assert_eq!(
|
||||
// contact_bob.is_verified(&alice.ctx).await.unwrap(),
|
||||
// VerifiedStatus::BidirectVerified
|
||||
// );
|
||||
|
||||
// let contact_alice = bob2.add_or_lookup_contact(&alice).await;
|
||||
// assert_eq!(
|
||||
// contact_alice.is_verified(&bob2.ctx).await.unwrap(),
|
||||
// VerifiedStatus::BidirectVerified
|
||||
// );
|
||||
|
||||
// // Bob can read Alice's messages again
|
||||
// let received = tcm
|
||||
// .try_send_recv(&alice, &bob2, "Can you read this again?")
|
||||
// .await;
|
||||
// assert_eq!(received.text.as_ref().unwrap(), "Can you read this again?");
|
||||
// assert!(received.error.is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
13
src/blob.rs
13
src/blob.rs
@@ -326,7 +326,10 @@ impl<'a> BlobObject<'a> {
|
||||
|
||||
// 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, img_wh, Some(20_000))? {
|
||||
if let Some(new_name) = self
|
||||
.recode_to_size(context, blob_abs, img_wh, Some(20_000))
|
||||
.await?
|
||||
{
|
||||
self.name = new_name;
|
||||
}
|
||||
Ok(())
|
||||
@@ -349,7 +352,8 @@ impl<'a> BlobObject<'a> {
|
||||
};
|
||||
|
||||
if self
|
||||
.recode_to_size(context, blob_abs, img_wh, None)?
|
||||
.recode_to_size(context, blob_abs, img_wh, None)
|
||||
.await?
|
||||
.is_some()
|
||||
{
|
||||
return Err(format_err!(
|
||||
@@ -359,7 +363,7 @@ impl<'a> BlobObject<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn recode_to_size(
|
||||
async fn recode_to_size(
|
||||
&self,
|
||||
context: &Context,
|
||||
mut blob_abs: PathBuf,
|
||||
@@ -734,7 +738,7 @@ mod tests {
|
||||
check_image_size(avatar_src, 1000, 1000);
|
||||
check_image_size(&avatar_blob, BALANCED_AVATAR_SIZE, BALANCED_AVATAR_SIZE);
|
||||
|
||||
async fn file_size(path_buf: &Path) -> u64 {
|
||||
async fn file_size(path_buf: &PathBuf) -> u64 {
|
||||
let file = File::open(path_buf).await.unwrap();
|
||||
file.metadata().await.unwrap().len()
|
||||
}
|
||||
@@ -742,6 +746,7 @@ mod tests {
|
||||
let blob = BlobObject::new_from_path(&t, &avatar_blob).await.unwrap();
|
||||
|
||||
blob.recode_to_size(&t, blob.to_abs_path(), 1000, Some(3000))
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(file_size(&avatar_blob).await <= 3000);
|
||||
assert!(file_size(&avatar_blob).await > 2000);
|
||||
|
||||
284
src/chat.rs
284
src/chat.rs
@@ -32,7 +32,6 @@ use crate::receive_imf::ReceivedMsg;
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::smtp::send_msg_to_smtp;
|
||||
use crate::stock_str;
|
||||
use crate::stock_str::ByContact;
|
||||
use crate::tools::{
|
||||
create_id, create_outgoing_rfc724_mid, create_smeared_timestamp, create_smeared_timestamps,
|
||||
get_abs_path, gm2local_offset, improve_single_line_input, time, IsNoneOrEmpty,
|
||||
@@ -41,7 +40,7 @@ use crate::webxdc::WEBXDC_SUFFIX;
|
||||
use crate::{location, sql};
|
||||
|
||||
/// An chat item, such as a message or a marker.
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub enum ChatItem {
|
||||
Message {
|
||||
msg_id: MsgId,
|
||||
@@ -415,6 +414,7 @@ impl ChatId {
|
||||
promote: bool,
|
||||
from_id: ContactId,
|
||||
) -> Result<()> {
|
||||
let msg_text = context.stock_protection_msg(protect, from_id).await;
|
||||
let cmd = match protect {
|
||||
ProtectionStatus::Protected => SystemMessage::ChatProtectionEnabled,
|
||||
ProtectionStatus::Unprotected => SystemMessage::ChatProtectionDisabled,
|
||||
@@ -423,11 +423,7 @@ impl ChatId {
|
||||
if promote {
|
||||
let mut msg = Message {
|
||||
viewtype: Viewtype::Text,
|
||||
text: Some(
|
||||
context
|
||||
.stock_protection_msg(protect, ByContact::SelfName)
|
||||
.await,
|
||||
),
|
||||
text: Some(msg_text),
|
||||
..Default::default()
|
||||
};
|
||||
msg.param.set_cmd(cmd);
|
||||
@@ -436,9 +432,7 @@ impl ChatId {
|
||||
add_info_msg_with_cmd(
|
||||
context,
|
||||
self,
|
||||
&context
|
||||
.stock_protection_msg(protect, ByContact::YouOrName(from_id))
|
||||
.await,
|
||||
&msg_text,
|
||||
cmd,
|
||||
create_smeared_timestamp(context).await,
|
||||
None,
|
||||
@@ -1135,11 +1129,6 @@ impl Chat {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Returns mailing list address where messages are sent to.
|
||||
pub fn get_mailinglist_addr(&self) -> Option<&str> {
|
||||
self.param.get(Param::ListPost)
|
||||
}
|
||||
|
||||
/// Returns profile image path for the chat.
|
||||
pub async fn get_profile_image(&self, context: &Context) -> Result<Option<PathBuf>> {
|
||||
if let Some(image_rel) = self.param.get(Param::ProfileImage) {
|
||||
@@ -2260,7 +2249,7 @@ pub async fn get_chat_msgs(
|
||||
let curr_day = curr_local_timestamp / 86400;
|
||||
if curr_day != last_day {
|
||||
ret.push(ChatItem::DayMarker {
|
||||
timestamp: curr_day * 86400, // Convert day back to Unix timestamp
|
||||
timestamp: curr_day,
|
||||
});
|
||||
last_day = curr_day;
|
||||
}
|
||||
@@ -2429,7 +2418,7 @@ pub(crate) async fn mark_old_messages_as_noticed(
|
||||
|
||||
pub async fn get_chat_media(
|
||||
context: &Context,
|
||||
chat_id: Option<ChatId>,
|
||||
chat_id: ChatId,
|
||||
msg_type: Viewtype,
|
||||
msg_type2: Viewtype,
|
||||
msg_type3: Viewtype,
|
||||
@@ -2440,13 +2429,11 @@ pub async fn get_chat_media(
|
||||
.query_map(
|
||||
"SELECT id
|
||||
FROM msgs
|
||||
WHERE (1=? OR chat_id=?)
|
||||
WHERE chat_id=?
|
||||
AND (type=? OR type=? OR type=?)
|
||||
AND hidden=0
|
||||
ORDER BY timestamp, id;",
|
||||
paramsv![
|
||||
chat_id.is_none(),
|
||||
chat_id.unwrap_or_else(|| ChatId::new(0)),
|
||||
chat_id,
|
||||
msg_type,
|
||||
if msg_type2 != Viewtype::Unknown {
|
||||
msg_type2
|
||||
@@ -2487,7 +2474,7 @@ pub async fn get_next_media(
|
||||
if let Ok(msg) = Message::load_from_db(context, curr_msg_id).await {
|
||||
let list: Vec<MsgId> = get_chat_media(
|
||||
context,
|
||||
Some(msg.chat_id),
|
||||
msg.chat_id,
|
||||
if msg_type != Viewtype::Unknown {
|
||||
msg_type
|
||||
} else {
|
||||
@@ -2531,7 +2518,7 @@ pub async fn get_chat_contacts(context: &Context, chat_id: ChatId) -> Result<Vec
|
||||
LEFT JOIN contacts c
|
||||
ON c.id=cc.contact_id
|
||||
WHERE cc.chat_id=?
|
||||
ORDER BY c.id=1, c.last_seen DESC, c.id DESC;",
|
||||
ORDER BY c.id=1, LOWER(c.name||c.addr), c.id;",
|
||||
paramsv![chat_id],
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
@@ -2750,7 +2737,7 @@ pub(crate) async fn add_contact_to_chat_ex(
|
||||
msg.viewtype = Viewtype::Text;
|
||||
|
||||
msg.text =
|
||||
Some(stock_str::msg_add_member(context, contact.get_addr(), ByContact::SelfName).await);
|
||||
Some(stock_str::msg_add_member(context, contact.get_addr(), ContactId::SELF).await);
|
||||
msg.param.set_cmd(SystemMessage::MemberAddedToGroup);
|
||||
msg.param.set(Param::Arg, contact.get_addr());
|
||||
msg.param.set_int(Param::Arg2, from_handshake.into());
|
||||
@@ -2794,7 +2781,7 @@ pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId)
|
||||
Ok(needs_attach)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub enum MuteDuration {
|
||||
NotMuted,
|
||||
Forever,
|
||||
@@ -2869,7 +2856,7 @@ pub async fn remove_contact_from_chat(
|
||||
let mut success = false;
|
||||
|
||||
/* we do not check if "contact_id" exists but just delete all records with the id from chats_contacts */
|
||||
/* this allows to delete pending references to deleted contacts. Of course, this should _not_ happen. */
|
||||
/* this allows to delete pending references to deleted contacts. Of course, this should _not_ happen. */
|
||||
if let Ok(chat) = Chat::load_from_db(context, chat_id).await {
|
||||
if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
|
||||
if !chat.is_self_in_chat(context).await? {
|
||||
@@ -2883,13 +2870,13 @@ pub async fn remove_contact_from_chat(
|
||||
if contact.id == ContactId::SELF {
|
||||
set_group_explicitly_left(context, &chat.grpid).await?;
|
||||
msg.text =
|
||||
Some(stock_str::msg_group_left(context, ByContact::SelfName).await);
|
||||
Some(stock_str::msg_group_left(context, ContactId::SELF).await);
|
||||
} else {
|
||||
msg.text = Some(
|
||||
stock_str::msg_del_member(
|
||||
context,
|
||||
contact.get_addr(),
|
||||
ByContact::SelfName,
|
||||
ContactId::SELF,
|
||||
)
|
||||
.await,
|
||||
);
|
||||
@@ -2982,8 +2969,7 @@ pub async fn set_chat_name(context: &Context, chat_id: ChatId, new_name: &str) -
|
||||
if chat.is_promoted() && !chat.is_mailing_list() && chat.typ != Chattype::Broadcast {
|
||||
msg.viewtype = Viewtype::Text;
|
||||
msg.text = Some(
|
||||
stock_str::msg_grp_name(context, &chat.name, &new_name, ByContact::SelfName)
|
||||
.await,
|
||||
stock_str::msg_grp_name(context, &chat.name, &new_name, ContactId::SELF).await,
|
||||
);
|
||||
msg.param.set_cmd(SystemMessage::GroupNameChanged);
|
||||
if !chat.name.is_empty() {
|
||||
@@ -3033,14 +3019,14 @@ pub async fn set_chat_profile_image(
|
||||
if new_image.as_ref().is_empty() {
|
||||
chat.param.remove(Param::ProfileImage);
|
||||
msg.param.remove(Param::Arg);
|
||||
msg.text = Some(stock_str::msg_grp_img_deleted(context, ByContact::SelfName).await);
|
||||
msg.text = Some(stock_str::msg_grp_img_deleted(context, ContactId::SELF).await);
|
||||
} else {
|
||||
let mut image_blob =
|
||||
BlobObject::new_from_path(context, Path::new(new_image.as_ref())).await?;
|
||||
image_blob.recode_to_avatar_size(context).await?;
|
||||
chat.param.set(Param::ProfileImage, image_blob.as_name());
|
||||
msg.param.set(Param::Arg, image_blob.as_name());
|
||||
msg.text = Some(stock_str::msg_grp_img_changed(context, ByContact::SelfName).await);
|
||||
msg.text = Some(stock_str::msg_grp_img_changed(context, ContactId::SELF).await);
|
||||
}
|
||||
chat.update_param(context).await?;
|
||||
if chat.is_promoted() && !chat.is_mailing_list() {
|
||||
@@ -3376,15 +3362,6 @@ pub(crate) async fn delete_and_reset_all_device_msgs(context: &Context) -> Resul
|
||||
.sql
|
||||
.execute("DELETE FROM devmsglabels;", paramsv![])
|
||||
.await?;
|
||||
|
||||
// Insert labels for welcome messages to avoid them being readded on reconfiguration.
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
r#"INSERT INTO devmsglabels (label) VALUES ("core-welcome-image"), ("core-welcome")"#,
|
||||
paramsv![],
|
||||
)
|
||||
.await?;
|
||||
context.set_config(Config::QuotaExceeding, None).await?;
|
||||
Ok(())
|
||||
}
|
||||
@@ -3709,11 +3686,11 @@ mod tests {
|
||||
|
||||
// create group and sync it to the second device
|
||||
let a1_chat_id = create_group_chat(&a1, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let sent = a1.send_text(a1_chat_id, "ho!").await;
|
||||
a1.send_text(a1_chat_id, "ho!").await;
|
||||
let a1_msg = a1.get_last_msg().await;
|
||||
let a1_chat = Chat::load_from_db(&a1, a1_chat_id).await?;
|
||||
|
||||
let a2_msg = a2.recv_msg(&sent).await;
|
||||
let a2_msg = a2.recv_msg(&a1.pop_sent_msg().await).await;
|
||||
let a2_chat_id = a2_msg.chat_id;
|
||||
let a2_chat = Chat::load_from_db(&a2, a2_chat_id).await?;
|
||||
|
||||
@@ -4570,24 +4547,26 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_protection() -> Result<()> {
|
||||
async fn test_set_protection() {
|
||||
let t = TestContext::new_alice().await;
|
||||
t.set_config_bool(Config::BccSelf, false).await?;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
|
||||
assert!(!chat.is_protected());
|
||||
assert!(chat.is_unpromoted());
|
||||
|
||||
// enable protection on unpromoted chat, the info-message is added via add_info_msg()
|
||||
chat_id
|
||||
.set_protection(&t, ProtectionStatus::Protected)
|
||||
.await?;
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
|
||||
assert!(chat.is_protected());
|
||||
assert!(chat.is_unpromoted());
|
||||
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0).await?;
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0).await.unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
let msg = t.get_last_msg_in(chat_id).await;
|
||||
@@ -4598,9 +4577,10 @@ mod tests {
|
||||
// disable protection again, still unpromoted
|
||||
chat_id
|
||||
.set_protection(&t, ProtectionStatus::Unprotected)
|
||||
.await?;
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
|
||||
assert!(!chat.is_protected());
|
||||
assert!(chat.is_unpromoted());
|
||||
|
||||
@@ -4610,20 +4590,21 @@ mod tests {
|
||||
assert_eq!(msg.get_state(), MessageState::InNoticed);
|
||||
|
||||
// send a message, this switches to promoted state
|
||||
send_text_msg(&t, chat_id, "hi!".to_string()).await?;
|
||||
send_text_msg(&t, chat_id, "hi!".to_string()).await.unwrap();
|
||||
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
|
||||
assert!(!chat.is_protected());
|
||||
assert!(!chat.is_unpromoted());
|
||||
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0).await?;
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0).await.unwrap();
|
||||
assert_eq!(msgs.len(), 3);
|
||||
|
||||
// enable protection on promoted chat, the info-message is sent via send_msg() this time
|
||||
chat_id
|
||||
.set_protection(&t, ProtectionStatus::Protected)
|
||||
.await?;
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
.await
|
||||
.unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
|
||||
assert!(chat.is_protected());
|
||||
assert!(!chat.is_unpromoted());
|
||||
|
||||
@@ -4631,8 +4612,6 @@ mod tests {
|
||||
assert!(msg.is_info());
|
||||
assert_eq!(msg.get_info_type(), SystemMessage::ChatProtectionEnabled);
|
||||
assert_eq!(msg.get_state(), MessageState::OutDelivered); // as bcc-self is disabled and there is nobody else in the chat
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
@@ -5345,35 +5324,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_info_message_wording() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let alice_grp = create_group_chat(&alice, ProtectionStatus::Unprotected, "grp").await?;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_grp,
|
||||
Contact::create(&alice, "", "bob@example.net").await?,
|
||||
)
|
||||
.await?;
|
||||
alice.send_text(alice_grp, "alice->bob").await;
|
||||
add_contact_to_chat(
|
||||
&alice,
|
||||
alice_grp,
|
||||
Contact::create(&alice, "", "claire@example.org").await?,
|
||||
)
|
||||
.await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
let msg = Message::load_from_db(&alice, sent2.sender_msg_id).await?;
|
||||
|
||||
// For DC, info message reads "You added"; for non-DC MUA, info message should not read "You added"
|
||||
let you_added = "You added";
|
||||
assert!(msg.get_text().unwrap().contains(you_added));
|
||||
assert!(!sent2.payload().contains(you_added));
|
||||
assert!(sent2.payload().contains("added by"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_can_send_group() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
@@ -5499,8 +5449,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(&alice).await?,
|
||||
"No encryption:\n\
|
||||
fiona@example.net\n\
|
||||
bob@example.net"
|
||||
bob@example.net\n\
|
||||
fiona@example.net"
|
||||
);
|
||||
|
||||
let direct_chat = bob.create_chat(&alice).await;
|
||||
@@ -5531,158 +5481,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_get_chat_media() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id1 = create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||
let chat_id2 = create_group_chat(&t, ProtectionStatus::Unprotected, "bar").await?;
|
||||
|
||||
assert_eq!(
|
||||
get_chat_media(
|
||||
&t,
|
||||
Some(chat_id1),
|
||||
Viewtype::Image,
|
||||
Viewtype::Sticker,
|
||||
Viewtype::Unknown
|
||||
)
|
||||
.await?
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
|
||||
async fn send_media(
|
||||
t: &TestContext,
|
||||
chat_id: ChatId,
|
||||
msg_type: Viewtype,
|
||||
name: &str,
|
||||
bytes: &[u8],
|
||||
) -> Result<MsgId> {
|
||||
let file = t.get_blobdir().join(name);
|
||||
tokio::fs::write(&file, bytes).await?;
|
||||
let mut msg = Message::new(msg_type);
|
||||
msg.set_file(file.to_str().unwrap(), None);
|
||||
send_msg(t, chat_id, &mut msg).await
|
||||
}
|
||||
|
||||
send_media(
|
||||
&t,
|
||||
chat_id1,
|
||||
Viewtype::Image,
|
||||
"a.jpg",
|
||||
include_bytes!("../test-data/image/rectangle200x180-rotated.jpg"),
|
||||
)
|
||||
.await?;
|
||||
send_media(
|
||||
&t,
|
||||
chat_id1,
|
||||
Viewtype::Sticker,
|
||||
"b.png",
|
||||
include_bytes!("../test-data/image/avatar64x64.png"),
|
||||
)
|
||||
.await?;
|
||||
send_media(
|
||||
&t,
|
||||
chat_id2,
|
||||
Viewtype::Image,
|
||||
"c.jpg",
|
||||
include_bytes!("../test-data/image/avatar64x64.png"),
|
||||
)
|
||||
.await?;
|
||||
send_media(
|
||||
&t,
|
||||
chat_id2,
|
||||
Viewtype::Webxdc,
|
||||
"d.xdc",
|
||||
include_bytes!("../test-data/webxdc/minimal.xdc"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(
|
||||
get_chat_media(
|
||||
&t,
|
||||
Some(chat_id1),
|
||||
Viewtype::Image,
|
||||
Viewtype::Unknown,
|
||||
Viewtype::Unknown,
|
||||
)
|
||||
.await?
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
get_chat_media(
|
||||
&t,
|
||||
Some(chat_id1),
|
||||
Viewtype::Sticker,
|
||||
Viewtype::Unknown,
|
||||
Viewtype::Unknown,
|
||||
)
|
||||
.await?
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
get_chat_media(
|
||||
&t,
|
||||
Some(chat_id1),
|
||||
Viewtype::Sticker,
|
||||
Viewtype::Image,
|
||||
Viewtype::Unknown,
|
||||
)
|
||||
.await?
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
get_chat_media(
|
||||
&t,
|
||||
Some(chat_id2),
|
||||
Viewtype::Webxdc,
|
||||
Viewtype::Unknown,
|
||||
Viewtype::Unknown,
|
||||
)
|
||||
.await?
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(
|
||||
get_chat_media(
|
||||
&t,
|
||||
None,
|
||||
Viewtype::Image,
|
||||
Viewtype::Unknown,
|
||||
Viewtype::Unknown,
|
||||
)
|
||||
.await?
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(
|
||||
get_chat_media(
|
||||
&t,
|
||||
None,
|
||||
Viewtype::Image,
|
||||
Viewtype::Sticker,
|
||||
Viewtype::Unknown,
|
||||
)
|
||||
.await?
|
||||
.len(),
|
||||
3
|
||||
);
|
||||
assert_eq!(
|
||||
get_chat_media(
|
||||
&t,
|
||||
None,
|
||||
Viewtype::Image,
|
||||
Viewtype::Sticker,
|
||||
Viewtype::Webxdc,
|
||||
)
|
||||
.await?
|
||||
.len(),
|
||||
4
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use sha1::{Digest, Sha1};
|
||||
fn str_to_angle(s: &str) -> f64 {
|
||||
let bytes = s.as_bytes();
|
||||
let result = Sha1::digest(bytes);
|
||||
let checksum: u16 = result.first().map_or(0, |&x| u16::from(x))
|
||||
let checksum: u16 = result.get(0).map_or(0, |&x| u16::from(x))
|
||||
+ 256 * result.get(1).map_or(0, |&x| u16::from(x));
|
||||
f64::from(checksum) / 65536.0 * 360.0
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! # Key-value configuration management.
|
||||
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use strum::{EnumProperty as EnumPropertyTrait, IntoEnumIterator};
|
||||
use strum::{EnumProperty, IntoEnumIterator};
|
||||
use strum_macros::{AsRefStr, Display, EnumIter, EnumProperty, EnumString};
|
||||
|
||||
use crate::blob::BlobObject;
|
||||
@@ -55,7 +55,7 @@ pub enum Config {
|
||||
Selfstatus,
|
||||
Selfavatar,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
#[strum(props(default = "0"))]
|
||||
BccSelf,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
@@ -184,12 +184,6 @@ pub enum Config {
|
||||
/// In a future versions, this switch may be removed.
|
||||
#[strum(props(default = "0"))]
|
||||
SendSyncMsgs,
|
||||
|
||||
/// Space-separated list of all the authserv-ids which we believe
|
||||
/// may be the one of our email server.
|
||||
///
|
||||
/// See `crate::authres::update_authservid_candidates`.
|
||||
AuthservIdCandidates,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
@@ -204,7 +198,7 @@ impl Context {
|
||||
let rel_path = self.sql.get_raw_config(key).await?;
|
||||
rel_path.map(|p| get_abs_path(self, &p).to_string_lossy().into_owned())
|
||||
}
|
||||
Config::SysVersion => Some((*DC_VERSION_STR).clone()),
|
||||
Config::SysVersion => Some((&*DC_VERSION_STR).clone()),
|
||||
Config::SysMsgsizeMaxRecommended => Some(format!("{}", RECOMMENDED_FILE_SIZE)),
|
||||
Config::SysConfigKeys => Some(get_config_keys_string()),
|
||||
_ => self.sql.get_raw_config(key).await?,
|
||||
@@ -282,24 +276,6 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_config_name_and_addr(&self) -> String {
|
||||
let name = self
|
||||
.get_config(Config::Displayname)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_default();
|
||||
let addr = self
|
||||
.get_config(Config::Addr)
|
||||
.await
|
||||
.unwrap_or_default()
|
||||
.unwrap_or_else(|| "unconfigured".to_string());
|
||||
if !name.is_empty() {
|
||||
format!("{} ({})", name, addr)
|
||||
} else {
|
||||
addr
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the given config key.
|
||||
/// If `None` is passed as a value the value is cleared and set to the default if there is one.
|
||||
pub async fn set_config(&self, key: Config, value: Option<&str>) -> Result<()> {
|
||||
|
||||
@@ -12,11 +12,9 @@ use percent_encoding::{utf8_percent_encode, NON_ALPHANUMERIC};
|
||||
use tokio::task;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::imap::Imap;
|
||||
use crate::job;
|
||||
use crate::log::LogExt;
|
||||
use crate::login_param::{CertificateChecks, LoginParam, ServerLoginParam, Socks5Config};
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::oauth2::get_oauth2_addr;
|
||||
@@ -103,11 +101,35 @@ impl Context {
|
||||
info!(self, "Configure ...");
|
||||
|
||||
let mut param = LoginParam::load_candidate_params(self).await?;
|
||||
let old_addr = self.get_config(Config::ConfiguredAddr).await?;
|
||||
let success = configure(self, &mut param).await;
|
||||
self.set_config(Config::NotifyAboutWrongPw, None).await?;
|
||||
|
||||
on_configure_completed(self, param, old_addr).await?;
|
||||
if let Some(provider) = param.provider {
|
||||
if let Some(config_defaults) = &provider.config_defaults {
|
||||
for def in config_defaults.iter() {
|
||||
if !self.config_exists(def.key).await? {
|
||||
info!(self, "apply config_defaults {}={}", def.key, def.value);
|
||||
self.set_config(def.key, Some(def.value)).await?;
|
||||
} else {
|
||||
info!(
|
||||
self,
|
||||
"skip already set config_defaults {}={}", def.key, def.value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !provider.after_login_hint.is_empty() {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text = Some(provider.after_login_hint.to_string());
|
||||
if chat::add_device_msg(self, Some("core-provider-info"), Some(&mut msg))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
warn!(self, "cannot add after_login_hint as core-provider-info");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
success?;
|
||||
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
|
||||
@@ -116,54 +138,6 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
async fn on_configure_completed(
|
||||
context: &Context,
|
||||
param: LoginParam,
|
||||
old_addr: Option<String>,
|
||||
) -> Result<()> {
|
||||
if let Some(provider) = param.provider {
|
||||
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?;
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"skip already set config_defaults {}={}", def.key, def.value
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !provider.after_login_hint.is_empty() {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
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()
|
||||
{
|
||||
warn!(context, "cannot add after_login_hint as core-provider-info");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(new_addr) = context.get_config(Config::ConfiguredAddr).await? {
|
||||
if let Some(old_addr) = old_addr {
|
||||
if !addr_cmp(&new_addr, &old_addr) {
|
||||
let mut msg = Message::new(Viewtype::Text);
|
||||
msg.text =
|
||||
Some(stock_str::aeap_explanation_and_link(context, old_addr, new_addr).await);
|
||||
chat::add_device_msg(context, None, Some(&mut msg))
|
||||
.await
|
||||
.ok_or_log_msg(context, "Cannot add AEAP explanation");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
progress!(ctx, 1);
|
||||
|
||||
@@ -196,7 +170,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
}
|
||||
// no oauth? - just continue it's no error
|
||||
|
||||
let parsed = EmailAddress::new(¶m.addr).context("Bad email-address")?;
|
||||
let parsed: EmailAddress = param.addr.parse().context("Bad email-address")?;
|
||||
let param_domain = parsed.domain;
|
||||
let param_addr_urlencoded = utf8_percent_encode(¶m.addr, NON_ALPHANUMERIC).to_string();
|
||||
|
||||
@@ -579,7 +553,8 @@ async fn try_imap_one_param(
|
||||
|
||||
let (_s, r) = async_channel::bounded(1);
|
||||
|
||||
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r) {
|
||||
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r).await
|
||||
{
|
||||
Err(err) => {
|
||||
info!(context, "failure: {}", err);
|
||||
return Err(ConfigurationError {
|
||||
@@ -667,9 +642,6 @@ async fn nicer_configuration_error(context: &Context, errors: Vec<ConfigurationE
|
||||
.to_lowercase()
|
||||
.contains("temporary failure in name resolution")
|
||||
|| e.msg.to_lowercase().contains("name or service not known")
|
||||
|| e.msg
|
||||
.to_lowercase()
|
||||
.contains("failed to lookup address information")
|
||||
}) {
|
||||
return stock_str::error_no_network(context).await;
|
||||
}
|
||||
|
||||
@@ -167,11 +167,6 @@ pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
|
||||
|
||||
/// String that indicates that something is left out or truncated.
|
||||
pub const DC_ELLIPSIS: &str = "[...]";
|
||||
// how many lines desktop can display when fullscreen (fullscreen at zoomlevel 1x)
|
||||
// (taken from "subjective" testing what looks ok)
|
||||
pub const DC_DESIRED_TEXT_LINES: usize = 38;
|
||||
// how many chars desktop can display per line (from "subjective" testing)
|
||||
pub const DC_DESIRED_TEXT_LINE_LEN: usize = 100;
|
||||
|
||||
/// Message length limit.
|
||||
///
|
||||
@@ -181,7 +176,7 @@ pub const DC_DESIRED_TEXT_LINE_LEN: usize = 100;
|
||||
///
|
||||
/// Note that for simplicity maximum length is defined as the number of Unicode Scalar Values (Rust
|
||||
/// `char`s), not Unicode Grapheme Clusters.
|
||||
pub const DC_DESIRED_TEXT_LEN: usize = DC_DESIRED_TEXT_LINE_LEN * DC_DESIRED_TEXT_LINES;
|
||||
pub const DC_DESIRED_TEXT_LEN: usize = 5000;
|
||||
|
||||
// Flags for empty server job
|
||||
|
||||
|
||||
321
src/contact.rs
321
src/contact.rs
@@ -1,20 +1,14 @@
|
||||
//! Contacts module
|
||||
|
||||
use std::cmp::Reverse;
|
||||
use std::collections::BinaryHeap;
|
||||
use std::convert::{TryFrom, TryInto};
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
use anyhow::{bail, ensure, Context as _, Result};
|
||||
use async_channel::{self as channel, Receiver, Sender};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tokio::task;
|
||||
use tokio::time::{timeout, Duration};
|
||||
|
||||
use crate::aheader::EncryptPreference;
|
||||
use crate::chat::ChatId;
|
||||
@@ -30,19 +24,14 @@ use crate::mimeparser::AvatarAction;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::tools::{duration_to_str, get_abs_path, improve_single_line_input, time, EmailAddress};
|
||||
use crate::tools::{get_abs_path, improve_single_line_input, EmailAddress};
|
||||
use crate::{chat, stock_str};
|
||||
|
||||
/// Time during which a contact is considered as seen recently.
|
||||
const SEEN_RECENTLY_SECONDS: i64 = 600;
|
||||
|
||||
/// Contact ID, including reserved IDs.
|
||||
///
|
||||
/// Some contact IDs are reserved to identify special contacts. This
|
||||
/// type can represent both the special as well as normal contacts.
|
||||
#[derive(
|
||||
Debug, Copy, Clone, Default, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
|
||||
)]
|
||||
#[derive(Debug, Copy, Clone, Default, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
pub struct ContactId(u32);
|
||||
|
||||
impl ContactId {
|
||||
@@ -229,7 +218,7 @@ pub enum Origin {
|
||||
/// set on Bob's side for contacts scanned and verified from a QR code. Only means the contact has once been established using the "securejoin" procedure in the past, getting the current key verification status requires calling contact_is_verified() !
|
||||
SecurejoinJoined = 0x0200_0000,
|
||||
|
||||
/// contact added manually by create_contact(), this should be the largest origin as otherwise the user cannot modify the names
|
||||
/// contact added mannually by create_contact(), this should be the largest origin as otherwise the user cannot modify the names
|
||||
ManuallyCreated = 0x0400_0000,
|
||||
}
|
||||
|
||||
@@ -334,11 +323,6 @@ impl Contact {
|
||||
self.last_seen
|
||||
}
|
||||
|
||||
/// Returns `true` if this contact was seen recently.
|
||||
pub fn was_seen_recently(&self) -> bool {
|
||||
time() - self.last_seen <= SEEN_RECENTLY_SECONDS
|
||||
}
|
||||
|
||||
/// Check if a contact is blocked.
|
||||
pub async fn is_blocked_load(context: &Context, id: ContactId) -> Result<bool> {
|
||||
let blocked = Self::load_from_db(context, id).await?.blocked;
|
||||
@@ -455,7 +439,7 @@ impl Contact {
|
||||
/// - "row_authname": name as authorized from a contact, set only through a From-header
|
||||
/// Depending on the origin, both, "row_name" and "row_authname" are updated from "name".
|
||||
///
|
||||
/// Returns the contact_id and a `Modifier` value indicating if a modification occurred.
|
||||
/// Returns the contact_id and a `Modifier` value indicating if a modification occured.
|
||||
pub(crate) async fn add_or_lookup(
|
||||
context: &Context,
|
||||
name: &str,
|
||||
@@ -514,9 +498,9 @@ impl Contact {
|
||||
let mut update_addr = false;
|
||||
let mut row_id = 0;
|
||||
|
||||
if let Some((id, row_name, row_addr, row_origin, row_authname)) = context
|
||||
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
.query_row(
|
||||
"SELECT id, name, addr, origin, authname \
|
||||
FROM contacts WHERE addr=? COLLATE NOCASE;",
|
||||
paramsv![addr.to_string()],
|
||||
@@ -530,7 +514,7 @@ impl Contact {
|
||||
Ok((row_id, row_name, row_addr, row_origin, row_authname))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
.await
|
||||
{
|
||||
let update_name = manual && name != row_name;
|
||||
let update_authname = !manual
|
||||
@@ -724,7 +708,7 @@ impl Contact {
|
||||
AND c.blocked=0 \
|
||||
AND (iif(c.name='',c.authname,c.name) LIKE ? OR c.addr LIKE ?) \
|
||||
AND (1=? OR LENGTH(ps.verified_key_fingerprint)!=0) \
|
||||
ORDER BY c.last_seen DESC, c.id DESC;",
|
||||
ORDER BY LOWER(iif(c.name='',c.authname,c.name)||c.addr),c.id;",
|
||||
sql::repeat_vars(self_addrs.len())
|
||||
),
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![
|
||||
@@ -776,7 +760,7 @@ impl Contact {
|
||||
AND id>?
|
||||
AND origin>=?
|
||||
AND blocked=0
|
||||
ORDER BY last_seen DESC, id DESC;",
|
||||
ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
|
||||
sql::repeat_vars(self_addrs.len())
|
||||
),
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![
|
||||
@@ -865,7 +849,7 @@ impl Contact {
|
||||
let list = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY last_seen DESC, id DESC;",
|
||||
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
|
||||
paramsv![ContactId::LAST_SPECIAL],
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| {
|
||||
@@ -904,8 +888,11 @@ impl Contact {
|
||||
EncryptPreference::Reset => stock_str::encr_none(context).await,
|
||||
};
|
||||
|
||||
let finger_prints = stock_str::finger_prints(context).await;
|
||||
ret += &format!("{}.\n{}:", stock_message, finger_prints);
|
||||
ret += &format!(
|
||||
"{}.\n{}:",
|
||||
stock_message,
|
||||
stock_str::finger_prints(context).await
|
||||
);
|
||||
|
||||
let fingerprint_self = SignedPublicKey::load_self(context)
|
||||
.await?
|
||||
@@ -944,36 +931,43 @@ impl Contact {
|
||||
Ok(ret)
|
||||
}
|
||||
|
||||
/// Delete a contact so that it disappears from the corresponding lists.
|
||||
/// Depending on whether there are ongoing chats, deletion is done by physical deletion or hiding.
|
||||
/// The contact is deleted from the local device.
|
||||
/// Delete a contact. The contact is deleted from the local device. It may happen that this is not
|
||||
/// possible as the contact is in use. In this case, the contact can be blocked.
|
||||
///
|
||||
/// May result in a `#DC_EVENT_CONTACTS_CHANGED` event.
|
||||
pub async fn delete(context: &Context, contact_id: ContactId) -> Result<()> {
|
||||
ensure!(!contact_id.is_special(), "Can not delete special contact");
|
||||
|
||||
context
|
||||
let count_chats = context
|
||||
.sql
|
||||
.transaction(move |transaction| {
|
||||
// make sure, the transaction starts with a write command and becomes EXCLUSIVE by that -
|
||||
// upgrading later may be impossible by races.
|
||||
let deleted_contacts = transaction.execute(
|
||||
"DELETE FROM contacts WHERE id=?
|
||||
AND (SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?)=0;",
|
||||
paramsv![contact_id, contact_id],
|
||||
)?;
|
||||
if deleted_contacts == 0 {
|
||||
transaction.execute(
|
||||
"UPDATE contacts SET origin=? WHERE id=?;",
|
||||
paramsv![Origin::Hidden, contact_id],
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
})
|
||||
.count(
|
||||
"SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;",
|
||||
paramsv![contact_id],
|
||||
)
|
||||
.await?;
|
||||
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
Ok(())
|
||||
if count_chats == 0 {
|
||||
match context
|
||||
.sql
|
||||
.execute("DELETE FROM contacts WHERE id=?;", paramsv![contact_id])
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
context.emit_event(EventType::ContactsChanged(None));
|
||||
return Ok(());
|
||||
}
|
||||
Err(err) => {
|
||||
error!(context, "delete_contact {} failed ({})", contact_id, err);
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!(
|
||||
context,
|
||||
"could not delete contact {}, there are {} chats with it", contact_id, count_chats
|
||||
);
|
||||
bail!("Could not delete contact with ongoing chats");
|
||||
}
|
||||
|
||||
/// Get a single contact object. For a list, see eg. get_contacts().
|
||||
@@ -1184,7 +1178,7 @@ impl Contact {
|
||||
|
||||
/// Returns false if addr is an invalid address, otherwise true.
|
||||
pub fn may_be_valid_addr(addr: &str) -> bool {
|
||||
let res = EmailAddress::new(addr);
|
||||
let res = addr.parse::<EmailAddress>();
|
||||
res.is_ok()
|
||||
}
|
||||
|
||||
@@ -1369,17 +1363,13 @@ pub(crate) async fn update_last_seen(
|
||||
"Can not update special contact last seen timestamp"
|
||||
);
|
||||
|
||||
if context
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE contacts SET last_seen = ?1 WHERE last_seen < ?1 AND id = ?2",
|
||||
paramsv![timestamp, contact_id],
|
||||
)
|
||||
.await?
|
||||
> 0
|
||||
{
|
||||
context.interrupt_recently_seen(contact_id, timestamp).await;
|
||||
}
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1439,139 +1429,13 @@ fn split_address_book(book: &str) -> Vec<(&str, &str)> {
|
||||
.chunks(2)
|
||||
.into_iter()
|
||||
.filter_map(|chunk| {
|
||||
let name = chunk.first()?;
|
||||
let name = chunk.get(0)?;
|
||||
let addr = chunk.get(1)?;
|
||||
Some((*name, *addr))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RecentlySeenInterrupt {
|
||||
contact_id: ContactId,
|
||||
timestamp: i64,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct RecentlySeenLoop {
|
||||
/// Task running "recently seen" loop.
|
||||
handle: task::JoinHandle<()>,
|
||||
|
||||
interrupt_send: Sender<RecentlySeenInterrupt>,
|
||||
}
|
||||
|
||||
impl RecentlySeenLoop {
|
||||
pub(crate) fn new(context: Context) -> Self {
|
||||
let (interrupt_send, interrupt_recv) = channel::bounded(1);
|
||||
|
||||
let handle = task::spawn(async move { Self::run(context, interrupt_recv).await });
|
||||
Self {
|
||||
handle,
|
||||
interrupt_send,
|
||||
}
|
||||
}
|
||||
|
||||
async fn run(context: Context, interrupt: Receiver<RecentlySeenInterrupt>) {
|
||||
type MyHeapElem = (Reverse<i64>, ContactId);
|
||||
|
||||
// Priority contains all recently seen sorted by the timestamp
|
||||
// when they become not recently seen.
|
||||
//
|
||||
// Initialize with contacts which are currently seen, but will
|
||||
// become unseen in the future.
|
||||
let mut unseen_queue: BinaryHeap<MyHeapElem> = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id, last_seen FROM contacts
|
||||
WHERE last_seen > ?",
|
||||
paramsv![time() - SEEN_RECENTLY_SECONDS],
|
||||
|row| {
|
||||
let contact_id: ContactId = row.get("id")?;
|
||||
let last_seen: i64 = row.get("last_seen")?;
|
||||
Ok((Reverse(last_seen + SEEN_RECENTLY_SECONDS), contact_id))
|
||||
},
|
||||
|rows| {
|
||||
rows.collect::<std::result::Result<BinaryHeap<MyHeapElem>, _>>()
|
||||
.map_err(Into::into)
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
loop {
|
||||
let now = SystemTime::now();
|
||||
|
||||
let (until, contact_id) =
|
||||
if let Some((Reverse(timestamp), contact_id)) = unseen_queue.peek() {
|
||||
(
|
||||
UNIX_EPOCH
|
||||
+ Duration::from_secs((*timestamp).try_into().unwrap_or(u64::MAX))
|
||||
+ Duration::from_secs(1),
|
||||
Some(contact_id),
|
||||
)
|
||||
} else {
|
||||
// Sleep for 24 hours.
|
||||
(now + Duration::from_secs(86400), None)
|
||||
};
|
||||
|
||||
if let Ok(duration) = until.duration_since(now) {
|
||||
info!(
|
||||
context,
|
||||
"Recently seen loop waiting for {} or interrupt",
|
||||
duration_to_str(duration)
|
||||
);
|
||||
|
||||
match timeout(duration, interrupt.recv()).await {
|
||||
Err(_) => {
|
||||
// Timeout, notify about contact.
|
||||
if let Some(contact_id) = contact_id {
|
||||
context.emit_event(EventType::ContactsChanged(Some(*contact_id)));
|
||||
unseen_queue.pop();
|
||||
}
|
||||
}
|
||||
Ok(Err(err)) => {
|
||||
warn!(
|
||||
context,
|
||||
"Error receiving an interruption in recently seen loop: {}", err
|
||||
);
|
||||
}
|
||||
Ok(Ok(RecentlySeenInterrupt {
|
||||
contact_id,
|
||||
timestamp,
|
||||
})) => {
|
||||
// Received an interrupt.
|
||||
unseen_queue.push((Reverse(timestamp + SEEN_RECENTLY_SECONDS), contact_id));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Recently seen loop is not waiting, event is already due."
|
||||
);
|
||||
|
||||
// Event is already in the past.
|
||||
if let Some(contact_id) = contact_id {
|
||||
context.emit_event(EventType::ContactsChanged(Some(*contact_id)));
|
||||
}
|
||||
unseen_queue.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn interrupt(&self, contact_id: ContactId, timestamp: i64) {
|
||||
self.interrupt_send
|
||||
.try_send(RecentlySeenInterrupt {
|
||||
contact_id,
|
||||
timestamp,
|
||||
})
|
||||
.ok();
|
||||
}
|
||||
|
||||
pub(crate) fn abort(self) {
|
||||
self.handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -1580,7 +1444,7 @@ mod tests {
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::message::Message;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{self, TestContext, TestContextManager};
|
||||
use crate::test_utils::{self, TestContext};
|
||||
|
||||
#[test]
|
||||
fn test_contact_id_values() {
|
||||
@@ -1731,7 +1595,7 @@ mod tests {
|
||||
);
|
||||
assert_eq!(Contact::add_address_book(&t, book).await.unwrap(), 4);
|
||||
|
||||
// check first added contact, this modifies authname because it is empty
|
||||
// check first added contact, this modifies authname beacuse it is empty
|
||||
let (contact_id, sth_modified) =
|
||||
Contact::add_or_lookup(&t, "bla foo", "one@eins.org", Origin::IncomingUnknownTo)
|
||||
.await
|
||||
@@ -1939,70 +1803,21 @@ mod tests {
|
||||
// Create Bob contact
|
||||
let (contact_id, _) =
|
||||
Contact::add_or_lookup(&alice, "Bob", "bob@example.net", Origin::ManuallyCreated)
|
||||
.await?;
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let chat = alice
|
||||
.create_chat_with_contact("Bob", "bob@example.net")
|
||||
.await;
|
||||
assert_eq!(
|
||||
Contact::get_all(&alice, 0, Some("bob@example.net"))
|
||||
.await?
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
|
||||
// If a contact has ongoing chats, contact is only hidden on deletion
|
||||
Contact::delete(&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"))
|
||||
.await?
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
// Can't delete a contact with ongoing chats.
|
||||
assert!(Contact::delete(&alice, contact_id).await.is_err());
|
||||
|
||||
// Delete chat.
|
||||
chat.get_id().delete(&alice).await?;
|
||||
|
||||
// Can delete contact physically now
|
||||
// Can delete contact now.
|
||||
Contact::delete(&alice, contact_id).await?;
|
||||
assert!(Contact::load_from_db(&alice, contact_id).await.is_err());
|
||||
assert_eq!(
|
||||
Contact::get_all(&alice, 0, Some("bob@example.net"))
|
||||
.await?
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_delete_and_recreate_contact() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
|
||||
// test recreation after physical deletion
|
||||
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::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);
|
||||
assert_eq!(Contact::get_all(&t, 0, Some("foo@bar.de")).await?.len(), 1);
|
||||
|
||||
// test recreation after hiding
|
||||
t.create_chat_with_contact("Foo", "foo@bar.de").await;
|
||||
Contact::delete(&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::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);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -2455,28 +2270,4 @@ Hi."#;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_was_seen_recently() -> Result<()> {
|
||||
let mut tcm = TestContextManager::new();
|
||||
let alice = tcm.alice().await;
|
||||
let bob = tcm.bob().await;
|
||||
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let sent_msg = alice.send_text(chat.id, "moin").await;
|
||||
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let contacts = chat::get_chat_contacts(&bob, chat.id).await?;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(!contact.was_seen_recently());
|
||||
|
||||
bob.recv_msg(&sent_msg).await;
|
||||
let contact = Contact::get_by_id(&bob, *contacts.first().unwrap()).await?;
|
||||
assert!(contact.was_seen_recently());
|
||||
|
||||
let self_contact = Contact::get_by_id(&bob, ContactId::SELF).await?;
|
||||
assert!(!self_contact.was_seen_recently());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
282
src/context.rs
282
src/context.rs
@@ -23,159 +23,8 @@ use crate::quota::QuotaInfo;
|
||||
use crate::ratelimit::Ratelimit;
|
||||
use crate::scheduler::Scheduler;
|
||||
use crate::sql::Sql;
|
||||
use crate::stock_str::StockStrings;
|
||||
use crate::tools::{duration_to_str, time};
|
||||
|
||||
/// Builder for the [`Context`].
|
||||
///
|
||||
/// Many arguments to the [`Context`] are kind of optional and only needed to handle
|
||||
/// multiple contexts, for which the [account manager](crate::accounts::Accounts) should be
|
||||
/// used. This builder makes creating a new context simpler, especially for the
|
||||
/// standalone-context case.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Creating a new unecrypted database:
|
||||
///
|
||||
/// ```
|
||||
/// # let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
/// # rt.block_on(async move {
|
||||
/// use deltachat::context::ContextBuilder;
|
||||
///
|
||||
/// let dir = tempfile::tempdir().unwrap();
|
||||
/// let context = ContextBuilder::new(dir.path().join("db"))
|
||||
/// .open()
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// drop(context);
|
||||
/// # });
|
||||
/// ```
|
||||
///
|
||||
/// To use an encrypted database provide a password. If the database does not yet exist it
|
||||
/// will be created:
|
||||
///
|
||||
/// ```
|
||||
/// # let rt = tokio::runtime::Runtime::new().unwrap();
|
||||
/// # rt.block_on(async move {
|
||||
/// use deltachat::context::ContextBuilder;
|
||||
///
|
||||
/// let dir = tempfile::tempdir().unwrap();
|
||||
/// let context = ContextBuilder::new(dir.path().join("db"))
|
||||
/// .with_password("secret".into())
|
||||
/// .open()
|
||||
/// .await
|
||||
/// .unwrap();
|
||||
/// drop(context);
|
||||
/// # });
|
||||
/// ```
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct ContextBuilder {
|
||||
dbfile: PathBuf,
|
||||
id: u32,
|
||||
events: Events,
|
||||
stock_strings: StockStrings,
|
||||
password: Option<String>,
|
||||
}
|
||||
|
||||
impl ContextBuilder {
|
||||
/// Create the builder using the given database file.
|
||||
///
|
||||
/// The *dbfile* should be in a dedicated directory and this directory must exist. The
|
||||
/// [`Context`] will create other files and folders in the same directory as the
|
||||
/// database file used.
|
||||
pub fn new(dbfile: PathBuf) -> Self {
|
||||
ContextBuilder {
|
||||
dbfile,
|
||||
id: rand::random(),
|
||||
events: Events::new(),
|
||||
stock_strings: StockStrings::new(),
|
||||
password: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the context ID.
|
||||
///
|
||||
/// This identifier is used e.g. in [`Event`]s to identify which [`Context`] an event
|
||||
/// belongs to. The only real limit on it is that it should not conflict with any other
|
||||
/// [`Context`]s you currently have open. So if you handle multiple [`Context`]s you
|
||||
/// may want to use this.
|
||||
///
|
||||
/// Note that the [account manager](crate::accounts::Accounts) is designed to handle the
|
||||
/// common case for using multiple [`Context`] instances.
|
||||
pub fn with_id(mut self, id: u32) -> Self {
|
||||
self.id = id;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the event channel for this [`Context`].
|
||||
///
|
||||
/// Mostly useful when using multiple [`Context`]s, this allows creating one [`Events`]
|
||||
/// channel and passing it to all [`Context`]s so all events are recieved on the same
|
||||
/// channel.
|
||||
///
|
||||
/// Note that the [account manager](crate::accounts::Accounts) is designed to handle the
|
||||
/// common case for using multiple [`Context`] instances.
|
||||
pub fn with_events(mut self, events: Events) -> Self {
|
||||
self.events = events;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the [`StockStrings`] map to use for this [`Context`].
|
||||
///
|
||||
/// This is useful in order to share the same translation strings in all [`Context`]s.
|
||||
/// The mapping may be empty when set, it will be populated by
|
||||
/// [`Context::set_stock-translation`] or [`Accounts::set_stock_translation`] calls.
|
||||
///
|
||||
/// Note that the [account manager](crate::accounts::Accounts) is designed to handle the
|
||||
/// common case for using multiple [`Context`] instances.
|
||||
///
|
||||
/// [`Accounts::set_stock_translation`]: crate::accounts::Accounts::set_stock_translation
|
||||
pub fn with_stock_strings(mut self, stock_strings: StockStrings) -> Self {
|
||||
self.stock_strings = stock_strings;
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the password to unlock the database.
|
||||
///
|
||||
/// If an encrypted database is used it must be opened with a password. Setting a
|
||||
/// password on a new database will enable encryption.
|
||||
pub fn with_password(mut self, password: String) -> Self {
|
||||
self.password = Some(password);
|
||||
self
|
||||
}
|
||||
|
||||
/// Opens the [`Context`].
|
||||
pub async fn open(self) -> Result<Context, ContextError> {
|
||||
let context =
|
||||
Context::new_closed(&self.dbfile, self.id, self.events, self.stock_strings).await?;
|
||||
let password = self.password.unwrap_or_default();
|
||||
match context.open(password).await? {
|
||||
true => Ok(context),
|
||||
false => Err(ContextError::DatabaseEncrypted),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum ContextError {
|
||||
#[error("database could not be decrypted, incorrect or missing password")]
|
||||
DatabaseEncrypted,
|
||||
#[error("failed to open context")]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
|
||||
/// The context for a single DeltaChat account.
|
||||
///
|
||||
/// This contains all the state for a single DeltaChat account, including background tasks
|
||||
/// running in Tokio to operate the account. The [`Context`] can be cheaply cloned.
|
||||
///
|
||||
/// Each context, and thus each account, must be associated with an directory where all the
|
||||
/// state is kept. This state is also preserved between restarts.
|
||||
///
|
||||
/// To use multiple accounts it is best to look at the [accounts
|
||||
/// manager][crate::accounts::Accounts] which handles storing multiple accounts in a single
|
||||
/// directory structure and handles loading them all concurrently.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Context {
|
||||
pub(crate) inner: Arc<InnerContext>,
|
||||
@@ -202,7 +51,7 @@ pub struct InnerContext {
|
||||
pub(crate) oauth2_mutex: Mutex<()>,
|
||||
/// Mutex to prevent a race condition when a "your pw is wrong" warning is sent, resulting in multiple messeges being sent.
|
||||
pub(crate) wrong_pw_warning_mutex: Mutex<()>,
|
||||
pub(crate) translated_stockstrings: StockStrings,
|
||||
pub(crate) translated_stockstrings: RwLock<HashMap<usize, String>>,
|
||||
pub(crate) events: Events,
|
||||
|
||||
pub(crate) scheduler: RwLock<Option<Scheduler>>,
|
||||
@@ -270,13 +119,8 @@ pub fn get_info() -> BTreeMap<&'static str, String> {
|
||||
|
||||
impl Context {
|
||||
/// Creates new context and opens the database.
|
||||
pub async fn new(
|
||||
dbfile: &Path,
|
||||
id: u32,
|
||||
events: Events,
|
||||
stock_strings: StockStrings,
|
||||
) -> Result<Context> {
|
||||
let context = Self::new_closed(dbfile, id, events, stock_strings).await?;
|
||||
pub async fn new(dbfile: &Path, id: u32, events: Events) -> Result<Context> {
|
||||
let context = Self::new_closed(dbfile, id, events).await?;
|
||||
|
||||
// Open the database if is not encrypted.
|
||||
if context.check_passphrase("".to_string()).await? {
|
||||
@@ -286,12 +130,7 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Creates new context without opening the database.
|
||||
pub async fn new_closed(
|
||||
dbfile: &Path,
|
||||
id: u32,
|
||||
events: Events,
|
||||
stockstrings: StockStrings,
|
||||
) -> Result<Context> {
|
||||
pub async fn new_closed(dbfile: &Path, id: u32, events: Events) -> Result<Context> {
|
||||
let mut blob_fname = OsString::new();
|
||||
blob_fname.push(dbfile.file_name().unwrap_or_default());
|
||||
blob_fname.push("-blobs");
|
||||
@@ -299,7 +138,7 @@ impl Context {
|
||||
if !blobdir.exists() {
|
||||
tokio::fs::create_dir_all(&blobdir).await?;
|
||||
}
|
||||
let context = Context::with_blobdir(dbfile.into(), blobdir, id, events, stockstrings)?;
|
||||
let context = Context::with_blobdir(dbfile.into(), blobdir, id, events).await?;
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
@@ -330,12 +169,11 @@ impl Context {
|
||||
self.sql.check_passphrase(passphrase).await
|
||||
}
|
||||
|
||||
pub(crate) fn with_blobdir(
|
||||
pub(crate) async fn with_blobdir(
|
||||
dbfile: PathBuf,
|
||||
blobdir: PathBuf,
|
||||
id: u32,
|
||||
events: Events,
|
||||
stockstrings: StockStrings,
|
||||
) -> Result<Context> {
|
||||
ensure!(
|
||||
blobdir.is_dir(),
|
||||
@@ -352,10 +190,10 @@ impl Context {
|
||||
generating_key_mutex: Mutex::new(()),
|
||||
oauth2_mutex: Mutex::new(()),
|
||||
wrong_pw_warning_mutex: Mutex::new(()),
|
||||
translated_stockstrings: stockstrings,
|
||||
translated_stockstrings: RwLock::new(HashMap::new()),
|
||||
events,
|
||||
scheduler: RwLock::new(None),
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds.
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 3.0)), // Allow to send 3 messages immediately, no more than once every 20 seconds.
|
||||
quota: RwLock::new(None),
|
||||
server_id: RwLock::new(None),
|
||||
creation_time: std::time::SystemTime::now(),
|
||||
@@ -401,17 +239,6 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
/// Restarts the IO scheduler if it was running before
|
||||
/// when it is not running this is an no-op
|
||||
pub async fn restart_io_if_running(&self) {
|
||||
info!(self, "restarting IO");
|
||||
let is_running = { self.inner.scheduler.read().await.is_some() };
|
||||
if is_running {
|
||||
self.stop_io().await;
|
||||
self.start_io().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the underlying SQL instance.
|
||||
///
|
||||
/// Warning: this is only here for testing, not part of the public API.
|
||||
@@ -508,7 +335,6 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) async fn shall_stop_ongoing(&self) -> bool {
|
||||
match &*self.running_state.read().await {
|
||||
RunningState::Running { .. } => false,
|
||||
@@ -570,10 +396,6 @@ impl Context {
|
||||
.await?
|
||||
.unwrap_or_default();
|
||||
|
||||
let configured_inbox_folder = self
|
||||
.get_config(Config::ConfiguredInboxFolder)
|
||||
.await?
|
||||
.unwrap_or_else(|| "<unset>".to_string());
|
||||
let configured_sentbox_folder = self
|
||||
.get_config(Config::ConfiguredSentboxFolder)
|
||||
.await?
|
||||
@@ -645,7 +467,6 @@ impl Context {
|
||||
res.insert("mvbox_move", mvbox_move.to_string());
|
||||
res.insert("only_fetch_mvbox", only_fetch_mvbox.to_string());
|
||||
res.insert("folders_configured", folders_configured.to_string());
|
||||
res.insert("configured_inbox_folder", configured_inbox_folder);
|
||||
res.insert("configured_sentbox_folder", configured_sentbox_folder);
|
||||
res.insert("configured_mvbox_folder", configured_mvbox_folder);
|
||||
res.insert("mdns_enabled", mdns_enabled.to_string());
|
||||
@@ -699,12 +520,6 @@ impl Context {
|
||||
.await?
|
||||
.to_string(),
|
||||
);
|
||||
res.insert(
|
||||
"authserv_id_candidates",
|
||||
self.get_config(Config::AuthservIdCandidates)
|
||||
.await?
|
||||
.unwrap_or_default(),
|
||||
);
|
||||
|
||||
let elapsed = self.creation_time.elapsed();
|
||||
res.insert("uptime", duration_to_str(elapsed.unwrap_or_default()));
|
||||
@@ -779,7 +594,7 @@ impl Context {
|
||||
|
||||
let list = if let Some(chat_id) = chat_id {
|
||||
do_query(
|
||||
"SELECT m.id AS id
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
@@ -803,7 +618,7 @@ impl Context {
|
||||
// According to some tests, this limit speeds up eg. 2 character searches by factor 10.
|
||||
// The limit is documented and UI may add a hint when getting 1000 results.
|
||||
do_query(
|
||||
"SELECT m.id AS id
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
@@ -811,7 +626,7 @@ impl Context {
|
||||
ON m.chat_id=c.id
|
||||
WHERE m.chat_id>9
|
||||
AND m.hidden=0
|
||||
AND c.blocked!=1
|
||||
AND c.blocked=0
|
||||
AND ct.blocked=0
|
||||
AND m.txt LIKE ?
|
||||
ORDER BY m.id DESC LIMIT 1000",
|
||||
@@ -864,8 +679,6 @@ mod tests {
|
||||
use crate::chat::{
|
||||
get_chat_contacts, get_chat_msgs, send_msg, set_muted, Chat, ChatId, MuteDuration,
|
||||
};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::ContactId;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::receive_imf::receive_imf;
|
||||
@@ -881,7 +694,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir()?;
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
tokio::fs::write(&dbfile, b"123").await?;
|
||||
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await?;
|
||||
let res = Context::new(&dbfile, 1, Events::new()).await?;
|
||||
|
||||
// Broken database is indistinguishable from encrypted one.
|
||||
assert_eq!(res.is_open().await, false);
|
||||
@@ -1027,9 +840,7 @@ mod tests {
|
||||
async fn test_blobdir_exists() {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
Context::new(&dbfile, 1, Events::new()).await.unwrap();
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
assert!(blobdir.is_dir());
|
||||
}
|
||||
@@ -1040,7 +851,7 @@ mod tests {
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
tokio::fs::write(&blobdir, b"123").await.unwrap();
|
||||
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await;
|
||||
let res = Context::new(&dbfile, 1, Events::new()).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -1050,9 +861,7 @@ mod tests {
|
||||
let subdir = tmp.path().join("subdir");
|
||||
let dbfile = subdir.join("db.sqlite");
|
||||
let dbfile2 = dbfile.clone();
|
||||
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
Context::new(&dbfile, 1, Events::new()).await.unwrap();
|
||||
assert!(subdir.is_dir());
|
||||
assert!(dbfile2.is_file());
|
||||
}
|
||||
@@ -1062,7 +871,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = PathBuf::new();
|
||||
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new(), StockStrings::new());
|
||||
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new()).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -1071,7 +880,7 @@ mod tests {
|
||||
let tmp = tempfile::tempdir().unwrap();
|
||||
let dbfile = tmp.path().join("db.sqlite");
|
||||
let blobdir = tmp.path().join("blobs");
|
||||
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new(), StockStrings::new());
|
||||
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new()).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -1201,59 +1010,6 @@ mod tests {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_search_unaccepted_requests() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
receive_imf(
|
||||
&t,
|
||||
b"From: BobBar <bob@example.org>\n\
|
||||
To: alice@example.org\n\
|
||||
Subject: foo\n\
|
||||
Message-ID: <msg1234@example.org>\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Date: Tue, 25 Oct 2022 13:37:00 +0000\n\
|
||||
\n\
|
||||
hello bob, foobar test!\n",
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let chat_id = t.get_last_msg().await.get_chat_id();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert_eq!(chat.get_type(), Chattype::Single);
|
||||
assert!(chat.is_contact_request());
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
|
||||
|
||||
chat_id.block(&t).await?;
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 0);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
0
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 0);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 0);
|
||||
|
||||
let contact_ids = get_chat_contacts(&t, chat_id).await?;
|
||||
Contact::unblock(&t, *contact_ids.first().unwrap()).await?;
|
||||
|
||||
assert_eq!(Chatlist::try_load(&t, 0, None, None).await?.len(), 1);
|
||||
assert_eq!(
|
||||
Chatlist::try_load(&t, 0, Some("BobBar"), None).await?.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(t.search_msgs(None, "foobar").await?.len(), 1);
|
||||
assert_eq!(t.search_msgs(Some(chat_id), "foobar").await?.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_limit_search_msgs() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
@@ -1293,7 +1049,7 @@ mod tests {
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
|
||||
let id = 1;
|
||||
let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new())
|
||||
let context = Context::new_closed(&dbfile, id, Events::new())
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
@@ -1301,7 +1057,7 @@ mod tests {
|
||||
drop(context);
|
||||
|
||||
let id = 2;
|
||||
let context = Context::new(&dbfile, id, Events::new(), StockStrings::new())
|
||||
let context = Context::new(&dbfile, id, Events::new())
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.is_open().await, false);
|
||||
|
||||
397
src/decrypt.rs
397
src/decrypt.rs
@@ -1,397 +0,0 @@
|
||||
//! End-to-end decryption support.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use mailparse::ParsedMail;
|
||||
use mailparse::SingleInfo;
|
||||
|
||||
use crate::aheader::Aheader;
|
||||
use crate::authres;
|
||||
use crate::authres::handle_authres;
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||
use crate::keyring::Keyring;
|
||||
use crate::log::LogExt;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::pgp;
|
||||
|
||||
/// Tries to decrypt a message, but only if it is structured as an
|
||||
/// Autocrypt message.
|
||||
///
|
||||
/// Returns decrypted body and a set of valid signature fingerprints
|
||||
/// if successful.
|
||||
///
|
||||
/// If the message is wrongly signed, this will still return the decrypted
|
||||
/// message but the HashSet will be empty.
|
||||
pub async fn try_decrypt(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
decryption_info: &DecryptionInfo,
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
// Possibly perform decryption
|
||||
let public_keyring_for_validate = keyring_from_peerstate(&decryption_info.peerstate);
|
||||
|
||||
let encrypted_data_part = match get_autocrypt_mime(mail)
|
||||
.or_else(|| get_mixed_up_mime(mail))
|
||||
.or_else(|| get_attachment_mime(mail))
|
||||
{
|
||||
None => {
|
||||
// not an autocrypt mime message, abort and ignore
|
||||
return Ok(None);
|
||||
}
|
||||
Some(res) => res,
|
||||
};
|
||||
info!(context, "Detected Autocrypt-mime message");
|
||||
let private_keyring: Keyring<SignedSecretKey> = Keyring::new_self(context)
|
||||
.await
|
||||
.context("failed to get own keyring")?;
|
||||
|
||||
decrypt_part(
|
||||
encrypted_data_part,
|
||||
private_keyring,
|
||||
public_keyring_for_validate,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn prepare_decryption(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
from: &[SingleInfo],
|
||||
message_time: i64,
|
||||
) -> Result<DecryptionInfo> {
|
||||
let from = if let Some(f) = from.first() {
|
||||
&f.addr
|
||||
} else {
|
||||
return Ok(DecryptionInfo::default());
|
||||
};
|
||||
|
||||
let autocrypt_header = Aheader::from_headers(from, &mail.headers)
|
||||
.ok_or_log_msg(context, "Failed to parse Autocrypt header")
|
||||
.flatten();
|
||||
|
||||
let dkim_results = handle_authres(context, mail, from, message_time).await?;
|
||||
|
||||
let peerstate = get_autocrypt_peerstate(
|
||||
context,
|
||||
from,
|
||||
autocrypt_header.as_ref(),
|
||||
message_time,
|
||||
// Disallowing keychanges is disabled for now:
|
||||
true, // dkim_results.allow_keychange,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(DecryptionInfo {
|
||||
from: from.to_string(),
|
||||
autocrypt_header,
|
||||
peerstate,
|
||||
message_time,
|
||||
dkim_results,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct DecryptionInfo {
|
||||
/// The From address. This is the address from the unnencrypted, outer
|
||||
/// From header.
|
||||
pub from: String,
|
||||
pub autocrypt_header: Option<Aheader>,
|
||||
/// The peerstate that will be used to validate the signatures
|
||||
pub peerstate: Option<Peerstate>,
|
||||
/// The timestamp when the message was sent.
|
||||
/// If this is older than the peerstate's last_seen, this probably
|
||||
/// means out-of-order message arrival, We don't modify the
|
||||
/// peerstate in this case.
|
||||
pub message_time: i64,
|
||||
pub(crate) dkim_results: authres::DkimResults,
|
||||
}
|
||||
|
||||
/// Returns a reference to the encrypted payload of a ["Mixed
|
||||
/// Up"][pgpmime-message-mangling] message.
|
||||
///
|
||||
/// According to [RFC 3156] encrypted messages should have
|
||||
/// `multipart/encrypted` MIME type and two parts, but Microsoft
|
||||
/// Exchange and ProtonMail IMAP/SMTP Bridge are known to mangle this
|
||||
/// structure by changing the type to `multipart/mixed` and prepending
|
||||
/// an empty part at the start.
|
||||
///
|
||||
/// ProtonMail IMAP/SMTP Bridge prepends a part literally saying
|
||||
/// "Empty Message", so we don't check its contents at all, checking
|
||||
/// only for `text/plain` type.
|
||||
///
|
||||
/// Returns `None` if the message is not a "Mixed Up" message.
|
||||
///
|
||||
/// [RFC 3156]: https://www.rfc-editor.org/info/rfc3156
|
||||
/// [pgpmime-message-mangling]: https://tools.ietf.org/id/draft-dkg-openpgp-pgpmime-message-mangling-00.html
|
||||
fn get_mixed_up_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
|
||||
if mail.ctype.mimetype != "multipart/mixed" {
|
||||
return None;
|
||||
}
|
||||
if let [first_part, second_part, third_part] = &mail.subparts[..] {
|
||||
if first_part.ctype.mimetype == "text/plain"
|
||||
&& second_part.ctype.mimetype == "application/pgp-encrypted"
|
||||
&& third_part.ctype.mimetype == "application/octet-stream"
|
||||
{
|
||||
Some(third_part)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the encrypted payload of a message turned into attachment.
|
||||
///
|
||||
/// Google Workspace has an option "Append footer" which appends standard footer defined
|
||||
/// by administrator to all outgoing messages. However, there is no plain text part in
|
||||
/// encrypted messages sent by Delta Chat, so Google Workspace turns the message into
|
||||
/// multipart/mixed MIME, where the first part is an empty plaintext part with a footer
|
||||
/// and the second part is the original encrypted message.
|
||||
fn get_attachment_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
|
||||
if mail.ctype.mimetype != "multipart/mixed" {
|
||||
return None;
|
||||
}
|
||||
if let [first_part, second_part] = &mail.subparts[..] {
|
||||
if first_part.ctype.mimetype == "text/plain"
|
||||
&& second_part.ctype.mimetype == "multipart/encrypted"
|
||||
{
|
||||
get_autocrypt_mime(second_part)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a reference to the encrypted payload of a valid PGP/MIME message.
|
||||
///
|
||||
/// Returns `None` if the message is not a valid PGP/MIME message.
|
||||
fn get_autocrypt_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<'b>> {
|
||||
if mail.ctype.mimetype != "multipart/encrypted" {
|
||||
return None;
|
||||
}
|
||||
if let [first_part, second_part] = &mail.subparts[..] {
|
||||
if first_part.ctype.mimetype == "application/pgp-encrypted"
|
||||
&& second_part.ctype.mimetype == "application/octet-stream"
|
||||
{
|
||||
Some(second_part)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns Ok(None) if nothing encrypted was found.
|
||||
async fn decrypt_part(
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: Keyring<SignedPublicKey>,
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
let data = mail.get_body_raw()?;
|
||||
|
||||
if has_decrypted_pgp_armor(&data) {
|
||||
let (plain, ret_valid_signatures) =
|
||||
pgp::pk_decrypt(data, private_keyring, &public_keyring_for_validate).await?;
|
||||
|
||||
// Check for detached signatures.
|
||||
// If decrypted part is a multipart/signed, then there is a detached signature.
|
||||
let decrypted_part = mailparse::parse_mail(&plain)?;
|
||||
if let Some((content, valid_detached_signatures)) =
|
||||
validate_detached_signature(&decrypted_part, &public_keyring_for_validate)?
|
||||
{
|
||||
return Ok(Some((content, valid_detached_signatures)));
|
||||
} else {
|
||||
// If the message was wrongly or not signed, still return the plain text.
|
||||
// The caller has to check if the signatures set is empty then.
|
||||
|
||||
return Ok(Some((plain, ret_valid_signatures)));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
fn has_decrypted_pgp_armor(input: &[u8]) -> bool {
|
||||
if let Some(index) = input.iter().position(|b| *b > b' ') {
|
||||
if input.len() - index > 26 {
|
||||
let start = index;
|
||||
let end = start + 27;
|
||||
|
||||
return &input[start..end] == b"-----BEGIN PGP MESSAGE-----";
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
|
||||
/// Validates signatures of Multipart/Signed message part, as defined in RFC 1847.
|
||||
///
|
||||
/// Returns `None` if the part is not a Multipart/Signed part, otherwise retruns the set of key
|
||||
/// fingerprints for which there is a valid signature.
|
||||
fn validate_detached_signature(
|
||||
mail: &ParsedMail<'_>,
|
||||
public_keyring_for_validate: &Keyring<SignedPublicKey>,
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
if mail.ctype.mimetype != "multipart/signed" {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
if let [first_part, second_part] = &mail.subparts[..] {
|
||||
// First part is the content, second part is the signature.
|
||||
let content = first_part.raw_bytes;
|
||||
let signature = second_part.get_body_raw()?;
|
||||
let ret_valid_signatures =
|
||||
pgp::pk_validate(content, &signature, public_keyring_for_validate)?;
|
||||
|
||||
Ok(Some((content.to_vec(), ret_valid_signatures)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
fn keyring_from_peerstate(peerstate: &Option<Peerstate>) -> Keyring<SignedPublicKey> {
|
||||
let mut public_keyring_for_validate: Keyring<SignedPublicKey> = Keyring::new();
|
||||
if let Some(ref peerstate) = *peerstate {
|
||||
if let Some(key) = &peerstate.public_key {
|
||||
public_keyring_for_validate.add(key.clone());
|
||||
} else if let Some(key) = &peerstate.gossip_key {
|
||||
public_keyring_for_validate.add(key.clone());
|
||||
}
|
||||
}
|
||||
public_keyring_for_validate
|
||||
}
|
||||
|
||||
/// Applies Autocrypt header to Autocrypt peer state and saves it into the database.
|
||||
///
|
||||
/// If we already know this fingerprint from another contact's peerstate, return that
|
||||
/// peerstate in order to make AEAP work, but don't save it into the db yet.
|
||||
///
|
||||
/// The param `allow_change` is used to prevent the autocrypt key from being changed
|
||||
/// if we suspect that the message may be forged and have a spoofed sender identity.
|
||||
///
|
||||
/// Returns updated peerstate.
|
||||
pub(crate) async fn get_autocrypt_peerstate(
|
||||
context: &Context,
|
||||
from: &str,
|
||||
autocrypt_header: Option<&Aheader>,
|
||||
message_time: i64,
|
||||
allow_change: bool,
|
||||
) -> Result<Option<Peerstate>> {
|
||||
let mut peerstate;
|
||||
|
||||
// Apply Autocrypt header
|
||||
if let Some(header) = autocrypt_header {
|
||||
// The "from_verified_fingerprint" part is for AEAP:
|
||||
// If we know this fingerprint from another addr,
|
||||
// we may want to do a transition from this other addr
|
||||
// (and keep its peerstate)
|
||||
// For security reasons, for now, we only do a transition
|
||||
// if the fingerprint is verified.
|
||||
peerstate = Peerstate::from_verified_fingerprint_or_addr(
|
||||
context,
|
||||
&header.public_key.fingerprint(),
|
||||
from,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
if addr_cmp(&peerstate.addr, from) {
|
||||
if allow_change {
|
||||
peerstate.apply_header(header, message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"Refusing to update existing peerstate of {}", &peerstate.addr
|
||||
);
|
||||
}
|
||||
}
|
||||
// If `peerstate.addr` and `from` differ, this means that
|
||||
// someone is using the same key but a different addr, probably
|
||||
// because they made an AEAP transition.
|
||||
// But we don't know if that's legit until we checked the
|
||||
// signatures, so wait until then with writing anything
|
||||
// to the database.
|
||||
} else {
|
||||
let p = Peerstate::from_header(header, message_time);
|
||||
p.save_to_db(&context.sql, true).await?;
|
||||
peerstate = Some(p);
|
||||
}
|
||||
} else {
|
||||
peerstate = Peerstate::from_addr(context, from).await?;
|
||||
}
|
||||
|
||||
Ok(peerstate)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::TestContext;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_has_decrypted_pgp_armor() {
|
||||
let data = b" -----BEGIN PGP MESSAGE-----";
|
||||
assert_eq!(has_decrypted_pgp_armor(data), true);
|
||||
|
||||
let data = b" \n-----BEGIN PGP MESSAGE-----";
|
||||
assert_eq!(has_decrypted_pgp_armor(data), true);
|
||||
|
||||
let data = b" -----BEGIN PGP MESSAGE---";
|
||||
assert_eq!(has_decrypted_pgp_armor(data), false);
|
||||
|
||||
let data = b" -----BEGIN PGP MESSAGE-----";
|
||||
assert_eq!(has_decrypted_pgp_armor(data), true);
|
||||
|
||||
let data = b"blas";
|
||||
assert_eq!(has_decrypted_pgp_armor(data), false);
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mixed_up_mime() -> Result<()> {
|
||||
// "Mixed Up" mail as received when sending an encrypted
|
||||
// message using Delta Chat Desktop via ProtonMail IMAP/SMTP
|
||||
// Bridge.
|
||||
let mixed_up_mime = include_bytes!("../test-data/message/protonmail-mixed-up.eml");
|
||||
let mail = mailparse::parse_mail(mixed_up_mime)?;
|
||||
assert!(get_autocrypt_mime(&mail).is_none());
|
||||
assert!(get_mixed_up_mime(&mail).is_some());
|
||||
assert!(get_attachment_mime(&mail).is_none());
|
||||
|
||||
// Same "Mixed Up" mail repaired by Thunderbird 78.9.0.
|
||||
//
|
||||
// It added `X-Enigmail-Info: Fixed broken PGP/MIME message`
|
||||
// header although the repairing is done by the built-in
|
||||
// OpenPGP support, not Enigmail.
|
||||
let repaired_mime = include_bytes!("../test-data/message/protonmail-repaired.eml");
|
||||
let mail = mailparse::parse_mail(repaired_mime)?;
|
||||
assert!(get_autocrypt_mime(&mail).is_some());
|
||||
assert!(get_mixed_up_mime(&mail).is_none());
|
||||
assert!(get_attachment_mime(&mail).is_none());
|
||||
|
||||
// Another form of "Mixed Up" mail created by Google Workspace,
|
||||
// where original message is turned into attachment to empty plaintext message.
|
||||
let attachment_mime = include_bytes!("../test-data/message/google-workspace-mixed-up.eml");
|
||||
let mail = mailparse::parse_mail(attachment_mime)?;
|
||||
assert!(get_autocrypt_mime(&mail).is_none());
|
||||
assert!(get_mixed_up_mime(&mail).is_none());
|
||||
assert!(get_attachment_mime(&mail).is_some());
|
||||
|
||||
let bob = TestContext::new_bob().await;
|
||||
receive_imf(&bob, attachment_mime, false).await?;
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert_eq!(msg.text.as_deref(), Some("Hello from Thunderbird!"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
117
src/download.rs
117
src/download.rs
@@ -256,7 +256,7 @@ impl MimeMessage {
|
||||
mod tests {
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::chat::{get_chat_msgs, send_msg};
|
||||
use crate::chat::send_msg;
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::message::Viewtype;
|
||||
use crate::receive_imf::receive_imf_inner;
|
||||
@@ -410,119 +410,4 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_status_update_expands_to_nothing() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_id = alice.create_chat(&bob).await.id;
|
||||
|
||||
let file = alice.get_blobdir().join("minimal.xdc");
|
||||
tokio::fs::write(&file, include_bytes!("../test-data/webxdc/minimal.xdc")).await?;
|
||||
let mut instance = Message::new(Viewtype::File);
|
||||
instance.set_file(file.to_str().unwrap(), None);
|
||||
let _sent1 = alice.send_msg(chat_id, &mut instance).await;
|
||||
|
||||
alice
|
||||
.send_webxdc_status_update(instance.id, r#"{"payload":7}"#, "d")
|
||||
.await?;
|
||||
alice.flush_status_updates().await?;
|
||||
let sent2 = alice.pop_sent_msg().await;
|
||||
let sent2_rfc724_mid = Message::load_from_db(&alice, sent2.sender_msg_id)
|
||||
.await?
|
||||
.rfc724_mid;
|
||||
|
||||
// not downloading the status update results in an placeholder
|
||||
receive_imf_inner(
|
||||
&bob,
|
||||
&sent2_rfc724_mid,
|
||||
sent2.payload().as_bytes(),
|
||||
false,
|
||||
Some(sent2.payload().len() as u32),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = bob.get_last_msg().await;
|
||||
let chat_id = msg.chat_id;
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id, 0).await?.len(), 1);
|
||||
assert_eq!(msg.download_state(), DownloadState::Available);
|
||||
|
||||
// downloading the status update afterwards expands to nothing and moves the placeholder to trash-chat
|
||||
// (usually status updates are too small for not being downloaded directly)
|
||||
receive_imf_inner(
|
||||
&bob,
|
||||
&sent2_rfc724_mid,
|
||||
sent2.payload().as_bytes(),
|
||||
false,
|
||||
None,
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id, 0).await?.len(), 0);
|
||||
assert!(Message::load_from_db(&bob, msg.id)
|
||||
.await?
|
||||
.chat_id
|
||||
.is_trash());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_mdn_expands_to_nothing() -> Result<()> {
|
||||
let bob = TestContext::new_bob().await;
|
||||
let raw = b"Subject: Message opened\n\
|
||||
Date: Mon, 10 Jan 2020 00:00:00 +0000\n\
|
||||
Chat-Version: 1.0\n\
|
||||
Message-ID: <bar@example.org>\n\
|
||||
To: Alice <alice@example.org>\n\
|
||||
From: Bob <bob@example.org>\n\
|
||||
Content-Type: multipart/report; report-type=disposition-notification;\n\t\
|
||||
boundary=\"kJBbU58X1xeWNHgBtTbMk80M5qnV4N\"\n\
|
||||
\n\
|
||||
\n\
|
||||
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\
|
||||
Content-Type: text/plain; charset=utf-8\n\
|
||||
\n\
|
||||
bla\n\
|
||||
\n\
|
||||
\n\
|
||||
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N\n\
|
||||
Content-Type: message/disposition-notification\n\
|
||||
\n\
|
||||
Reporting-UA: Delta Chat 1.88.0\n\
|
||||
Original-Recipient: rfc822;bob@example.org\n\
|
||||
Final-Recipient: rfc822;bob@example.org\n\
|
||||
Original-Message-ID: <foo@example.org>\n\
|
||||
Disposition: manual-action/MDN-sent-automatically; displayed\n\
|
||||
\n\
|
||||
\n\
|
||||
--kJBbU58X1xeWNHgBtTbMk80M5qnV4N--\n\
|
||||
";
|
||||
|
||||
// not downloading the mdn results in an placeholder
|
||||
receive_imf_inner(
|
||||
&bob,
|
||||
"bar@example.org",
|
||||
raw,
|
||||
false,
|
||||
Some(raw.len() as u32),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
let msg = bob.get_last_msg().await;
|
||||
let chat_id = msg.chat_id;
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id, 0).await?.len(), 1);
|
||||
assert_eq!(msg.download_state(), DownloadState::Available);
|
||||
|
||||
// downloading the mdn afterwards expands to nothing and deletes the placeholder directly
|
||||
// (usually mdn are too small for not being downloaded directly)
|
||||
receive_imf_inner(&bob, "bar@example.org", raw, false, None, false).await?;
|
||||
assert_eq!(get_chat_msgs(&bob, chat_id, 0).await?.len(), 0);
|
||||
assert!(Message::load_from_db(&bob, msg.id)
|
||||
.await?
|
||||
.chat_id
|
||||
.is_trash());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user