mirror of
https://github.com/chatmail/core.git
synced 2026-04-15 20:46:30 +03:00
Compare commits
216 Commits
jsonrpc-ap
...
py-1.97.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
54a157a629 | ||
|
|
427adefb42 | ||
|
|
f0dede26a3 | ||
|
|
36f85a6a5a | ||
|
|
137567554d | ||
|
|
72941e51fc | ||
|
|
836c016f97 | ||
|
|
e8ea9b7127 | ||
|
|
f80c78536f | ||
|
|
7877187894 | ||
|
|
a384a57979 | ||
|
|
86e1476dee | ||
|
|
81d0ecd8f6 | ||
|
|
6f329c9e96 | ||
|
|
0e2445c7a0 | ||
|
|
7ed947f598 | ||
|
|
b85f6ea6c3 | ||
|
|
045472deac | ||
|
|
71cad4df58 | ||
|
|
48786522c8 | ||
|
|
0827f1b2f6 | ||
|
|
5973bb8610 | ||
|
|
00ca5132b4 | ||
|
|
191d203b24 | ||
|
|
06f96011d8 | ||
|
|
f2e292c702 | ||
|
|
832deb8e97 | ||
|
|
74a3c57222 | ||
|
|
4a19092db0 | ||
|
|
5c652d913a | ||
|
|
053213f50e | ||
|
|
342e946f49 | ||
|
|
d15ab1355b | ||
|
|
e47f4b8f80 | ||
|
|
637a4eb351 | ||
|
|
a233aeec4a | ||
|
|
d24104154f | ||
|
|
7fa2706d0e | ||
|
|
2a5365d46d | ||
|
|
72cc853420 | ||
|
|
eea111df6f | ||
|
|
130bea9e25 | ||
|
|
754242439a | ||
|
|
3fab9e4cec | ||
|
|
8b6290120e | ||
|
|
564aef2a2a | ||
|
|
5958324550 | ||
|
|
683fc1f081 | ||
|
|
9277b46620 | ||
|
|
261926222b | ||
|
|
35cfefd934 | ||
|
|
aa13374523 | ||
|
|
fd1cd39c7c | ||
|
|
185d0bf3a3 | ||
|
|
837f3ce04c | ||
|
|
c661619263 | ||
|
|
b2f7a7bb2e | ||
|
|
1965866813 | ||
|
|
110f56777d | ||
|
|
37c6001b6c | ||
|
|
9a9c91e591 | ||
|
|
17276179e7 | ||
|
|
335d780f3e | ||
|
|
450d113993 | ||
|
|
cd6d181bbc | ||
|
|
c92c6a24a0 | ||
|
|
ffe7216194 | ||
|
|
474eb7cbc8 | ||
|
|
252b528f40 | ||
|
|
a4357712bf | ||
|
|
62afd3d4c3 | ||
|
|
569628a202 | ||
|
|
c75bc66560 | ||
|
|
ad8d3e2444 | ||
|
|
56cd875fbd | ||
|
|
06873ed04f | ||
|
|
5b518e588f | ||
|
|
d3f2db2326 | ||
|
|
e619d9690d | ||
|
|
40dc182295 | ||
|
|
3cf1aad551 | ||
|
|
7cff681234 | ||
|
|
54b10106bd | ||
|
|
484aa54ed6 | ||
|
|
417bddfa16 | ||
|
|
1e4e799f2e | ||
|
|
33b18e3014 | ||
|
|
96ce8eb851 | ||
|
|
8de5e964e0 | ||
|
|
43150195d4 | ||
|
|
065c7af9a0 | ||
|
|
949370ad63 | ||
|
|
4b91a88bc9 | ||
|
|
d04a0c8f2f | ||
|
|
6f6618d46f | ||
|
|
90eb39439a | ||
|
|
9813265ef4 | ||
|
|
8acbf9babf | ||
|
|
97d7c20549 | ||
|
|
4eddd7f616 | ||
|
|
217217bd2b | ||
|
|
37eeb55a5b | ||
|
|
d32da8e4d5 | ||
|
|
da3052fdc2 | ||
|
|
97982ef93a | ||
|
|
25bff21edd | ||
|
|
b2b22c8b85 | ||
|
|
5663b1c539 | ||
|
|
c68a4543db | ||
|
|
88dc8a389a | ||
|
|
e7b6c689ce | ||
|
|
6ec9b0a0b0 | ||
|
|
682ec563c8 | ||
|
|
8eb4d9bf08 | ||
|
|
977a8cf33f | ||
|
|
db496b82fb | ||
|
|
9d578884f9 | ||
|
|
84ae27744a | ||
|
|
54df44d930 | ||
|
|
f79b16c244 | ||
|
|
e7957b1661 | ||
|
|
7d97ab2731 | ||
|
|
8be5ca6c30 | ||
|
|
fdf91b772e | ||
|
|
d297aa70bf | ||
|
|
4877f56752 | ||
|
|
839021f9eb | ||
|
|
8533057881 | ||
|
|
6aa5c60963 | ||
|
|
f548b248eb | ||
|
|
5e06568ac6 | ||
|
|
9707f13a42 | ||
|
|
82d36e604c | ||
|
|
c9fc353b60 | ||
|
|
8b2ece63ab | ||
|
|
14045a6162 | ||
|
|
c0d1c97490 | ||
|
|
50e53f2c82 | ||
|
|
95a9f1e2f8 | ||
|
|
355f18184c | ||
|
|
120a96cd8b | ||
|
|
64b534fc61 | ||
|
|
0887acf1bf | ||
|
|
142c02b425 | ||
|
|
3f6fbdbd21 | ||
|
|
25ea739e4d | ||
|
|
df1e95ef18 | ||
|
|
5f97cf9de1 | ||
|
|
cb003cb21d | ||
|
|
3ce636fa9a | ||
|
|
39b5c5b946 | ||
|
|
29a4dd20dd | ||
|
|
f38df31509 | ||
|
|
4fc9257f65 | ||
|
|
31a2f8c878 | ||
|
|
544df73b01 | ||
|
|
f49dce4f95 | ||
|
|
a54ef74c3f | ||
|
|
750dcb500e | ||
|
|
15e598b4a3 | ||
|
|
25d255f3a9 | ||
|
|
75960e8782 | ||
|
|
ab52f8c55d | ||
|
|
88be304b5b | ||
|
|
40fb02a00f | ||
|
|
5a4b12c914 | ||
|
|
bf5edfa3b3 | ||
|
|
64dd2f4af6 | ||
|
|
52736f2b36 | ||
|
|
64515786be | ||
|
|
52a8ec48b7 | ||
|
|
d72cf3fb43 | ||
|
|
5920c5c136 | ||
|
|
a60da6deac | ||
|
|
aef19cb0e0 | ||
|
|
cddd38cdff | ||
|
|
9d5bce9b7e | ||
|
|
9f2100deee | ||
|
|
5f779ca9b2 | ||
|
|
9926804f1b | ||
|
|
294d8862e4 | ||
|
|
d09be1f7e3 | ||
|
|
ed5fc820c2 | ||
|
|
248d9600c5 | ||
|
|
cfadf20d08 | ||
|
|
32eb016ee7 | ||
|
|
2e009d1327 | ||
|
|
797b9fb087 | ||
|
|
e5c255e011 | ||
|
|
39ae44dbf0 | ||
|
|
c9a1ebf257 | ||
|
|
f5c5429fe8 | ||
|
|
8c70393c90 | ||
|
|
37cb16b95c | ||
|
|
fe420ac559 | ||
|
|
50e1866572 | ||
|
|
88402288f9 | ||
|
|
68a15725d2 | ||
|
|
64b4d421c7 | ||
|
|
dde5223929 | ||
|
|
6ae278f735 | ||
|
|
0b2c3ee163 | ||
|
|
62a0ce29e9 | ||
|
|
91b345abfe | ||
|
|
2f3f5a34b4 | ||
|
|
d9003f344e | ||
|
|
6b9aac5234 | ||
|
|
02a3c5d308 | ||
|
|
50c398c2cc | ||
|
|
4a6a08578f | ||
|
|
4f2c68e5a4 | ||
|
|
a797961a25 | ||
|
|
e149cd7afe | ||
|
|
874d103a8d | ||
|
|
b85a369341 | ||
|
|
522040810d |
2
.github/mergeable.yml
vendored
2
.github/mergeable.yml
vendored
@@ -16,7 +16,7 @@ mergeable:
|
||||
required: ['CHANGELOG.md']
|
||||
- do: dependent
|
||||
changed:
|
||||
file: 'deltachat-ffi/**'
|
||||
file: 'deltachat-ffi/src/**'
|
||||
required: ['CHANGELOG.md']
|
||||
fail:
|
||||
- do: checks
|
||||
|
||||
7
.github/workflows/ci.yml
vendored
7
.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
|
||||
args: --workspace --tests --examples --benches --features repl -- -D warnings
|
||||
|
||||
docs:
|
||||
name: Rust doc comments
|
||||
@@ -116,6 +116,11 @@ 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
Normal file
83
.github/workflows/jsonrpc-client-npm-package.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
||||
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/"
|
||||
2
.github/workflows/node-tests.yml
vendored
2
.github/workflows/node-tests.yml
vendored
@@ -52,6 +52,7 @@ jobs:
|
||||
npm install --verbose
|
||||
|
||||
- name: Test
|
||||
timeout-minutes: 10
|
||||
if: runner.os != 'Windows'
|
||||
run: |
|
||||
cd node
|
||||
@@ -59,6 +60,7 @@ 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
|
||||
|
||||
215
CHANGELOG.md
215
CHANGELOG.md
@@ -3,27 +3,234 @@
|
||||
## Unreleased
|
||||
|
||||
### API-Changes
|
||||
- jsonrpc api over websocket server (basically a new api next to the cffi) #3463
|
||||
- jsonrpc methods in cffi #3463:
|
||||
|
||||
### Changes
|
||||
|
||||
### 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
|
||||
- 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:
|
||||
- `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
|
||||
|
||||
503
Cargo.lock
generated
503
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
20
Cargo.toml
20
Cargo.toml
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.87.0"
|
||||
version = "1.97.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
@@ -32,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 = "0.2"
|
||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
||||
escaper = "0.1"
|
||||
futures = "0.3"
|
||||
hex = "0.4.0"
|
||||
image = { version = "0.24.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||
image = { version = "0.24.4", 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"
|
||||
@@ -46,7 +46,7 @@ native-tls = "0.2"
|
||||
num_cpus = "1.13"
|
||||
num-derive = "0.3"
|
||||
num-traits = "0.2"
|
||||
once_cell = "1.12.0"
|
||||
once_cell = "1.15.0"
|
||||
percent-encoding = "2.0"
|
||||
pgp = { version = "0.8", default-features = false }
|
||||
pretty_env_logger = { version = "0.4", optional = true }
|
||||
@@ -54,10 +54,10 @@ quick-xml = "0.23"
|
||||
r2d2 = "0.8"
|
||||
r2d2_sqlite = "0.20"
|
||||
rand = "0.8"
|
||||
regex = "1.5"
|
||||
regex = "1.6"
|
||||
rusqlite = { version = "0.27", features = ["sqlcipher"] }
|
||||
rust-hsluv = "0.1"
|
||||
rustyline = { version = "9", optional = true }
|
||||
rustyline = { version = "10", optional = true }
|
||||
sanitize-filename = "0.4"
|
||||
serde_json = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@@ -74,16 +74,16 @@ fast-socks5 = "0.8"
|
||||
humansize = "1"
|
||||
qrcodegen = "1.7.0"
|
||||
tagger = "4.3.3"
|
||||
textwrap = "0.15.0"
|
||||
textwrap = "0.15.1"
|
||||
async-channel = "1.6.1"
|
||||
futures-lite = "1.12.0"
|
||||
tokio-stream = { version = "0.1.9", features = ["fs"] }
|
||||
reqwest = { version = "0.11.11", features = ["json"] }
|
||||
tokio-stream = { version = "0.1.10", features = ["fs"] }
|
||||
reqwest = { version = "0.11.12", 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.4", features = ["async_tokio"] }
|
||||
criterion = { version = "0.3.6", features = ["async_tokio"] }
|
||||
futures-lite = "1.12"
|
||||
log = "0.4"
|
||||
pretty_env_logger = "0.4"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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;
|
||||
|
||||
@@ -8,7 +9,9 @@ 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()).await.unwrap();
|
||||
let context = Context::new(&dbfile, id, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let book = (0..n)
|
||||
.map(|i| format!("Name {}\naddr{}@example.org\n", i, i))
|
||||
|
||||
@@ -5,11 +5,14 @@ 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()).await.unwrap();
|
||||
let context = Context::new(dbfile, id, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
for c in chats.iter().take(10) {
|
||||
black_box(chat::get_chat_msgs(&context, *c, 0).await.ok());
|
||||
@@ -23,7 +26,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())
|
||||
let context = Context::new(Path::new(&path), 100, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap();
|
||||
|
||||
@@ -4,6 +4,7 @@ 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) {
|
||||
@@ -16,7 +17,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())
|
||||
Context::new(Path::new(&path), 100, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap()
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ use deltachat::{
|
||||
context::Context,
|
||||
imex::{imex, ImexMode},
|
||||
receive_imf::receive_imf,
|
||||
stock_str::StockStrings,
|
||||
Events,
|
||||
};
|
||||
use tempfile::tempdir;
|
||||
@@ -41,7 +42,9 @@ 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()).await.unwrap();
|
||||
let context = Context::new(&dbfile, id, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let backup: PathBuf = std::env::current_dir()
|
||||
.unwrap()
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
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())
|
||||
let context = Context::new(dbfile.as_ref(), id, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.87.0"
|
||||
version = "1.97.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.12.0"
|
||||
once_cell = "1.15.0"
|
||||
|
||||
[features]
|
||||
default = ["vendored"]
|
||||
|
||||
@@ -22,9 +22,11 @@ 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
|
||||
*
|
||||
@@ -391,7 +393,8 @@ 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.
|
||||
* adds Auto-Submitted header to outgoing messages
|
||||
* and accepts contact requests automatically (calling dc_accept_chat() is not needed for bots).
|
||||
* - `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.
|
||||
@@ -465,10 +468,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 or DC_QR_WEBRTC_INSTANCE.
|
||||
* QR code is DC_QR_ACCOUNT, DC_QR_LOGIN 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
|
||||
* e.g. `addr` and `mail_pw` for DC_QR_ACCOUNT and DC_QR_LOGIN
|
||||
* or `webrtc_instance` for DC_QR_WEBRTC_INSTANCE.
|
||||
*
|
||||
* @memberof dc_context_t
|
||||
@@ -1263,7 +1266,7 @@ void dc_marknoticed_chat (dc_context_t* context, uint32_t ch
|
||||
|
||||
|
||||
/**
|
||||
* Returns all message IDs of the given types in a chat.
|
||||
* Returns all message IDs of the given types in a given chat or any chat.
|
||||
* Typically used to show a gallery.
|
||||
* The result must be dc_array_unref()'d
|
||||
*
|
||||
@@ -1273,7 +1276,8 @@ 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 The chat ID to get all messages with media from.
|
||||
* @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 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.
|
||||
@@ -1284,7 +1288,6 @@ 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.
|
||||
*
|
||||
@@ -1385,6 +1388,9 @@ 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.
|
||||
@@ -2168,11 +2174,10 @@ 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 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 function creates the setup message and adds it to outgoing message queue.
|
||||
* The message is sent asynchronously.
|
||||
*
|
||||
* After everything succeeded, the required setup code is returned in the following format:
|
||||
* The required setup code is returned in the following format:
|
||||
*
|
||||
* ~~~
|
||||
* 1234-1234-1234-1234-1234-1234-1234-1234-1234
|
||||
@@ -2238,8 +2243,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(),
|
||||
* dc_initiate_key_transfer() or dc_imex(). As there is always at most only
|
||||
* Typical ongoing processes are started by dc_configure()
|
||||
* 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
|
||||
@@ -2265,6 +2270,7 @@ 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.
|
||||
@@ -2292,7 +2298,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::test1=Formatted fingerprint
|
||||
* - DC_QR_FPR_WITHOUT_ADDR with dc_lot_t::text1=Formatted fingerprint
|
||||
* the scanned QR code contains a fingerprint but no e-mail address;
|
||||
* suggest the user to establish an encrypted connection first.
|
||||
*
|
||||
@@ -2305,7 +2311,8 @@ 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,
|
||||
* 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;
|
||||
* ask the user if they want to start chatting;
|
||||
* if so, call dc_create_chat_by_contact_id().
|
||||
*
|
||||
@@ -2336,6 +2343,10 @@ 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.
|
||||
@@ -2509,9 +2520,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 decending;
|
||||
* The array is sorted descending;
|
||||
* the first entry in the array is the location with the newest timestamp.
|
||||
* Note that this is only realated to the recent postion of the user
|
||||
* Note that this is only related to the recent position of the user
|
||||
* if dc_array_is_independent() returns 0.
|
||||
* The returned array must be freed using dc_array_unref().
|
||||
*
|
||||
@@ -2835,7 +2846,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_accounts_get_next_event() on the emitter.
|
||||
* and call dc_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.
|
||||
@@ -2843,13 +2854,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_accounts_event_emitter_unref() after usage.
|
||||
* Must be freed using dc_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_accounts_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts);
|
||||
dc_event_emitter_t* dc_accounts_get_event_emitter (dc_accounts_t* accounts);
|
||||
|
||||
|
||||
/**
|
||||
@@ -3256,6 +3267,19 @@ 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
|
||||
@@ -3743,6 +3767,11 @@ 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.
|
||||
@@ -4624,6 +4653,22 @@ 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.
|
||||
*
|
||||
@@ -5231,9 +5276,8 @@ 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().
|
||||
* If you are using the dc_accounts_t account manager,
|
||||
* dc_accounts_event_emitter_t must be used instead.
|
||||
* You can get an event emitter from a context using dc_get_event_emitter()
|
||||
* or dc_accounts_get_event_emitter().
|
||||
*/
|
||||
|
||||
/**
|
||||
@@ -5249,6 +5293,8 @@ 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.
|
||||
@@ -5259,39 +5305,8 @@ dc_event_t* dc_get_next_event(dc_event_emitter_t* emitter);
|
||||
*/
|
||||
void dc_event_emitter_unref(dc_event_emitter_t* emitter);
|
||||
|
||||
|
||||
/**
|
||||
* @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);
|
||||
|
||||
// Alias for backwards compatibility, use dc_event_emtitter_unref instead.
|
||||
#define dc_accounts_event_emitter_unref dc_event_emitter_unref
|
||||
|
||||
/**
|
||||
* @class dc_event_t
|
||||
@@ -5363,7 +5378,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_accounts_get_next_event().
|
||||
* @param event The event object as returned from dc_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);
|
||||
@@ -5713,7 +5728,15 @@ 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
|
||||
#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
|
||||
|
||||
|
||||
/**
|
||||
@@ -5947,28 +5970,38 @@ 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"
|
||||
@@ -6015,9 +6048,7 @@ 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
|
||||
|
||||
/// "Group image deleted."
|
||||
///
|
||||
/// Used in status messages for deleted group images.
|
||||
/// @deprecated Deprecated, this string is no longer needed.
|
||||
#define DC_STR_MSGGRPIMGDELETED 33
|
||||
|
||||
/// "End-to-end encryption preferred."
|
||||
@@ -6070,6 +6101,8 @@ 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"
|
||||
@@ -6077,6 +6110,8 @@ 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."
|
||||
@@ -6140,6 +6175,8 @@ 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."
|
||||
@@ -6147,26 +6184,36 @@ 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.
|
||||
@@ -6207,12 +6254,11 @@ void dc_event_unref(dc_event_t* event);
|
||||
|
||||
/// "Chat protection enabled."
|
||||
///
|
||||
/// Used in status messages.
|
||||
|
||||
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_ENABLED_PROTECTION and DC_STR_MSG_PROTECTION_ENABLED_BY.
|
||||
#define DC_STR_PROTECTION_ENABLED 88
|
||||
|
||||
/// "Chat protection disabled."
|
||||
///
|
||||
/// Used in status messages.
|
||||
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_DISABLED_PROTECTION and DC_STR_MSG_PROTECTION_DISABLED_BY.
|
||||
#define DC_STR_PROTECTION_DISABLED 89
|
||||
|
||||
/// "Reply"
|
||||
@@ -6234,29 +6280,37 @@ 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 (alwasy >1) the timer is set to.
|
||||
///
|
||||
/// `%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.
|
||||
#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"
|
||||
@@ -6413,6 +6467,264 @@ 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
|
||||
|
||||
/**
|
||||
* @}
|
||||
*/
|
||||
|
||||
@@ -38,6 +38,7 @@ use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||
use deltachat::key::DcKey;
|
||||
use deltachat::message::MsgId;
|
||||
use deltachat::stock_str::StockMessage;
|
||||
use deltachat::stock_str::StockStrings;
|
||||
use deltachat::webxdc::StatusUpdateSerial;
|
||||
use deltachat::*;
|
||||
use deltachat::{accounts::Accounts, log::LogExt};
|
||||
@@ -98,7 +99,12 @@ 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()))
|
||||
block_on(Context::new(
|
||||
as_path(dbfile),
|
||||
id,
|
||||
Events::new(),
|
||||
StockStrings::new(),
|
||||
))
|
||||
} else {
|
||||
eprintln!("blobdir can not be defined explicitly anymore");
|
||||
return ptr::null_mut();
|
||||
@@ -122,7 +128,12 @@ 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())) {
|
||||
match block_on(Context::new_closed(
|
||||
as_path(dbfile),
|
||||
id,
|
||||
Events::new(),
|
||||
StockStrings::new(),
|
||||
)) {
|
||||
Ok(context) => Box::into_raw(Box::new(context)),
|
||||
Err(err) => {
|
||||
eprintln!("failed to create context: {:#}", err);
|
||||
@@ -169,7 +180,7 @@ pub unsafe extern "C" fn dc_context_unref(context: *mut dc_context_t) {
|
||||
eprintln!("ignoring careless call to dc_context_unref()");
|
||||
return;
|
||||
}
|
||||
Box::from_raw(context);
|
||||
drop(Box::from_raw(context));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -463,7 +474,7 @@ pub unsafe extern "C" fn dc_event_unref(a: *mut dc_event_t) {
|
||||
return;
|
||||
}
|
||||
|
||||
Box::from_raw(a);
|
||||
drop(Box::from_raw(a));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -504,6 +515,7 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -550,6 +562,7 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -581,6 +594,7 @@ 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::SelfavatarChanged => 0,
|
||||
EventType::ChatModified(_) => 0,
|
||||
EventType::MsgsChanged { msg_id, .. }
|
||||
@@ -637,6 +651,7 @@ 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 {
|
||||
@@ -683,7 +698,7 @@ pub unsafe extern "C" fn dc_event_emitter_unref(emitter: *mut dc_event_emitter_t
|
||||
return;
|
||||
}
|
||||
|
||||
Box::from_raw(emitter);
|
||||
drop(Box::from_raw(emitter));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -1208,6 +1223,11 @@ 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));
|
||||
@@ -1216,16 +1236,10 @@ pub unsafe extern "C" fn dc_get_chat_media(
|
||||
|
||||
block_on(async move {
|
||||
Box::into_raw(Box::new(
|
||||
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(),
|
||||
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(),
|
||||
))
|
||||
})
|
||||
}
|
||||
@@ -2423,7 +2437,7 @@ pub unsafe extern "C" fn dc_array_unref(a: *mut dc_array::dc_array_t) {
|
||||
return;
|
||||
}
|
||||
|
||||
Box::from_raw(a);
|
||||
drop(Box::from_raw(a));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2604,7 +2618,7 @@ pub unsafe extern "C" fn dc_chatlist_unref(chatlist: *mut dc_chatlist_t) {
|
||||
eprintln!("ignoring careless call to dc_chatlist_unref()");
|
||||
return;
|
||||
}
|
||||
Box::from_raw(chatlist);
|
||||
drop(Box::from_raw(chatlist));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2749,7 +2763,7 @@ pub unsafe extern "C" fn dc_chat_unref(chat: *mut dc_chat_t) {
|
||||
return;
|
||||
}
|
||||
|
||||
Box::from_raw(chat);
|
||||
drop(Box::from_raw(chat));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -2782,6 +2796,16 @@ 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().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() {
|
||||
@@ -3009,7 +3033,7 @@ pub unsafe extern "C" fn dc_msg_unref(msg: *mut dc_msg_t) {
|
||||
return;
|
||||
}
|
||||
|
||||
Box::from_raw(msg);
|
||||
drop(Box::from_raw(msg));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3731,7 +3755,7 @@ pub unsafe extern "C" fn dc_contact_unref(contact: *mut dc_contact_t) {
|
||||
eprintln!("ignoring careless call to dc_contact_unref()");
|
||||
return;
|
||||
}
|
||||
Box::from_raw(contact);
|
||||
drop(Box::from_raw(contact));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -3850,6 +3874,16 @@ 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() {
|
||||
@@ -3885,7 +3919,7 @@ pub unsafe extern "C" fn dc_lot_unref(lot: *mut dc_lot_t) {
|
||||
return;
|
||||
}
|
||||
|
||||
Box::from_raw(lot);
|
||||
drop(Box::from_raw(lot));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
@@ -4177,7 +4211,8 @@ pub unsafe extern "C" fn dc_accounts_get_account(
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(async move { accounts.read().await.get_account(id).await })
|
||||
block_on(accounts.read())
|
||||
.get_account(id)
|
||||
.map(|ctx| Box::into_raw(Box::new(ctx)))
|
||||
.unwrap_or_else(std::ptr::null_mut)
|
||||
}
|
||||
@@ -4192,7 +4227,8 @@ pub unsafe extern "C" fn dc_accounts_get_selected_account(
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
block_on(async move { accounts.read().await.get_selected_account().await })
|
||||
block_on(accounts.read())
|
||||
.get_selected_account()
|
||||
.map(|ctx| Box::into_raw(Box::new(ctx)))
|
||||
.unwrap_or_else(std::ptr::null_mut)
|
||||
}
|
||||
@@ -4337,7 +4373,7 @@ pub unsafe extern "C" fn dc_accounts_get_all(accounts: *mut dc_accounts_t) -> *m
|
||||
}
|
||||
|
||||
let accounts = &*accounts;
|
||||
let list = block_on(async move { accounts.read().await.get_all().await });
|
||||
let list = block_on(accounts.read()).get_all();
|
||||
let array: dc_array_t = list.into();
|
||||
|
||||
Box::into_raw(Box::new(array))
|
||||
@@ -4397,87 +4433,77 @@ 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_accounts_event_emitter_t {
|
||||
) -> *mut dc_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(async move { accounts.read().await.get_event_emitter().await });
|
||||
let emitter = block_on(accounts.read()).get_event_emitter();
|
||||
|
||||
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::DeltaChatApiV0;
|
||||
use deltachat_jsonrpc::api::CommandApi;
|
||||
use deltachat_jsonrpc::events::event_to_json_rpc_notification;
|
||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||
|
||||
pub struct dc_jsonrpc_instance_t {
|
||||
receiver: OutReceiver,
|
||||
handle: RpcSession<DeltaChatApiV0>,
|
||||
handle: RpcSession<CommandApi>,
|
||||
event_thread: JoinHandle<Result<(), anyhow::Error>>,
|
||||
}
|
||||
|
||||
#[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 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 cmd_api =
|
||||
deltachat_jsonrpc::api::CommandApi::from_arc((*account_manager).inner.clone());
|
||||
|
||||
let (request_handle, receiver) = RpcClient::new();
|
||||
let handle = RpcSession::new(request_handle, rpc_api);
|
||||
let request_handle2 = request_handle.clone();
|
||||
let handle = RpcSession::new(request_handle, cmd_api);
|
||||
|
||||
let instance = dc_jsonrpc_instance_t { receiver, handle };
|
||||
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,
|
||||
};
|
||||
|
||||
Box::into_raw(Box::new(instance))
|
||||
}
|
||||
@@ -4488,8 +4514,8 @@ mod jsonrpc {
|
||||
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
|
||||
return;
|
||||
}
|
||||
|
||||
Box::from_raw(jsonrpc_instance);
|
||||
(*jsonrpc_instance).event_thread.abort();
|
||||
drop(Box::from_raw(jsonrpc_instance));
|
||||
}
|
||||
|
||||
#[no_mangle]
|
||||
|
||||
@@ -51,13 +51,14 @@ impl Lot {
|
||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
|
||||
Qr::Account { domain } => Some(domain),
|
||||
Qr::WebrtcInstance { domain, .. } => Some(domain),
|
||||
Qr::Addr { .. } => None,
|
||||
Qr::Addr { draft, .. } => draft.as_deref(),
|
||||
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),
|
||||
}
|
||||
@@ -79,7 +80,13 @@ impl Lot {
|
||||
Some(SummaryPrefix::Username(_username)) => Meaning::Text1Username,
|
||||
Some(SummaryPrefix::Me(_text)) => Meaning::Text1Self,
|
||||
},
|
||||
Self::Qr(_qr) => Meaning::None,
|
||||
Self::Qr(qr) => match qr {
|
||||
Qr::Addr {
|
||||
draft: Some(_draft),
|
||||
..
|
||||
} => Meaning::Text1Draft,
|
||||
_ => Meaning::None,
|
||||
},
|
||||
Self::Error(_err) => Meaning::None,
|
||||
}
|
||||
}
|
||||
@@ -102,6 +109,7 @@ impl Lot {
|
||||
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
|
||||
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
|
||||
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
|
||||
Qr::Login { .. } => LotState::QrLogin,
|
||||
},
|
||||
Self::Error(_err) => LotState::QrError,
|
||||
}
|
||||
@@ -118,13 +126,14 @@ 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(),
|
||||
}
|
||||
@@ -189,6 +198,9 @@ 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.86.0"
|
||||
version = "1.97.0"
|
||||
description = "DeltaChat JSON-RPC API"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
@@ -20,18 +20,19 @@ serde = { version = "1.0", features = ["derive"] }
|
||||
tempfile = "3.3.0"
|
||||
log = "0.4"
|
||||
async-channel = { version = "1.6.1" }
|
||||
futures = { version = "0.3.19" }
|
||||
serde_json = "1.0.75"
|
||||
futures = { version = "0.3.24" }
|
||||
serde_json = "1.0.85"
|
||||
yerpc = { version = "^0.3.1", features = ["anyhow_expose"] }
|
||||
typescript-type-def = { version = "0.5.3", features = ["json_value"] }
|
||||
tokio = { version = "1.19.2" }
|
||||
tokio = { version = "1.21.2" }
|
||||
|
||||
# optional dependencies
|
||||
axum = { version = "0.5.9", optional = true, features = ["ws"] }
|
||||
env_logger = { version = "0.9.0", optional = true }
|
||||
axum = { version = "0.5.16", optional = true, features = ["ws"] }
|
||||
env_logger = { version = "0.9.1", optional = true }
|
||||
walkdir = "2.3.2"
|
||||
|
||||
[dev-dependencies]
|
||||
tokio = { version = "1.19.2", features = ["full", "rt-multi-thread"] }
|
||||
tokio = { version = "1.21.2", features = ["full", "rt-multi-thread"] }
|
||||
|
||||
|
||||
[features]
|
||||
|
||||
@@ -4,139 +4,353 @@ use serde_json::{json, Value};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
pub fn event_to_json_rpc_notification(event: Event) -> Value {
|
||||
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();
|
||||
let id: JSONRPCEventType = event.typ.into();
|
||||
json!({
|
||||
"id": id,
|
||||
"event": id,
|
||||
"contextId": event.id,
|
||||
"field1": field1,
|
||||
"field2": field2
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
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,
|
||||
#[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,
|
||||
},
|
||||
|
||||
/// 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,
|
||||
},
|
||||
|
||||
/// 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.
|
||||
ConnectivityChanged,
|
||||
|
||||
SelfavatarChanged,
|
||||
WebxdcStatusUpdate,
|
||||
|
||||
#[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,
|
||||
},
|
||||
}
|
||||
|
||||
impl From<EventType> for EventTypeName {
|
||||
impl From<EventType> for JSONRPCEventType {
|
||||
fn from(event: EventType) -> Self {
|
||||
use EventTypeName::*;
|
||||
use JSONRPCEventType::*;
|
||||
match event {
|
||||
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::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::IncomingMsg { chat_id, msg_id } => IncomingMsg {
|
||||
chat_id: chat_id.to_u32(),
|
||||
msg_id: msg_id.to_u32(),
|
||||
},
|
||||
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::ConnectivityChanged => ConnectivityChanged,
|
||||
EventType::SelfavatarChanged => SelfavatarChanged,
|
||||
EventType::WebxdcStatusUpdate { .. } => WebxdcStatusUpdate,
|
||||
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(),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -150,7 +364,8 @@ fn generate_events_ts_types_definition() {
|
||||
root_namespace: None,
|
||||
..typescript_type_def::DefinitionFileOptions::default()
|
||||
};
|
||||
typescript_type_def::write_definition_file::<_, EventTypeName>(&mut buf, options).unwrap();
|
||||
typescript_type_def::write_definition_file::<_, JSONRPCEventType>(&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,10 +1,13 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use deltachat::chat::get_chat_contacts;
|
||||
use std::time::{Duration, SystemTime};
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
|
||||
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::Serialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
@@ -33,6 +36,8 @@ 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: String,
|
||||
}
|
||||
|
||||
impl FullChat {
|
||||
@@ -65,12 +70,25 @@ 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().to_string();
|
||||
|
||||
Ok(FullChat {
|
||||
id: chat_id,
|
||||
name: chat.name.clone(),
|
||||
is_protected: chat.is_protected(),
|
||||
profile_image, //BLOBS ?
|
||||
archived: chat.get_visibility() == deltachat::chat::ChatVisibility::Archived,
|
||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||
chat_type: chat
|
||||
.get_type()
|
||||
.to_u32()
|
||||
@@ -87,6 +105,109 @@ 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::ContactId;
|
||||
use deltachat::contact::{Contact, ContactId};
|
||||
use deltachat::{
|
||||
chat::{get_chat_contacts, ChatVisibility},
|
||||
chatlist::Chatlist,
|
||||
@@ -42,8 +42,11 @@ 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")]
|
||||
@@ -92,10 +95,20 @@ pub(crate) async fn get_chat_list_item_by_id(
|
||||
|
||||
let self_in_group = chat_contacts.contains(&ContactId::SELF);
|
||||
|
||||
let dm_chat_contact = if chat.get_type() == Chattype::Single {
|
||||
chat_contacts.get(0).map(|contact_id| contact_id.to_u32())
|
||||
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,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
(None, false)
|
||||
};
|
||||
|
||||
let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?;
|
||||
@@ -121,6 +134,8 @@ 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,6 +20,9 @@ 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 {
|
||||
@@ -45,6 +48,8 @@ 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(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
47
deltachat-jsonrpc/src/api/types/location.rs
Normal file
47
deltachat-jsonrpc/src/api/types/location.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
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,6 +1,10 @@
|
||||
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;
|
||||
@@ -9,7 +13,9 @@ use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
use super::color_int_to_hex_string;
|
||||
use super::contact::ContactObject;
|
||||
use super::webxdc::WebxdcMessageInfo;
|
||||
|
||||
#[derive(Serialize, TypeDef)]
|
||||
#[serde(rename = "Message", rename_all = "camelCase")]
|
||||
@@ -17,8 +23,9 @@ pub struct MessageObject {
|
||||
id: u32,
|
||||
chat_id: u32,
|
||||
from_id: u32,
|
||||
quoted_text: Option<String>,
|
||||
quoted_message_id: Option<u32>,
|
||||
quote: Option<MessageQuote>,
|
||||
parent_id: Option<u32>,
|
||||
|
||||
text: Option<String>,
|
||||
has_location: bool,
|
||||
has_html: bool,
|
||||
@@ -53,29 +60,91 @@ pub struct MessageObject {
|
||||
file_mime: Option<String>,
|
||||
file_bytes: u64,
|
||||
file_name: Option<String>,
|
||||
|
||||
webxdc_info: Option<WebxdcMessageInfo>,
|
||||
|
||||
download_state: DownloadState,
|
||||
}
|
||||
|
||||
#[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);
|
||||
let message = Message::load_from_db(context, msg_id).await?;
|
||||
Self::from_msg_id(context, msg_id).await
|
||||
}
|
||||
|
||||
let quoted_message_id = message
|
||||
.quoted_message(context)
|
||||
.await?
|
||||
.map(|m| m.get_id().to_u32());
|
||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||
let message = Message::load_from_db(context, msg_id).await?;
|
||||
|
||||
let sender_contact = Contact::load_from_db(context, message.get_from_id()).await?;
|
||||
let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?;
|
||||
let 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
|
||||
{
|
||||
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
|
||||
};
|
||||
|
||||
Ok(MessageObject {
|
||||
id: message_id,
|
||||
id: msg_id.to_u32(),
|
||||
chat_id: message.get_chat_id().to_u32(),
|
||||
from_id: message.get_from_id().to_u32(),
|
||||
quoted_text: message.quoted_text(),
|
||||
quoted_message_id,
|
||||
quote,
|
||||
parent_id,
|
||||
text: message.get_text(),
|
||||
has_location: message.has_location(),
|
||||
has_html: message.has_html(),
|
||||
@@ -121,6 +190,9 @@ impl MessageObject {
|
||||
file_mime: message.get_filemime(),
|
||||
file_bytes,
|
||||
file_name: message.get_filename(),
|
||||
webxdc_info,
|
||||
|
||||
download_state,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -200,3 +272,145 @@ 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)]
|
||||
#[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 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
use deltachat::qr::Qr;
|
||||
use serde::Serialize;
|
||||
use typescript_type_def::TypeDef;
|
||||
|
||||
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 webxdc;
|
||||
@@ -17,3 +22,213 @@ fn maybe_empty_string_to_option(string: String) -> Option<String> {
|
||||
Some(string)
|
||||
}
|
||||
}
|
||||
|
||||
#[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 },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,8 @@ 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 {
|
||||
@@ -47,6 +49,7 @@ impl WebxdcMessageInfo {
|
||||
document,
|
||||
summary,
|
||||
source_code_url,
|
||||
internet_access,
|
||||
} = message.get_webxdc_info(context).await?;
|
||||
|
||||
Ok(Self {
|
||||
@@ -55,6 +58,7 @@ 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,11 +1,10 @@
|
||||
pub mod api;
|
||||
pub use api::events;
|
||||
pub use api::{Accounts, DeltaChatApiV0};
|
||||
pub use yerpc;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::api::{Accounts, DeltaChatApiV0};
|
||||
use super::api::{Accounts, CommandApi};
|
||||
use async_channel::unbounded;
|
||||
use futures::StreamExt;
|
||||
use tempfile::TempDir;
|
||||
@@ -15,7 +14,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 = DeltaChatApiV0::new(accounts);
|
||||
let api = CommandApi::new(accounts);
|
||||
|
||||
let (sender, mut receiver) = unbounded::<String>();
|
||||
|
||||
@@ -56,7 +55,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 = DeltaChatApiV0::new(accounts);
|
||||
let api = CommandApi::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, DeltaChatApiV0};
|
||||
use api::{Accounts, CommandApi};
|
||||
|
||||
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 = DeltaChatApiV0::new(accounts);
|
||||
let state = CommandApi::new(accounts);
|
||||
|
||||
let app = Router::new()
|
||||
.route("/rpc/v0", get(handler))
|
||||
.route("/ws", 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<DeltaChatApiV0>) -> Response {
|
||||
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> 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().await;
|
||||
let events = api.accounts.read().await.get_event_emitter();
|
||||
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 { DeltaChat, DeltaChatEvent } from "../deltachat.js";
|
||||
import { DcEvent, DeltaChat } 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", event => {
|
||||
onIncomingEvent(event)
|
||||
})
|
||||
client.on("ALL", (accountId, event) => {
|
||||
onIncomingEvent(accountId, 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");
|
||||
@@ -73,7 +73,7 @@ async function run() {
|
||||
chatId
|
||||
);
|
||||
write($main, `<h3>${chat.name}</h3>`);
|
||||
const messageIds = await client.rpc.messageListGetMessageIds(
|
||||
const messageIds = await client.rpc.getMessageIds(
|
||||
selectedAccount,
|
||||
chatId,
|
||||
0
|
||||
@@ -88,14 +88,15 @@ async function run() {
|
||||
}
|
||||
}
|
||||
|
||||
function onIncomingEvent(event: DeltaChatEvent) {
|
||||
function onIncomingEvent(accountId: number, event: DcEvent) {
|
||||
write(
|
||||
$side,
|
||||
`
|
||||
<p class="message">
|
||||
[<strong>${event.id}</strong> on account ${event.contextId}]<br>
|
||||
<em>f1:</em> ${JSON.stringify(event.field1)}<br>
|
||||
<em>f2:</em> ${JSON.stringify(event.field2)}
|
||||
[<strong>${event.type}</strong> on account ${accountId}]<br>
|
||||
<em>f1:</em> ${JSON.stringify(
|
||||
Object.assign({}, event, { type: undefined })
|
||||
)}
|
||||
</p>`
|
||||
);
|
||||
}
|
||||
|
||||
@@ -66,6 +66,26 @@ 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.
|
||||
*/
|
||||
@@ -73,6 +93,13 @@ 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.
|
||||
*
|
||||
@@ -111,18 +138,21 @@ 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, dc_check_qr() should confirm the type of the
|
||||
* QR code is DC_QR_ACCOUNT or DC_QR_WEBRTC_INSTANCE.
|
||||
* Before this function is called, `checkQr()` should confirm the type of the
|
||||
* QR code is `account` or `webrtcInstance`.
|
||||
*
|
||||
* 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)>;
|
||||
}
|
||||
@@ -132,6 +162,11 @@ 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.
|
||||
@@ -147,6 +182,16 @@ 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
|
||||
@@ -176,6 +221,16 @@ 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>;
|
||||
@@ -201,6 +256,14 @@ export class RawClient {
|
||||
return (this._transport.request('chatlist_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 acceptChat(accountId: T.U32, chatId: T.U32): Promise<null> {
|
||||
return (this._transport.request('accept_chat', [accountId, chatId] as RPC.Params)) as Promise<null>;
|
||||
@@ -211,14 +274,322 @@ 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 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)[]>;
|
||||
}
|
||||
|
||||
|
||||
@@ -227,10 +598,81 @@ export class RawClient {
|
||||
}
|
||||
|
||||
|
||||
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 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>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch info desktop needs for creating a notification for a message
|
||||
*/
|
||||
public messageGetNotificationInfo(accountId: T.U32, messageId: T.U32): Promise<T.MessageNotificationInfo> {
|
||||
return (this._transport.request('message_get_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>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single contact options by ID.
|
||||
*/
|
||||
@@ -287,6 +729,36 @@ export class RawClient {
|
||||
return (this._transport.request('contacts_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)>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all message IDs of the given types in a chat.
|
||||
* Typically used to show a gallery.
|
||||
@@ -294,11 +766,84 @@ 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 chatGetMedia(accountId: T.U32, chatId: T.U32, messageType: T.Viewtype, orMessageType2: (T.Viewtype|null), orMessageType3: (T.Viewtype|null)): Promise<(T.U32)[]> {
|
||||
public chatGetMedia(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('chat_get_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 chatGetNeighboringMedia(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('chat_get_neighboring_media', [accountId, msgId, messageType, orMessageType2, orMessageType3] as RPC.Params)) as Promise<[(T.U32|null),(T.U32|null)]>;
|
||||
}
|
||||
|
||||
|
||||
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 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 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>;
|
||||
@@ -316,6 +861,53 @@ export class RawClient {
|
||||
return (this._transport.request('message_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>;
|
||||
}
|
||||
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
/**
|
||||
* for desktop, get stickers from stickers folder,
|
||||
* grouped by the 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)[]>>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the messageid of the sent message
|
||||
*/
|
||||
@@ -324,4 +916,14 @@ export class RawClient {
|
||||
}
|
||||
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -1,3 +1,202 @@
|
||||
// AUTO-GENERATED by typescript-type-def
|
||||
|
||||
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");
|
||||
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;})|({
|
||||
/**
|
||||
* 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;})|({
|
||||
/**
|
||||
* 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;}));
|
||||
|
||||
@@ -2,17 +2,66 @@
|
||||
|
||||
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 ChatListEntry=[U32,U32];
|
||||
export type I64=number;
|
||||
export type ChatListEntry=[U32,U32];
|
||||
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);})|{"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;};
|
||||
"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;};
|
||||
|
||||
/**
|
||||
* 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;}));
|
||||
export type Viewtype=("Unknown"|
|
||||
/**
|
||||
* Text message.
|
||||
@@ -58,9 +107,8 @@ 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 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.
|
||||
@@ -93,5 +141,23 @@ export type WebxdcMessageInfo={
|
||||
* defaults to an empty string.
|
||||
* Implementations may offer an menu or a button to open this URL.
|
||||
*/
|
||||
"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];
|
||||
"sourceCodeUrl":(string|null);
|
||||
/**
|
||||
* True if full internet access should be granted to the app.
|
||||
*/
|
||||
"internetAccess":boolean;};
|
||||
export type DownloadState=("Done"|"Available"|"Failure"|"InProgress");
|
||||
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;"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;};
|
||||
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,null,U32,U32,(Message|null),U32,U32,U32,U32,string,U32,Record<string,(string)[]>,U32,string,U32,U32,U32,U32,(string|null),(string|null),([F64,F64]|null),(U32|null),[U32,Message],U32,U32,(string|null),(string|null),(U32|null),null];
|
||||
|
||||
@@ -1,29 +1,5 @@
|
||||
{
|
||||
"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": {
|
||||
"isomorphic-ws": "^4.0.1",
|
||||
"tiny-emitter": "git+https://github.com/Simon-Laux/tiny-emitter.git",
|
||||
@@ -47,5 +23,29 @@
|
||||
"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 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 .",
|
||||
"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 --features webserver --bin deltachat-jsonrpc-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.97.0"
|
||||
}
|
||||
@@ -1,40 +1,57 @@
|
||||
import * as T from "../generated/types.js";
|
||||
import * as RPC from "../generated/jsonrpc.js";
|
||||
import { RawClient } from "../generated/client.js";
|
||||
import { EventTypeName } from "../generated/events.js";
|
||||
import { Event } from "../generated/events.js";
|
||||
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
|
||||
import { TinyEmitter } from "tiny-emitter";
|
||||
|
||||
export type DeltaChatEvent = {
|
||||
id: EventTypeName;
|
||||
type DCWireEvent<T extends Event> = {
|
||||
event: T;
|
||||
contextId: number;
|
||||
field1: any;
|
||||
field2: any;
|
||||
};
|
||||
export type Events = Record<
|
||||
EventTypeName | "ALL",
|
||||
(event: DeltaChatEvent) => void
|
||||
>;
|
||||
// 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 class BaseDeltaChat<
|
||||
Transport extends BaseTransport<any>
|
||||
> extends TinyEmitter<Events> {
|
||||
rpc: RawClient;
|
||||
account?: T.Account;
|
||||
private contextEmitters: TinyEmitter<Events>[] = [];
|
||||
private contextEmitters: TinyEmitter<ContextEvents>[] = [];
|
||||
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 DeltaChatEvent;
|
||||
this.emit(event.id, event);
|
||||
this.emit("ALL", event);
|
||||
const event = request.params! as DCWireEvent<Event>;
|
||||
this.emit(event.event.type, event.contextId, event.event as any);
|
||||
this.emit("ALL", event.contextId, event.event as any);
|
||||
|
||||
if (this.contextEmitters[event.contextId]) {
|
||||
this.contextEmitters[event.contextId].emit(event.id, event);
|
||||
this.contextEmitters[event.contextId].emit("ALL", event);
|
||||
this.contextEmitters[event.contextId].emit(
|
||||
event.event.type,
|
||||
event.event as any
|
||||
);
|
||||
this.contextEmitters[event.contextId].emit("ALL", event.event);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -70,7 +87,7 @@ 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;
|
||||
}
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
import { assert, expect } from "chai";
|
||||
import { DeltaChat, DeltaChatEvent, EventTypeName } from "../deltachat.js";
|
||||
import {
|
||||
RpcServerHandle,
|
||||
createTempUser,
|
||||
startServer,
|
||||
} from "./test_base.js";
|
||||
import { DeltaChat, DcEvent } 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;
|
||||
@@ -16,7 +12,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(
|
||||
@@ -31,10 +27,10 @@ describe("online tests", function () {
|
||||
this.skip();
|
||||
}
|
||||
serverHandle = await startServer();
|
||||
dc = new DeltaChat(serverHandle.url)
|
||||
dc = new DeltaChat(serverHandle.url);
|
||||
|
||||
dc.on("ALL", ({ id, contextId }) => {
|
||||
if (id !== "Info") console.log(contextId, id);
|
||||
dc.on("ALL", (contextId, { type }) => {
|
||||
if (type !== "Info") console.log(contextId, type);
|
||||
});
|
||||
|
||||
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
|
||||
@@ -74,7 +70,7 @@ describe("online tests", function () {
|
||||
addr: account2.email,
|
||||
mail_pw: account2.password,
|
||||
});
|
||||
await dc.rpc.configure(accountId2)
|
||||
await dc.rpc.configure(accountId2);
|
||||
accountsConfigured = true;
|
||||
});
|
||||
|
||||
@@ -89,16 +85,19 @@ describe("online tests", function () {
|
||||
account2.email,
|
||||
null
|
||||
);
|
||||
const chatId = await dc.rpc.contactsCreateChatByContactId(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, "Hello", chatId);
|
||||
const { field1: chatIdOnAccountB } = await eventPromise;
|
||||
const { chatId: chatIdOnAccountB } = await eventPromise;
|
||||
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
|
||||
const messageList = await dc.rpc.messageListGetMessageIds(
|
||||
const messageList = await dc.rpc.getMessageIds(
|
||||
accountId2,
|
||||
chatIdOnAccountB,
|
||||
0
|
||||
@@ -121,7 +120,10 @@ describe("online tests", function () {
|
||||
account2.email,
|
||||
null
|
||||
);
|
||||
const chatId = await dc.rpc.contactsCreateChatByContactId(accountId1, contactId);
|
||||
const chatId = await dc.rpc.contactsCreateChatByContactId(
|
||||
accountId1,
|
||||
contactId
|
||||
);
|
||||
const eventPromise = Promise.race([
|
||||
waitForEvent(dc, "MsgsChanged", accountId2),
|
||||
waitForEvent(dc, "IncomingMsg", accountId2),
|
||||
@@ -131,10 +133,10 @@ describe("online tests", function () {
|
||||
console.log("wait for message from A");
|
||||
|
||||
const event = await eventPromise;
|
||||
const { field1: chatIdOnAccountB } = event;
|
||||
const { chatId: chatIdOnAccountB } = event;
|
||||
|
||||
await dc.rpc.acceptChat(accountId2, chatIdOnAccountB);
|
||||
const messageList = await dc.rpc.messageListGetMessageIds(
|
||||
const messageList = await dc.rpc.getMessageIds(
|
||||
accountId2,
|
||||
chatIdOnAccountB,
|
||||
0
|
||||
@@ -154,7 +156,7 @@ describe("online tests", function () {
|
||||
await eventPromise2;
|
||||
|
||||
const messageId = (
|
||||
await dc.rpc.messageListGetMessageIds(accountId1, chatId, 0)
|
||||
await dc.rpc.getMessageIds(accountId1, chatId, 0)
|
||||
).reverse()[0];
|
||||
const message2 = await dc.rpc.messageGetMessage(accountId1, messageId);
|
||||
expect(message2.text).equal("super secret message");
|
||||
@@ -179,22 +181,22 @@ describe("online tests", function () {
|
||||
});
|
||||
});
|
||||
|
||||
async function waitForEvent(
|
||||
async function waitForEvent<T extends DcEvent["type"]>(
|
||||
dc: DeltaChat,
|
||||
eventType: EventTypeName,
|
||||
eventType: T,
|
||||
accountId: number,
|
||||
timeout: number = EVENT_TIMEOUT
|
||||
): Promise<DeltaChatEvent> {
|
||||
): Promise<Extract<DcEvent, { type: T }>> {
|
||||
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 = (event: DeltaChatEvent) => {
|
||||
if (event.contextId == accountId) {
|
||||
);
|
||||
const callback = (contextId: number, event: DcEvent) => {
|
||||
if (contextId == accountId) {
|
||||
dc.off(eventType, callback);
|
||||
clearTimeout(rejectTimeout)
|
||||
resolve(event);
|
||||
clearTimeout(rejectTimeout);
|
||||
resolve(event as any);
|
||||
}
|
||||
};
|
||||
dc.on(eventType, callback);
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
#![allow(clippy::format_push_string)]
|
||||
extern crate dirs;
|
||||
|
||||
use std::path::Path;
|
||||
@@ -982,11 +983,9 @@ 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.as_ref().unwrap().get_id(),
|
||||
sel_chat.map(|c| c.id),
|
||||
Viewtype::Image,
|
||||
Viewtype::Gif,
|
||||
Viewtype::Video,
|
||||
|
||||
@@ -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};
|
||||
@@ -299,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()).await?;
|
||||
let context = Context::new(Path::new(&args[1]), 0, Events::new(), StockStrings::new()).await?;
|
||||
|
||||
let events = context.get_event_emitter();
|
||||
tokio::task::spawn(async move {
|
||||
@@ -314,7 +314,6 @@ 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();
|
||||
|
||||
@@ -325,7 +324,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);
|
||||
|
||||
@@ -6,6 +6,7 @@ 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) {
|
||||
@@ -36,7 +37,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())
|
||||
let ctx = Context::new(&dbfile, 0, Events::new(), StockStrings::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-v1.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-1.x.x.tar.gz` to publish it to npm. You probably need write rights to npm.
|
||||
|
||||
## License
|
||||
|
||||
|
||||
@@ -56,6 +56,7 @@ module.exports = {
|
||||
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,
|
||||
@@ -102,6 +103,7 @@ 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,
|
||||
@@ -128,6 +130,10 @@ 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,
|
||||
@@ -158,6 +164,26 @@ 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,
|
||||
@@ -167,10 +193,20 @@ 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,
|
||||
@@ -190,10 +226,16 @@ 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,
|
||||
|
||||
@@ -30,5 +30,6 @@ module.exports = {
|
||||
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
|
||||
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
|
||||
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE'
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
2121: 'DC_EVENT_WEBXDC_INSTANCE_DELETED'
|
||||
}
|
||||
|
||||
@@ -39,6 +39,10 @@ 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)
|
||||
}
|
||||
@@ -92,6 +96,7 @@ export class Chat {
|
||||
color: this.color,
|
||||
id: this.getId(),
|
||||
name: this.getName(),
|
||||
mailinglistAddr: this.getMailinglistAddr(),
|
||||
profileImage: this.getProfileImage(),
|
||||
type: this.getType(),
|
||||
isSelfTalk: this.isSelfTalk(),
|
||||
|
||||
@@ -56,6 +56,7 @@ export enum C {
|
||||
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,
|
||||
@@ -102,6 +103,7 @@ 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,
|
||||
@@ -128,6 +130,10 @@ 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,
|
||||
@@ -158,6 +164,26 @@ 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,
|
||||
@@ -167,10 +193,20 @@ 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,
|
||||
@@ -190,10 +226,16 @@ 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,
|
||||
@@ -257,4 +299,5 @@ 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,6 +72,10 @@ 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, apiVersion: string = "v0") {
|
||||
startJsonRpcHandler(callback: ((response: string) => void) | null) {
|
||||
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, apiVersion, callback.bind(this))
|
||||
binding.dcn_accounts_start_jsonrpc(this.dcn_accounts, callback.bind(this))
|
||||
debug('Started JSON-RPC handler')
|
||||
this.jsonRpcStarted = true
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface ChatJSON {
|
||||
color: string
|
||||
id: number
|
||||
name: string
|
||||
mailinglistAddr: string
|
||||
profileImage: string
|
||||
type: number
|
||||
isSelfTalk: boolean
|
||||
|
||||
@@ -12,11 +12,12 @@ 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: 'Download the node-bindings.tar.gz',
|
||||
context: context || 'Download the node-bindings.tar.gz',
|
||||
target_url: base_url + file_url,
|
||||
}
|
||||
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
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)
|
||||
@@ -1,50 +0,0 @@
|
||||
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,6 +1628,18 @@ 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();
|
||||
@@ -1918,6 +1930,13 @@ 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();
|
||||
@@ -3093,14 +3112,14 @@ static void accounts_event_handler_thread_func(void* arg)
|
||||
|
||||
TRACE("event_handler_thread_func starting");
|
||||
|
||||
dc_accounts_event_emitter_t * dc_accounts_event_emitter = dc_accounts_get_event_emitter(dcn_accounts->dc_accounts);
|
||||
dc_event_emitter_t * dc_event_emitter = dc_accounts_get_event_emitter(dcn_accounts->dc_accounts);
|
||||
dc_event_t* event;
|
||||
while (true) {
|
||||
if (dc_accounts_event_emitter == NULL) {
|
||||
if (dc_event_emitter == NULL) {
|
||||
TRACE("event emitter is null, bailing");
|
||||
break;
|
||||
}
|
||||
event = dc_accounts_get_next_event(dc_accounts_event_emitter);
|
||||
event = dc_get_next_event(dc_event_emitter);
|
||||
if (event == NULL) {
|
||||
TRACE("no more events");
|
||||
break;
|
||||
@@ -3126,7 +3145,7 @@ static void accounts_event_handler_thread_func(void* arg)
|
||||
}
|
||||
}
|
||||
|
||||
dc_accounts_event_emitter_unref(dc_accounts_event_emitter);
|
||||
dc_event_emitter_unref(dc_event_emitter);
|
||||
|
||||
TRACE("event_handler_thread_func ended");
|
||||
|
||||
@@ -3313,10 +3332,9 @@ static void call_accounts_js_jsonrpc_handler(napi_env env, napi_value js_callbac
|
||||
}
|
||||
|
||||
NAPI_METHOD(dcn_accounts_start_jsonrpc) {
|
||||
NAPI_ARGV(3);
|
||||
NAPI_ARGV(2);
|
||||
NAPI_DCN_ACCOUNTS();
|
||||
NAPI_ARGV_UTF8_MALLOC(api_version, 1);
|
||||
napi_value callback = argv[2];
|
||||
napi_value callback = argv[1];
|
||||
|
||||
TRACE("calling..");
|
||||
napi_value async_resource_name;
|
||||
@@ -3339,7 +3357,7 @@ NAPI_METHOD(dcn_accounts_start_jsonrpc) {
|
||||
TRACE("done");
|
||||
|
||||
dcn_accounts->gc = 0;
|
||||
dcn_accounts->jsonrpc_instance = dc_jsonrpc_init(dcn_accounts->dc_accounts, api_version);
|
||||
dcn_accounts->jsonrpc_instance = dc_jsonrpc_init(dcn_accounts->dc_accounts);
|
||||
|
||||
TRACE("creating uv thread..");
|
||||
uv_thread_create(&dcn_accounts->jsonrpc_thread, accounts_jsonrpc_thread_func, dcn_accounts);
|
||||
@@ -3496,6 +3514,7 @@ 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,7 +89,11 @@ describe('JSON RPC', function () {
|
||||
const { dc } = DeltaChat.newTemporary()
|
||||
let promise_resolve
|
||||
const promise = new Promise((res, _rej) => {
|
||||
promise_resolve = res
|
||||
promise_resolve = (response) => {
|
||||
// ignore events
|
||||
const answer = JSON.parse(response)
|
||||
if (answer['method'] !== 'event') res(answer)
|
||||
}
|
||||
})
|
||||
dc.startJsonRpcHandler(promise_resolve)
|
||||
dc.jsonRpcRequest(
|
||||
@@ -106,7 +110,7 @@ describe('JSON RPC', function () {
|
||||
id: 2,
|
||||
result: [1],
|
||||
},
|
||||
JSON.parse(await promise)
|
||||
await promise
|
||||
)
|
||||
dc.close()
|
||||
})
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"@deltachat/jsonrpc-client": "file:deltachat-jsonrpc/typescript",
|
||||
"debug": "^4.1.1",
|
||||
"napi-macros": "^2.0.0",
|
||||
"node-gyp-build": "^4.1.0"
|
||||
@@ -58,8 +57,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"
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.87.0"
|
||||
"version": "1.97.0"
|
||||
}
|
||||
@@ -1,84 +1,77 @@
|
||||
=========================
|
||||
deltachat python bindings
|
||||
DeltaChat Python bindings
|
||||
=========================
|
||||
|
||||
This package provides bindings to the deltachat-core_ Rust -library
|
||||
which implements IMAP/SMTP/MIME/PGP e-mail standards and offers
|
||||
This package provides `Python bindings`_ to the `deltachat-core library`_
|
||||
which implements IMAP/SMTP/MIME/OpenPGP 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 venv # or: python -m venv
|
||||
source venv/bin/activate
|
||||
virtualenv env # or: python -m venv
|
||||
source env/bin/activate
|
||||
|
||||
Afterwards, invoking ``python`` or ``pip install`` only
|
||||
modifies files in your ``venv`` directory and leaves
|
||||
modifies files in your ``env`` directory and leaves
|
||||
your system installation alone.
|
||||
|
||||
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::
|
||||
For Linux we build wheels for all releases and push them to a python package
|
||||
index. To install the latest release::
|
||||
|
||||
pip install --pre -i https://m.devpi.net/dc/master deltachat
|
||||
pip install 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
|
||||
=============
|
||||
|
||||
After successful binding installation you can install a few more
|
||||
Python packages before running the tests::
|
||||
Recommended way to run tests is using `tox <https://tox.wiki>`_.
|
||||
After successful binding installation you can install tox
|
||||
and run the tests::
|
||||
|
||||
python -m pip install pytest pytest-xdist pytest-timeout pytest-rerunfailures requests
|
||||
pytest -v tests
|
||||
pip install tox
|
||||
tox -e py3
|
||||
|
||||
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/en/latest/).
|
||||
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/>`_.
|
||||
|
||||
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::
|
||||
|
||||
pytest
|
||||
|
||||
# or if you have installed pytest-xdist for parallel test execution
|
||||
pytest -n6
|
||||
tox -e py3
|
||||
|
||||
Each test run creates new accounts.
|
||||
|
||||
|
||||
.. _sourceinstall:
|
||||
|
||||
Installing bindings from source (Updated: July 2020)
|
||||
=========================================================
|
||||
Installing bindings from source
|
||||
===============================
|
||||
|
||||
Install Rust and Cargo first.
|
||||
The easiest is probably to use `rustup <https://rustup.rs/>`_.
|
||||
@@ -97,74 +90,42 @@ 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 and activate it in your shell::
|
||||
virtual environment with dependencies using tox
|
||||
and activate it in your shell::
|
||||
|
||||
cd python
|
||||
python3 -m venv venv # or: virtualenv venv
|
||||
source venv/bin/activate
|
||||
tox --devenv env
|
||||
source env/bin/activate
|
||||
|
||||
You should now be able to build the python bindings using the supplied script::
|
||||
|
||||
python install_python_bindings.py
|
||||
python3 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-tooling.
|
||||
can be done with Docker_ or Podman_.
|
||||
|
||||
using docker pull / premade images
|
||||
------------------------------------
|
||||
.. _Docker: https://www.docker.com/
|
||||
.. _Podman: https://podman.io/
|
||||
|
||||
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::
|
||||
If you want to build your own wheels, build container image first::
|
||||
|
||||
$ docker pull deltachat/coredeps
|
||||
$ cd deltachat-core-rust # cd to deltachat-core-rust working tree
|
||||
$ docker build -t deltachat/coredeps scripts/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 \
|
||||
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
|
||||
This will use the ``scripts/coredeps/Dockerfile`` to build
|
||||
container image called ``deltachat/coredeps``. You can afterwards
|
||||
find it with::
|
||||
|
||||
$ docker images
|
||||
|
||||
This docker image can be used to run tests and build Python wheels for all interpreters::
|
||||
|
||||
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``.
|
||||
$ docker run -e DCC_NEW_TMP_EMAIL \
|
||||
--rm -it -v $(pwd):/mnt -w /mnt \
|
||||
deltachat/coredeps scripts/run_all.sh
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
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"]
|
||||
cmd = ["cargo", "build", "-p", "deltachat_ffi", "--features", "jsonrpc"]
|
||||
|
||||
if target == "release":
|
||||
os.environ["CARGO_PROFILE_RELEASE_LTO"] = "on"
|
||||
|
||||
@@ -1,7 +1,41 @@
|
||||
[build-system]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0", "pkgconfig"]
|
||||
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=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>.*)?$'
|
||||
|
||||
@@ -1,4 +0,0 @@
|
||||
[devpi:upload]
|
||||
formats = sdist.tgz
|
||||
no-vcs = 1
|
||||
|
||||
@@ -1,40 +1,4 @@
|
||||
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",
|
||||
],
|
||||
)
|
||||
|
||||
from setuptools import setup
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
setup(cffi_modules=["src/deltachat/_build.py:ffibuilder"])
|
||||
|
||||
@@ -2,7 +2,7 @@ import sys
|
||||
|
||||
from pkg_resources import DistributionNotFound, get_distribution
|
||||
|
||||
from . import capi, const, events, hookspec # noqa
|
||||
from . import capi, events, hookspec # noqa
|
||||
from .account import Account, get_core_info # noqa
|
||||
from .capi import ffi # noqa
|
||||
from .chat import Chat # noqa
|
||||
@@ -17,14 +17,6 @@ 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.
|
||||
@@ -60,29 +52,7 @@ def run_cmdline(argv=None, account_plugins=None):
|
||||
|
||||
ac = Account(args.db)
|
||||
|
||||
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()
|
||||
ac.run_account(addr=args.email, password=args.password, account_plugins=account_plugins, show_ffi=args.show_ffi)
|
||||
|
||||
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
|
||||
from .events import EventThread, FFIEventLogger
|
||||
from .message import Message
|
||||
from .tracker import ConfigureTracker, ImexTracker
|
||||
|
||||
@@ -169,8 +169,6 @@ 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:
|
||||
@@ -598,6 +596,36 @@ 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.
|
||||
@@ -680,8 +708,9 @@ 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,14 +8,21 @@ import traceback
|
||||
from contextlib import contextmanager
|
||||
from queue import Empty, Queue
|
||||
|
||||
import deltachat
|
||||
|
||||
from . import const
|
||||
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
|
||||
@@ -239,7 +246,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 = deltachat.get_dc_event_name(evt)
|
||||
evt_name = 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,6 +507,8 @@ 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.
|
||||
@@ -518,6 +520,10 @@ 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,6 +497,8 @@ 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
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
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,8 +1581,9 @@ 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 msg1.text == "hi" or msg2.text == "hi"
|
||||
assert msg2.text == "hi"
|
||||
assert msg1.chat.id == msg2.chat.id
|
||||
|
||||
lp.sec("ac2: see if chat now has got the profile image")
|
||||
@@ -1596,6 +1597,8 @@ 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
|
||||
|
||||
@@ -1854,7 +1857,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/mod.rs returned false because the error message was changed
|
||||
# in configure.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:
|
||||
@@ -2052,6 +2055,47 @@ 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"] == "0"
|
||||
assert d["bcc_self"] == "1"
|
||||
|
||||
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") == "0"
|
||||
assert ac1.get_config("bcc_self") == "1"
|
||||
|
||||
def test_selfcontact_if_unconfigured(self, acfactory):
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
@@ -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_MSGGRPNAME, "abc %1$s xyz %2$s")
|
||||
ac1.set_stock_translation(const.DC_STR_GROUP_NAME_CHANGED_BY_YOU, "abc %1$s xyz %2$s")
|
||||
ac1._evtracker.consume_events()
|
||||
with pytest.raises(ValueError):
|
||||
ac1.set_stock_translation(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 by me."
|
||||
assert chat.get_messages()[-1].text == "abc homework xyz Homework"
|
||||
|
||||
@pytest.mark.parametrize("verified", [True, False])
|
||||
def test_group_chat_qr(self, acfactory, ac1, verified):
|
||||
@@ -497,12 +497,6 @@ 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,9 +9,8 @@ envlist =
|
||||
[testenv]
|
||||
commands =
|
||||
pytest -n6 --extra-info --reruns 2 --reruns-delay 5 -v -rsXx --ignored --strict-tls {posargs: tests examples}
|
||||
python tests/package_wheels.py {toxworkdir}/wheelhouse
|
||||
pip wheel . -w {toxworkdir}/wheelhouse --no-deps
|
||||
passenv =
|
||||
TRAVIS
|
||||
DCC_RS_DEV
|
||||
DCC_RS_TARGET
|
||||
DCC_NEW_TMP_EMAIL
|
||||
|
||||
@@ -14,18 +14,15 @@ 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.
|
||||
|
||||
- `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-doxygen.sh` generates C-docs which are then uploaded to https://c.delta.chat/
|
||||
|
||||
- `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
|
||||
@@ -33,19 +30,18 @@ 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.
|
||||
|
||||
# Outdated files (for later re-use)
|
||||
# coredeps Dockerfile
|
||||
|
||||
`coredeps/Dockerfile` specifies an image that contains all
|
||||
of Delta Chat's core dependencies. It used to run
|
||||
python tests and build wheels (binary packages for Python)
|
||||
of Delta Chat's core dependencies. It is used to
|
||||
build python 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 docker-coredeps
|
||||
docker build -t deltachat/doxygen docker-doxygen
|
||||
docker build -t deltachat/coredeps coredeps
|
||||
|
||||
Additionally, you can install qemu and build arm64 docker image:
|
||||
Additionally, you can install qemu and build arm64 docker image on x86\_64 machine:
|
||||
apt-get install qemu binfmt-support qemu-user-static
|
||||
docker build -t deltachat/coredeps-arm64 docker-coredeps-arm64
|
||||
docker build -t deltachat/coredeps-arm64 --build-arg BASEIMAGE=quay.io/pypa/manylinux2014_aarch64 coredeps
|
||||
|
||||
@@ -29,18 +29,19 @@ jobs:
|
||||
- name: c-docs
|
||||
image_resource:
|
||||
source:
|
||||
repository: hrektts/doxygen
|
||||
repository: alpine
|
||||
type: registry-image
|
||||
platform: linux
|
||||
run:
|
||||
path: bash
|
||||
path: sh
|
||||
args:
|
||||
- -exc
|
||||
- -ec
|
||||
- |
|
||||
apk add --no-cache doxygen git
|
||||
cd deltachat-core-rust
|
||||
bash scripts/run-doxygen.sh
|
||||
scripts/run-doxygen.sh
|
||||
cd ..
|
||||
cp -av deltachat-core-rust/deltachat-ffi/{html,xml} c-docs/
|
||||
cp -av deltachat-core-rust/deltachat-ffi/html deltachat-core-rust/deltachat-ffi/xml c-docs/
|
||||
|
||||
- task: upload-c-docs
|
||||
config:
|
||||
@@ -302,3 +303,73 @@ 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"
|
||||
export RUSTFLAGS="-Zprofile -Ccodegen-units=1 -Copt-level=0 -Clink-dead-code -Coverflow-checks=off -Zpanic_abort_tests -Cpanic=abort -Cdebuginfo=2"
|
||||
export RUSTDOCFLAGS="-Cpanic=abort"
|
||||
cargo clean
|
||||
cargo build
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
FROM debian:stable
|
||||
|
||||
# this is tagged as deltachat/doxygen
|
||||
|
||||
RUN apt-get update && apt-get install -y doxygen
|
||||
@@ -1,6 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -ex
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
cd deltachat-ffi
|
||||
PROJECT_NUMBER=$(git log -1 --format="%h (%cd)") doxygen
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/bin/sh
|
||||
#
|
||||
# Build the Delta Chat Core Rust library, Python wheels and docs
|
||||
|
||||
@@ -8,21 +8,16 @@ set -e -x
|
||||
|
||||
# compile core lib
|
||||
|
||||
export PATH=/root/.cargo/bin:$PATH
|
||||
cargo build --release -p deltachat_ffi
|
||||
# cargo test --all --all-features
|
||||
cargo build --release -p deltachat_ffi --features jsonrpc
|
||||
|
||||
# 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__
|
||||
@@ -33,11 +28,13 @@ mkdir -p $TOXWORKDIR
|
||||
# Note that the independent remote_tests_python step does all kinds of
|
||||
# live-testing already.
|
||||
unset DCC_NEW_TMP_EMAIL
|
||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,py310,pypy37,pypy38,pypy39,auditwheels
|
||||
popd
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
echo -----------------------
|
||||
echo generating python docs
|
||||
echo -----------------------
|
||||
(cd python && tox --workdir "$TOXWORKDIR" -e doc)
|
||||
tox --workdir "$TOXWORKDIR" -e doc
|
||||
|
||||
19
scripts/set_core_version.py
Normal file → Executable file
19
scripts/set_core_version.py
Normal file → Executable file
@@ -42,15 +42,15 @@ def replace_toml_version(relpath, newversion):
|
||||
|
||||
|
||||
def read_json_version(relpath):
|
||||
p = pathlib.Path("package.json")
|
||||
p = pathlib.Path(relpath)
|
||||
assert p.exists()
|
||||
with open(p, "r") as f:
|
||||
json_data = json.loads(f.read())
|
||||
return json_data["version"]
|
||||
|
||||
|
||||
def update_package_json(newversion):
|
||||
p = pathlib.Path("package.json")
|
||||
def update_package_json(relpath, newversion):
|
||||
p = pathlib.Path(relpath)
|
||||
assert p.exists()
|
||||
with open(p, "r") as f:
|
||||
json_data = json.loads(f.read())
|
||||
@@ -64,13 +64,15 @@ def main():
|
||||
parser.add_argument("newversion")
|
||||
|
||||
toml_list = ["Cargo.toml", "deltachat-ffi/Cargo.toml", "deltachat-jsonrpc/Cargo.toml"]
|
||||
json_list = ["package.json", "deltachat-jsonrpc/typescript/package.json"]
|
||||
try:
|
||||
opts = parser.parse_args()
|
||||
except SystemExit:
|
||||
print()
|
||||
for x in toml_list:
|
||||
print("{}: {}".format(x, read_toml_version(x)))
|
||||
print("package.json:", str(read_json_version("package.json")))
|
||||
for x in json_list:
|
||||
print("{}: {}".format(x, read_json_version(x)))
|
||||
print()
|
||||
raise SystemExit("need argument: new version, example: 1.25.0")
|
||||
|
||||
@@ -91,9 +93,12 @@ def main():
|
||||
else:
|
||||
raise SystemExit("CHANGELOG.md contains no entry for version: {}".format(newversion))
|
||||
|
||||
replace_toml_version("Cargo.toml", newversion)
|
||||
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
|
||||
update_package_json(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)
|
||||
|
||||
|
||||
print("running cargo check")
|
||||
subprocess.call(["cargo", "check"])
|
||||
|
||||
162
src/accounts.rs
162
src/accounts.rs
@@ -10,6 +10,7 @@ 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)]
|
||||
@@ -20,6 +21,12 @@ 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 {
|
||||
@@ -33,7 +40,7 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Creates a new default structure.
|
||||
pub async fn create(dir: &PathBuf) -> Result<()> {
|
||||
pub async fn create(dir: &Path) -> Result<()> {
|
||||
fs::create_dir_all(dir)
|
||||
.await
|
||||
.context("failed to create folder")?;
|
||||
@@ -55,8 +62,9 @@ impl Accounts {
|
||||
.await
|
||||
.context("failed to load accounts config")?;
|
||||
let events = Events::new();
|
||||
let stockstrings = StockStrings::new();
|
||||
let accounts = config
|
||||
.load_accounts(&events)
|
||||
.load_accounts(&events, &stockstrings)
|
||||
.await
|
||||
.context("failed to load accounts")?;
|
||||
|
||||
@@ -65,23 +73,24 @@ impl Accounts {
|
||||
config,
|
||||
accounts,
|
||||
events,
|
||||
stockstrings,
|
||||
})
|
||||
}
|
||||
|
||||
/// Get an account by its `id`:
|
||||
pub async fn get_account(&self, id: u32) -> Option<Context> {
|
||||
pub fn get_account(&self, id: u32) -> Option<Context> {
|
||||
self.accounts.get(&id).cloned()
|
||||
}
|
||||
|
||||
/// Get the currently selected account.
|
||||
pub async fn get_selected_account(&self) -> Option<Context> {
|
||||
let id = self.config.get_selected_account().await;
|
||||
pub fn get_selected_account(&self) -> Option<Context> {
|
||||
let id = self.config.get_selected_account();
|
||||
self.accounts.get(&id).cloned()
|
||||
}
|
||||
|
||||
/// Returns the currently selected account's id or None if no account is selected.
|
||||
pub async fn get_selected_account_id(&self) -> Option<u32> {
|
||||
match self.config.get_selected_account().await {
|
||||
pub fn get_selected_account_id(&self) -> Option<u32> {
|
||||
match self.config.get_selected_account() {
|
||||
0 => None,
|
||||
id => Some(id),
|
||||
}
|
||||
@@ -104,6 +113,7 @@ impl Accounts {
|
||||
&account_config.dbfile(),
|
||||
account_config.id,
|
||||
self.events.clone(),
|
||||
self.stockstrings.clone(),
|
||||
)
|
||||
.await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
@@ -119,6 +129,7 @@ impl Accounts {
|
||||
&account_config.dbfile(),
|
||||
account_config.id,
|
||||
self.events.clone(),
|
||||
self.stockstrings.clone(),
|
||||
)
|
||||
.await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
@@ -135,7 +146,7 @@ impl Accounts {
|
||||
ctx.stop_io().await;
|
||||
drop(ctx);
|
||||
|
||||
if let Some(cfg) = self.config.get_account(id).await {
|
||||
if let Some(cfg) = self.config.get_account(id) {
|
||||
// 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
|
||||
@@ -171,7 +182,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().await;
|
||||
let old_id = self.config.get_selected_account();
|
||||
|
||||
// create new account
|
||||
let account_config = self
|
||||
@@ -204,7 +215,13 @@ impl Accounts {
|
||||
|
||||
match res {
|
||||
Ok(_) => {
|
||||
let ctx = Context::new(&new_dbfile, account_config.id, self.events.clone()).await?;
|
||||
let ctx = Context::new(
|
||||
&new_dbfile,
|
||||
account_config.id,
|
||||
self.events.clone(),
|
||||
self.stockstrings.clone(),
|
||||
)
|
||||
.await?;
|
||||
self.accounts.insert(account_config.id, ctx);
|
||||
Ok(account_config.id)
|
||||
}
|
||||
@@ -225,7 +242,7 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Get a list of all account ids.
|
||||
pub async fn get_all(&self) -> Vec<u32> {
|
||||
pub fn get_all(&self) -> Vec<u32> {
|
||||
self.accounts.keys().copied().collect()
|
||||
}
|
||||
|
||||
@@ -281,7 +298,7 @@ impl Accounts {
|
||||
}
|
||||
|
||||
/// Returns event emitter.
|
||||
pub async fn get_event_emitter(&self) -> EventEmitter {
|
||||
pub fn get_event_emitter(&self) -> EventEmitter {
|
||||
self.events.get_emitter()
|
||||
}
|
||||
}
|
||||
@@ -339,17 +356,31 @@ impl Config {
|
||||
Ok(Config { file, inner })
|
||||
}
|
||||
|
||||
pub async fn load_accounts(&self, events: &Events) -> Result<BTreeMap<u32, Context>> {
|
||||
/// 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>> {
|
||||
let mut accounts = BTreeMap::new();
|
||||
|
||||
for account_config in &self.inner.accounts {
|
||||
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()
|
||||
)
|
||||
})?;
|
||||
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()
|
||||
)
|
||||
})?;
|
||||
|
||||
accounts.insert(account_config.id, ctx);
|
||||
}
|
||||
@@ -380,7 +411,6 @@ 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)
|
||||
}
|
||||
@@ -402,11 +432,11 @@ impl Config {
|
||||
self.sync().await
|
||||
}
|
||||
|
||||
async fn get_account(&self, id: u32) -> Option<AccountConfig> {
|
||||
fn get_account(&self, id: u32) -> Option<AccountConfig> {
|
||||
self.inner.accounts.iter().find(|e| e.id == id).cloned()
|
||||
}
|
||||
|
||||
pub async fn get_selected_account(&self) -> u32 {
|
||||
pub fn get_selected_account(&self) -> u32 {
|
||||
self.inner.selected_account
|
||||
}
|
||||
|
||||
@@ -447,6 +477,8 @@ 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();
|
||||
@@ -458,7 +490,7 @@ mod tests {
|
||||
let accounts2 = Accounts::open(p).await.unwrap();
|
||||
|
||||
assert_eq!(accounts1.accounts.len(), 1);
|
||||
assert_eq!(accounts1.config.get_selected_account().await, 1);
|
||||
assert_eq!(accounts1.config.get_selected_account(), 1);
|
||||
|
||||
assert_eq!(accounts1.dir, accounts2.dir);
|
||||
assert_eq!(accounts1.config, accounts2.config,);
|
||||
@@ -472,23 +504,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().await, 0);
|
||||
assert_eq!(accounts.config.get_selected_account(), 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().await, 1);
|
||||
assert_eq!(accounts.config.get_selected_account(), 1);
|
||||
|
||||
let id = accounts.add_account().await.unwrap();
|
||||
assert_eq!(id, 2);
|
||||
assert_eq!(accounts.config.get_selected_account().await, id);
|
||||
assert_eq!(accounts.config.get_selected_account(), id);
|
||||
assert_eq!(accounts.accounts.len(), 2);
|
||||
|
||||
accounts.select_account(1).await.unwrap();
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
assert_eq!(accounts.config.get_selected_account(), 1);
|
||||
|
||||
accounts.remove_account(1).await.unwrap();
|
||||
assert_eq!(accounts.config.get_selected_account().await, 2);
|
||||
assert_eq!(accounts.config.get_selected_account(), 2);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
}
|
||||
|
||||
@@ -498,17 +530,17 @@ mod tests {
|
||||
let p: PathBuf = dir.path().join("accounts");
|
||||
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
assert!(accounts.get_selected_account().await.is_none());
|
||||
assert_eq!(accounts.config.get_selected_account().await, 0);
|
||||
assert!(accounts.get_selected_account().is_none());
|
||||
assert_eq!(accounts.config.get_selected_account(), 0);
|
||||
|
||||
let id = accounts.add_account().await?;
|
||||
assert!(accounts.get_selected_account().await.is_some());
|
||||
assert!(accounts.get_selected_account().is_some());
|
||||
assert_eq!(id, 1);
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, id);
|
||||
assert_eq!(accounts.config.get_selected_account(), id);
|
||||
|
||||
accounts.remove_account(id).await?;
|
||||
assert!(accounts.get_selected_account().await.is_none());
|
||||
assert!(accounts.get_selected_account().is_none());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -520,10 +552,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().await, 0);
|
||||
assert_eq!(accounts.config.get_selected_account(), 0);
|
||||
|
||||
let extern_dbfile: PathBuf = dir.path().join("other");
|
||||
let ctx = Context::new(&extern_dbfile, 0, Events::new())
|
||||
let ctx = Context::new(&extern_dbfile, 0, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("me@mail.com"))
|
||||
@@ -537,9 +569,9 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(accounts.accounts.len(), 1);
|
||||
assert_eq!(accounts.config.get_selected_account().await, 1);
|
||||
assert_eq!(accounts.config.get_selected_account(), 1);
|
||||
|
||||
let ctx = accounts.get_selected_account().await.unwrap();
|
||||
let ctx = accounts.get_selected_account().unwrap();
|
||||
assert_eq!(
|
||||
"me@mail.com",
|
||||
ctx.get_config(crate::config::Config::Addr)
|
||||
@@ -562,7 +594,7 @@ mod tests {
|
||||
assert_eq!(id, expected_id);
|
||||
}
|
||||
|
||||
let ids = accounts.get_all().await;
|
||||
let ids = accounts.get_all();
|
||||
for (i, expected_id) in (1..10).enumerate() {
|
||||
assert_eq!(ids.get(i), Some(&expected_id));
|
||||
}
|
||||
@@ -577,16 +609,16 @@ mod tests {
|
||||
let (id0, id1, id2) = {
|
||||
let mut accounts = Accounts::new(p.clone()).await?;
|
||||
accounts.add_account().await?;
|
||||
let ids = accounts.get_all().await;
|
||||
let ids = accounts.get_all();
|
||||
assert_eq!(ids.len(), 1);
|
||||
|
||||
let id0 = *ids.get(0).unwrap();
|
||||
let ctx = accounts.get_account(id0).await.unwrap();
|
||||
let id0 = *ids.first().unwrap();
|
||||
let ctx = accounts.get_account(id0).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).await.unwrap();
|
||||
let ctx = accounts.get_account(id1).unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("two@example.org"))
|
||||
.await?;
|
||||
|
||||
@@ -597,7 +629,7 @@ mod tests {
|
||||
}
|
||||
|
||||
let id2 = accounts.add_account().await?;
|
||||
let ctx = accounts.get_account(id2).await.unwrap();
|
||||
let ctx = accounts.get_account(id2).unwrap();
|
||||
ctx.set_config(crate::config::Config::Addr, Some("three@example.org"))
|
||||
.await?;
|
||||
|
||||
@@ -611,31 +643,31 @@ mod tests {
|
||||
|
||||
let (id0_reopened, id1_reopened, id2_reopened) = {
|
||||
let accounts = Accounts::new(p.clone()).await?;
|
||||
let ctx = accounts.get_selected_account().await.unwrap();
|
||||
let ctx = accounts.get_selected_account().unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
Some("two@example.org".to_string())
|
||||
);
|
||||
|
||||
let ids = accounts.get_all().await;
|
||||
let ids = accounts.get_all();
|
||||
assert_eq!(ids.len(), 3);
|
||||
|
||||
let id0 = *ids.get(0).unwrap();
|
||||
let ctx = accounts.get_account(id0).await.unwrap();
|
||||
let id0 = *ids.first().unwrap();
|
||||
let ctx = accounts.get_account(id0).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).await.unwrap();
|
||||
let t = accounts.get_account(id1).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).await.unwrap();
|
||||
let ctx = accounts.get_account(id2).unwrap();
|
||||
assert_eq!(
|
||||
ctx.get_config(crate::config::Config::Addr).await?,
|
||||
Some("three@example.org".to_string())
|
||||
@@ -661,7 +693,7 @@ mod tests {
|
||||
assert_eq!(accounts.accounts.len(), 0);
|
||||
|
||||
// Create event emitter.
|
||||
let event_emitter = accounts.get_event_emitter().await;
|
||||
let event_emitter = accounts.get_event_emitter();
|
||||
|
||||
// Test that event emitter does not return `None` immediately.
|
||||
let duration = std::time::Duration::from_millis(1);
|
||||
@@ -692,7 +724,6 @@ 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
|
||||
@@ -707,7 +738,6 @@ 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);
|
||||
|
||||
@@ -720,4 +750,28 @@ 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(())
|
||||
}
|
||||
}
|
||||
|
||||
13
src/blob.rs
13
src/blob.rs
@@ -326,10 +326,7 @@ 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))
|
||||
.await?
|
||||
{
|
||||
if let Some(new_name) = self.recode_to_size(context, blob_abs, img_wh, Some(20_000))? {
|
||||
self.name = new_name;
|
||||
}
|
||||
Ok(())
|
||||
@@ -352,8 +349,7 @@ impl<'a> BlobObject<'a> {
|
||||
};
|
||||
|
||||
if self
|
||||
.recode_to_size(context, blob_abs, img_wh, None)
|
||||
.await?
|
||||
.recode_to_size(context, blob_abs, img_wh, None)?
|
||||
.is_some()
|
||||
{
|
||||
return Err(format_err!(
|
||||
@@ -363,7 +359,7 @@ impl<'a> BlobObject<'a> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn recode_to_size(
|
||||
fn recode_to_size(
|
||||
&self,
|
||||
context: &Context,
|
||||
mut blob_abs: PathBuf,
|
||||
@@ -738,7 +734,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: &PathBuf) -> u64 {
|
||||
async fn file_size(path_buf: &Path) -> u64 {
|
||||
let file = File::open(path_buf).await.unwrap();
|
||||
file.metadata().await.unwrap().len()
|
||||
}
|
||||
@@ -746,7 +742,6 @@ 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);
|
||||
|
||||
226
src/chat.rs
226
src/chat.rs
@@ -40,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)]
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum ChatItem {
|
||||
Message {
|
||||
msg_id: MsgId,
|
||||
@@ -1129,6 +1129,11 @@ impl Chat {
|
||||
&self.name
|
||||
}
|
||||
|
||||
/// Returns mailing list address where messages are sent to.
|
||||
pub fn get_mailinglist_addr(&self) -> &str {
|
||||
self.param.get(Param::ListPost).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
@@ -2249,7 +2254,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,
|
||||
timestamp: curr_day * 86400, // Convert day back to Unix timestamp
|
||||
});
|
||||
last_day = curr_day;
|
||||
}
|
||||
@@ -2418,7 +2423,7 @@ pub(crate) async fn mark_old_messages_as_noticed(
|
||||
|
||||
pub async fn get_chat_media(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
chat_id: Option<ChatId>,
|
||||
msg_type: Viewtype,
|
||||
msg_type2: Viewtype,
|
||||
msg_type3: Viewtype,
|
||||
@@ -2429,11 +2434,13 @@ pub async fn get_chat_media(
|
||||
.query_map(
|
||||
"SELECT id
|
||||
FROM msgs
|
||||
WHERE chat_id=?
|
||||
WHERE (1=? OR chat_id=?)
|
||||
AND (type=? OR type=? OR type=?)
|
||||
AND hidden=0
|
||||
ORDER BY timestamp, id;",
|
||||
paramsv![
|
||||
chat_id,
|
||||
if chat_id.is_none() { 1i32 } else { 0i32 },
|
||||
chat_id.unwrap_or_else(|| ChatId::new(0)),
|
||||
msg_type,
|
||||
if msg_type2 != Viewtype::Unknown {
|
||||
msg_type2
|
||||
@@ -2474,7 +2481,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,
|
||||
msg.chat_id,
|
||||
Some(msg.chat_id),
|
||||
if msg_type != Viewtype::Unknown {
|
||||
msg_type
|
||||
} else {
|
||||
@@ -2518,7 +2525,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, LOWER(c.name||c.addr), c.id;",
|
||||
ORDER BY c.id=1, c.last_seen DESC, c.id DESC;",
|
||||
paramsv![chat_id],
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| ids.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
@@ -2781,7 +2788,7 @@ pub(crate) async fn shall_attach_selfavatar(context: &Context, chat_id: ChatId)
|
||||
Ok(needs_attach)
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum MuteDuration {
|
||||
NotMuted,
|
||||
Forever,
|
||||
@@ -2856,7 +2863,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? {
|
||||
@@ -3362,6 +3369,15 @@ 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(())
|
||||
}
|
||||
@@ -4547,26 +4563,24 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_set_protection() {
|
||||
async fn test_set_protection() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "foo")
|
||||
.await
|
||||
.unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
|
||||
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?;
|
||||
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
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert!(chat.is_protected());
|
||||
assert!(chat.is_unpromoted());
|
||||
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0).await.unwrap();
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0).await?;
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
let msg = t.get_last_msg_in(chat_id).await;
|
||||
@@ -4577,10 +4591,9 @@ mod tests {
|
||||
// disable protection again, still unpromoted
|
||||
chat_id
|
||||
.set_protection(&t, ProtectionStatus::Unprotected)
|
||||
.await
|
||||
.unwrap();
|
||||
.await?;
|
||||
|
||||
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(chat.is_unpromoted());
|
||||
|
||||
@@ -4590,21 +4603,20 @@ 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.unwrap();
|
||||
send_text_msg(&t, chat_id, "hi!".to_string()).await?;
|
||||
|
||||
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert!(!chat.is_protected());
|
||||
assert!(!chat.is_unpromoted());
|
||||
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0).await.unwrap();
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0).await?;
|
||||
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
|
||||
.unwrap();
|
||||
let chat = Chat::load_from_db(&t, chat_id).await.unwrap();
|
||||
.await?;
|
||||
let chat = Chat::load_from_db(&t, chat_id).await?;
|
||||
assert!(chat.is_protected());
|
||||
assert!(!chat.is_unpromoted());
|
||||
|
||||
@@ -4612,6 +4624,8 @@ 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)]
|
||||
@@ -5449,8 +5463,8 @@ mod tests {
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(&alice).await?,
|
||||
"No encryption:\n\
|
||||
bob@example.net\n\
|
||||
fiona@example.net"
|
||||
fiona@example.net\n\
|
||||
bob@example.net"
|
||||
);
|
||||
|
||||
let direct_chat = bob.create_chat(&alice).await;
|
||||
@@ -5481,4 +5495,158 @@ 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.get(0).map_or(0, |&x| u16::from(x))
|
||||
let checksum: u16 = result.first().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, IntoEnumIterator};
|
||||
use strum::{EnumProperty as EnumPropertyTrait, 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 = "0"))]
|
||||
#[strum(props(default = "1"))]
|
||||
BccSelf,
|
||||
|
||||
#[strum(props(default = "1"))]
|
||||
@@ -198,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?,
|
||||
|
||||
@@ -12,9 +12,11 @@ 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;
|
||||
@@ -101,35 +103,11 @@ 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?;
|
||||
|
||||
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");
|
||||
}
|
||||
}
|
||||
}
|
||||
on_configure_completed(self, param, old_addr).await?;
|
||||
|
||||
success?;
|
||||
self.set_config(Config::NotifyAboutWrongPw, Some("1"))
|
||||
@@ -138,6 +116,54 @@ 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);
|
||||
|
||||
@@ -170,7 +196,7 @@ async fn configure(ctx: &Context, param: &mut LoginParam) -> Result<()> {
|
||||
}
|
||||
// no oauth? - just continue it's no error
|
||||
|
||||
let parsed: EmailAddress = param.addr.parse().context("Bad email-address")?;
|
||||
let parsed = EmailAddress::new(¶m.addr).context("Bad email-address")?;
|
||||
let param_domain = parsed.domain;
|
||||
let param_addr_urlencoded = utf8_percent_encode(¶m.addr, NON_ALPHANUMERIC).to_string();
|
||||
|
||||
@@ -553,8 +579,7 @@ 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).await
|
||||
{
|
||||
let mut imap = match Imap::new(param, socks5_config.clone(), addr, provider_strict_tls, r) {
|
||||
Err(err) => {
|
||||
info!(context, "failure: {}", err);
|
||||
return Err(ConfigurationError {
|
||||
@@ -642,6 +667,9 @@ 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,6 +167,11 @@ 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.
|
||||
///
|
||||
@@ -176,7 +181,7 @@ pub const DC_ELLIPSIS: &str = "[...]";
|
||||
///
|
||||
/// 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 = 5000;
|
||||
pub const DC_DESIRED_TEXT_LEN: usize = DC_DESIRED_TEXT_LINE_LEN * DC_DESIRED_TEXT_LINES;
|
||||
|
||||
// Flags for empty server job
|
||||
|
||||
|
||||
@@ -24,9 +24,12 @@ use crate::mimeparser::AvatarAction;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::sql::{self, params_iter};
|
||||
use crate::tools::{get_abs_path, improve_single_line_input, EmailAddress};
|
||||
use crate::tools::{get_abs_path, improve_single_line_input, time, 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
|
||||
@@ -323,6 +326,11 @@ 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;
|
||||
@@ -498,9 +506,9 @@ impl Contact {
|
||||
let mut update_addr = false;
|
||||
let mut row_id = 0;
|
||||
|
||||
if let Ok((id, row_name, row_addr, row_origin, row_authname)) = context
|
||||
if let Some((id, row_name, row_addr, row_origin, row_authname)) = context
|
||||
.sql
|
||||
.query_row(
|
||||
.query_row_optional(
|
||||
"SELECT id, name, addr, origin, authname \
|
||||
FROM contacts WHERE addr=? COLLATE NOCASE;",
|
||||
paramsv![addr.to_string()],
|
||||
@@ -514,7 +522,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
|
||||
@@ -708,7 +716,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 LOWER(iif(c.name='',c.authname,c.name)||c.addr),c.id;",
|
||||
ORDER BY c.last_seen DESC, c.id DESC;",
|
||||
sql::repeat_vars(self_addrs.len())
|
||||
),
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![
|
||||
@@ -760,7 +768,7 @@ impl Contact {
|
||||
AND id>?
|
||||
AND origin>=?
|
||||
AND blocked=0
|
||||
ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
|
||||
ORDER BY last_seen DESC, id DESC;",
|
||||
sql::repeat_vars(self_addrs.len())
|
||||
),
|
||||
rusqlite::params_from_iter(params_iter(&self_addrs).chain(params_iterv![
|
||||
@@ -849,7 +857,7 @@ impl Contact {
|
||||
let list = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY LOWER(iif(name='',authname,name)||addr),id;",
|
||||
"SELECT id FROM contacts WHERE id>? AND blocked!=0 ORDER BY last_seen DESC, id DESC;",
|
||||
paramsv![ContactId::LAST_SPECIAL],
|
||||
|row| row.get::<_, ContactId>(0),
|
||||
|ids| {
|
||||
@@ -888,11 +896,8 @@ impl Contact {
|
||||
EncryptPreference::Reset => stock_str::encr_none(context).await,
|
||||
};
|
||||
|
||||
ret += &format!(
|
||||
"{}.\n{}:",
|
||||
stock_message,
|
||||
stock_str::finger_prints(context).await
|
||||
);
|
||||
let finger_prints = stock_str::finger_prints(context).await;
|
||||
ret += &format!("{}.\n{}:", stock_message, finger_prints);
|
||||
|
||||
let fingerprint_self = SignedPublicKey::load_self(context)
|
||||
.await?
|
||||
@@ -1178,7 +1183,7 @@ impl Contact {
|
||||
|
||||
/// Returns false if addr is an invalid address, otherwise true.
|
||||
pub fn may_be_valid_addr(addr: &str) -> bool {
|
||||
let res = addr.parse::<EmailAddress>();
|
||||
let res = EmailAddress::new(addr);
|
||||
res.is_ok()
|
||||
}
|
||||
|
||||
@@ -1429,7 +1434,7 @@ fn split_address_book(book: &str) -> Vec<(&str, &str)> {
|
||||
.chunks(2)
|
||||
.into_iter()
|
||||
.filter_map(|chunk| {
|
||||
let name = chunk.get(0)?;
|
||||
let name = chunk.first()?;
|
||||
let addr = chunk.get(1)?;
|
||||
Some((*name, *addr))
|
||||
})
|
||||
@@ -1444,7 +1449,7 @@ mod tests {
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::message::Message;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{self, TestContext};
|
||||
use crate::test_utils::{self, TestContext, TestContextManager};
|
||||
|
||||
#[test]
|
||||
fn test_contact_id_values() {
|
||||
@@ -2270,4 +2275,28 @@ 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(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,6 +23,7 @@ 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};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
@@ -51,7 +52,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: RwLock<HashMap<usize, String>>,
|
||||
pub(crate) translated_stockstrings: StockStrings,
|
||||
pub(crate) events: Events,
|
||||
|
||||
pub(crate) scheduler: RwLock<Option<Scheduler>>,
|
||||
@@ -119,8 +120,13 @@ 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) -> Result<Context> {
|
||||
let context = Self::new_closed(dbfile, id, events).await?;
|
||||
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?;
|
||||
|
||||
// Open the database if is not encrypted.
|
||||
if context.check_passphrase("".to_string()).await? {
|
||||
@@ -130,7 +136,12 @@ impl Context {
|
||||
}
|
||||
|
||||
/// Creates new context without opening the database.
|
||||
pub async fn new_closed(dbfile: &Path, id: u32, events: Events) -> Result<Context> {
|
||||
pub async fn new_closed(
|
||||
dbfile: &Path,
|
||||
id: u32,
|
||||
events: Events,
|
||||
stockstrings: StockStrings,
|
||||
) -> Result<Context> {
|
||||
let mut blob_fname = OsString::new();
|
||||
blob_fname.push(dbfile.file_name().unwrap_or_default());
|
||||
blob_fname.push("-blobs");
|
||||
@@ -138,7 +149,7 @@ impl Context {
|
||||
if !blobdir.exists() {
|
||||
tokio::fs::create_dir_all(&blobdir).await?;
|
||||
}
|
||||
let context = Context::with_blobdir(dbfile.into(), blobdir, id, events).await?;
|
||||
let context = Context::with_blobdir(dbfile.into(), blobdir, id, events, stockstrings)?;
|
||||
Ok(context)
|
||||
}
|
||||
|
||||
@@ -169,11 +180,12 @@ impl Context {
|
||||
self.sql.check_passphrase(passphrase).await
|
||||
}
|
||||
|
||||
pub(crate) async fn with_blobdir(
|
||||
pub(crate) fn with_blobdir(
|
||||
dbfile: PathBuf,
|
||||
blobdir: PathBuf,
|
||||
id: u32,
|
||||
events: Events,
|
||||
stockstrings: StockStrings,
|
||||
) -> Result<Context> {
|
||||
ensure!(
|
||||
blobdir.is_dir(),
|
||||
@@ -190,10 +202,10 @@ impl Context {
|
||||
generating_key_mutex: Mutex::new(()),
|
||||
oauth2_mutex: Mutex::new(()),
|
||||
wrong_pw_warning_mutex: Mutex::new(()),
|
||||
translated_stockstrings: RwLock::new(HashMap::new()),
|
||||
translated_stockstrings: stockstrings,
|
||||
events,
|
||||
scheduler: RwLock::new(None),
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 3.0)), // Allow to send 3 messages immediately, no more than once every 20 seconds.
|
||||
ratelimit: RwLock::new(Ratelimit::new(Duration::new(60, 0), 6.0)), // Allow to send 6 messages immediately, no more than once every 10 seconds.
|
||||
quota: RwLock::new(None),
|
||||
server_id: RwLock::new(None),
|
||||
creation_time: std::time::SystemTime::now(),
|
||||
@@ -239,6 +251,17 @@ 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.
|
||||
@@ -335,6 +358,7 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub(crate) async fn shall_stop_ongoing(&self) -> bool {
|
||||
match &*self.running_state.read().await {
|
||||
RunningState::Running { .. } => false,
|
||||
@@ -594,7 +618,7 @@ impl Context {
|
||||
|
||||
let list = if let Some(chat_id) = chat_id {
|
||||
do_query(
|
||||
"SELECT m.id AS id, m.timestamp AS timestamp
|
||||
"SELECT m.id AS id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
@@ -618,7 +642,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, m.timestamp AS timestamp
|
||||
"SELECT m.id AS id
|
||||
FROM msgs m
|
||||
LEFT JOIN contacts ct
|
||||
ON m.from_id=ct.id
|
||||
@@ -694,7 +718,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()).await?;
|
||||
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await?;
|
||||
|
||||
// Broken database is indistinguishable from encrypted one.
|
||||
assert_eq!(res.is_open().await, false);
|
||||
@@ -840,7 +864,9 @@ 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()).await.unwrap();
|
||||
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
let blobdir = tmp.path().join("db.sqlite-blobs");
|
||||
assert!(blobdir.is_dir());
|
||||
}
|
||||
@@ -851,7 +877,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()).await;
|
||||
let res = Context::new(&dbfile, 1, Events::new(), StockStrings::new()).await;
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -861,7 +887,9 @@ mod tests {
|
||||
let subdir = tmp.path().join("subdir");
|
||||
let dbfile = subdir.join("db.sqlite");
|
||||
let dbfile2 = dbfile.clone();
|
||||
Context::new(&dbfile, 1, Events::new()).await.unwrap();
|
||||
Context::new(&dbfile, 1, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(subdir.is_dir());
|
||||
assert!(dbfile2.is_file());
|
||||
}
|
||||
@@ -871,7 +899,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()).await;
|
||||
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new(), StockStrings::new());
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -880,7 +908,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()).await;
|
||||
let res = Context::with_blobdir(dbfile, blobdir, 1, Events::new(), StockStrings::new());
|
||||
assert!(res.is_err());
|
||||
}
|
||||
|
||||
@@ -1049,7 +1077,7 @@ mod tests {
|
||||
let dbfile = dir.path().join("db.sqlite");
|
||||
|
||||
let id = 1;
|
||||
let context = Context::new_closed(&dbfile, id, Events::new())
|
||||
let context = Context::new_closed(&dbfile, id, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.open("foo".to_string()).await?, true);
|
||||
@@ -1057,7 +1085,7 @@ mod tests {
|
||||
drop(context);
|
||||
|
||||
let id = 2;
|
||||
let context = Context::new(&dbfile, id, Events::new())
|
||||
let context = Context::new(&dbfile, id, Events::new(), StockStrings::new())
|
||||
.await
|
||||
.context("failed to create context")?;
|
||||
assert_eq!(context.is_open().await, false);
|
||||
|
||||
375
src/decrypt.rs
Normal file
375
src/decrypt.rs
Normal file
@@ -0,0 +1,375 @@
|
||||
//! End-to-end decryption support.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::{Context as _, Result};
|
||||
use mailparse::ParsedMail;
|
||||
|
||||
use crate::aheader::Aheader;
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::headerdef::HeaderDefMap;
|
||||
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 create_decryption_info(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
message_time: i64,
|
||||
) -> Result<DecryptionInfo> {
|
||||
let from = mail
|
||||
.headers
|
||||
.get_header(HeaderDef::From_)
|
||||
.and_then(|from_addr| mailparse::addrparse_header(from_addr).ok())
|
||||
.and_then(|from| from.extract_single_info())
|
||||
.map(|from| from.addr)
|
||||
.unwrap_or_default();
|
||||
|
||||
let autocrypt_header = Aheader::from_headers(&from, &mail.headers)
|
||||
.ok_or_log_msg(context, "Failed to parse Autocrypt header")
|
||||
.flatten();
|
||||
|
||||
let peerstate =
|
||||
get_autocrypt_peerstate(context, &from, autocrypt_header.as_ref(), message_time).await?;
|
||||
|
||||
Ok(DecryptionInfo {
|
||||
from,
|
||||
autocrypt_header,
|
||||
peerstate,
|
||||
message_time,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(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,
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Returns updated peerstate.
|
||||
pub(crate) async fn get_autocrypt_peerstate(
|
||||
context: &Context,
|
||||
from: &str,
|
||||
autocrypt_header: Option<&Aheader>,
|
||||
message_time: i64,
|
||||
) -> 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) {
|
||||
peerstate.apply_header(header, message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
}
|
||||
// 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(())
|
||||
}
|
||||
}
|
||||
155
src/download.rs
155
src/download.rs
@@ -11,7 +11,7 @@ use crate::imap::{Imap, ImapActionResult};
|
||||
use crate::job::{self, Action, Job, Status};
|
||||
use crate::message::{Message, MsgId, Viewtype};
|
||||
use crate::mimeparser::{MimeMessage, Part};
|
||||
use crate::param::Params;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::tools::time;
|
||||
use crate::{job_try, stock_str, EventType};
|
||||
use std::cmp::max;
|
||||
@@ -69,6 +69,42 @@ impl Context {
|
||||
Ok(Some(max(MIN_DOWNLOAD_LIMIT, download_limit as u32)))
|
||||
}
|
||||
}
|
||||
|
||||
// Merges the two messages to `placeholder_msg_id`;
|
||||
// `full_msg_id` is no longer used afterwards.
|
||||
pub(crate) async fn merge_messages(
|
||||
&self,
|
||||
full_msg_id: MsgId,
|
||||
placeholder_msg_id: MsgId,
|
||||
) -> Result<()> {
|
||||
let placeholder = Message::load_from_db(self, placeholder_msg_id).await?;
|
||||
self.sql
|
||||
.transaction(move |transaction| {
|
||||
transaction
|
||||
.execute("DELETE FROM msgs WHERE id=?;", paramsv![placeholder_msg_id])?;
|
||||
transaction.execute(
|
||||
"UPDATE msgs SET id=? WHERE id=?",
|
||||
paramsv![placeholder_msg_id, full_msg_id],
|
||||
)?;
|
||||
Ok(())
|
||||
})
|
||||
.await?;
|
||||
let mut full = Message::load_from_db(self, placeholder_msg_id).await?;
|
||||
|
||||
for key in [
|
||||
Param::WebxdcSummary,
|
||||
Param::WebxdcSummaryTimestamp,
|
||||
Param::WebxdcDocument,
|
||||
Param::WebxdcDocumentTimestamp,
|
||||
] {
|
||||
if let Some(value) = placeholder.param.get(key) {
|
||||
full.param.set(key, value);
|
||||
}
|
||||
}
|
||||
full.update_param(self).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl MsgId {
|
||||
@@ -256,7 +292,7 @@ impl MimeMessage {
|
||||
mod tests {
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::chat::send_msg;
|
||||
use crate::chat::{get_chat_msgs, send_msg};
|
||||
use crate::ephemeral::Timer;
|
||||
use crate::message::Viewtype;
|
||||
use crate::receive_imf::receive_imf_inner;
|
||||
@@ -410,4 +446,119 @@ 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_rfc742_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_rfc742_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_rfc742_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(())
|
||||
}
|
||||
}
|
||||
|
||||
359
src/e2ee.rs
359
src/e2ee.rs
@@ -1,20 +1,13 @@
|
||||
//! End-to-end encryption support.
|
||||
|
||||
use std::collections::HashSet;
|
||||
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use mailparse::ParsedMail;
|
||||
use num_traits::FromPrimitive;
|
||||
|
||||
use crate::aheader::{Aheader, EncryptPreference};
|
||||
use crate::config::Config;
|
||||
use crate::contact::addr_cmp;
|
||||
use crate::context::Context;
|
||||
use crate::headerdef::HeaderDef;
|
||||
use crate::headerdef::HeaderDefMap;
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey, SignedSecretKey};
|
||||
use crate::key::{DcKey, SignedPublicKey, SignedSecretKey};
|
||||
use crate::keyring::Keyring;
|
||||
use crate::log::LogExt;
|
||||
use crate::peerstate::{Peerstate, PeerstateVerifiedStatus};
|
||||
use crate::pgp;
|
||||
|
||||
@@ -133,299 +126,6 @@ impl EncryptHelper {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Returns updated peerstate.
|
||||
pub(crate) async fn get_autocrypt_peerstate(
|
||||
context: &Context,
|
||||
from: &str,
|
||||
autocrypt_header: Option<&Aheader>,
|
||||
message_time: i64,
|
||||
) -> Result<Option<Peerstate>> {
|
||||
let mut peerstate;
|
||||
|
||||
// Apply Autocrypt header
|
||||
if let Some(header) = autocrypt_header {
|
||||
// The "from_nongossiped_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)
|
||||
peerstate = Peerstate::from_nongossiped_fingerprint_or_addr(
|
||||
context,
|
||||
&header.public_key.fingerprint(),
|
||||
from,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let Some(ref mut peerstate) = peerstate {
|
||||
if addr_cmp(&peerstate.addr, from) {
|
||||
peerstate.apply_header(header, message_time);
|
||||
peerstate.save_to_db(&context.sql, false).await?;
|
||||
}
|
||||
// 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)
|
||||
}
|
||||
|
||||
/// 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 context = context;
|
||||
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 create_decryption_info(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
message_time: i64,
|
||||
) -> Result<DecryptionInfo> {
|
||||
let from = mail
|
||||
.headers
|
||||
.get_header(HeaderDef::From_)
|
||||
.and_then(|from_addr| mailparse::addrparse_header(from_addr).ok())
|
||||
.and_then(|from| from.extract_single_info())
|
||||
.map(|from| from.addr)
|
||||
.unwrap_or_default();
|
||||
|
||||
let autocrypt_header = Aheader::from_headers(&from, &mail.headers)
|
||||
.ok_or_log_msg(context, "Failed to parse Autocrypt header")
|
||||
.flatten();
|
||||
|
||||
let peerstate =
|
||||
get_autocrypt_peerstate(context, &from, autocrypt_header.as_ref(), message_time).await?;
|
||||
|
||||
Ok(DecryptionInfo {
|
||||
from,
|
||||
autocrypt_header,
|
||||
peerstate,
|
||||
message_time,
|
||||
})
|
||||
}
|
||||
|
||||
#[derive(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,
|
||||
}
|
||||
|
||||
/// 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 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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
|
||||
/// Ensures a private key exists for the configured user.
|
||||
///
|
||||
/// Normally the private key is generated when the first message is
|
||||
@@ -448,7 +148,6 @@ mod tests {
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::Param;
|
||||
use crate::peerstate::ToSave;
|
||||
use crate::receive_imf::receive_imf;
|
||||
use crate::test_utils::{bob_keypair, TestContext};
|
||||
|
||||
use super::*;
|
||||
@@ -496,24 +195,6 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
);
|
||||
}
|
||||
|
||||
#[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_encrypted_no_autocrypt() -> anyhow::Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
@@ -648,42 +329,4 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
assert!(encrypt_helper.should_encrypt(&t, true, &ps).is_err());
|
||||
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
|
||||
}
|
||||
|
||||
#[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(())
|
||||
}
|
||||
}
|
||||
|
||||
184
src/ephemeral.rs
184
src/ephemeral.rs
@@ -36,15 +36,21 @@
|
||||
//!
|
||||
//! ## How messages are deleted
|
||||
//!
|
||||
//! When the message is deleted locally, its contents is removed and
|
||||
//! it is moved to the trash chat. This database entry is then used to
|
||||
//! track the Message-ID and corresponding IMAP folder and UID until
|
||||
//! the message is deleted from the server. Vice versa, when device
|
||||
//! deletes the message from the server, it removes IMAP folder and
|
||||
//! UID information, but keeps the message contents. When database
|
||||
//! entry is both moved to trash chat and does not contain UID
|
||||
//! information, it is deleted from the database, leaving no trace of
|
||||
//! the message.
|
||||
//! When Delta Chat deletes the message locally, it moves the message
|
||||
//! to the trash chat and removes actual message contents. Messages in
|
||||
//! the trash chat are called "tombstones" and track the Message-ID to
|
||||
//! prevent accidental redownloading of the message from the server,
|
||||
//! e.g. in case of UID validity change.
|
||||
//!
|
||||
//! Vice versa, when Delta Chat deletes the message from the server,
|
||||
//! it removes IMAP folder and UID row from the `imap` table, but
|
||||
//! keeps the message in the `msgs` table.
|
||||
//!
|
||||
//! Delta Chat eventually removes tombstones from the `msgs` table,
|
||||
//! leaving no trace of the message, when it thinks there are no more
|
||||
//! copies of the message stored on the server, i.e. when there is no
|
||||
//! corresponding `imap` table entry. This is done in the
|
||||
//! `prune_tombstones()` procedure during housekeeping.
|
||||
//!
|
||||
//! ## When messages are deleted
|
||||
//!
|
||||
@@ -326,35 +332,35 @@ pub(crate) async fn start_ephemeral_timers_msgids(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Deletes messages which are expired according to
|
||||
/// Selects messages which are expired according to
|
||||
/// `delete_device_after` setting or `ephemeral_timestamp` column.
|
||||
///
|
||||
/// Returns true if any message is deleted, so caller can emit
|
||||
/// MsgsChanged event. If nothing has been deleted, returns
|
||||
/// false. This function does not emit the MsgsChanged event itself,
|
||||
/// because it is also called when chatlist is reloaded, and emitting
|
||||
/// MsgsChanged there will cause infinite reload loop.
|
||||
pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Result<()> {
|
||||
let mut updated = context
|
||||
/// For each message a row ID, chat id and viewtype is returned.
|
||||
async fn select_expired_messages(
|
||||
context: &Context,
|
||||
now: i64,
|
||||
) -> Result<Vec<(MsgId, ChatId, Viewtype)>> {
|
||||
let mut rows = context
|
||||
.sql
|
||||
.execute(
|
||||
// If you change which information is removed here, also change MsgId::trash() and
|
||||
// which information receive_imf::add_parts() still adds to the db if the chat_id is TRASH
|
||||
.query_map(
|
||||
r#"
|
||||
UPDATE msgs
|
||||
SET
|
||||
chat_id=?, txt='', subject='', txt_raw='',
|
||||
mime_headers='', from_id=0, to_id=0, param=''
|
||||
SELECT id, chat_id, type
|
||||
FROM msgs
|
||||
WHERE
|
||||
ephemeral_timestamp != 0
|
||||
AND ephemeral_timestamp <= ?
|
||||
AND chat_id != ?
|
||||
"#,
|
||||
paramsv![DC_CHAT_ID_TRASH, now, DC_CHAT_ID_TRASH],
|
||||
paramsv![now, DC_CHAT_ID_TRASH],
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let viewtype: Viewtype = row.get("type")?;
|
||||
Ok((id, chat_id, viewtype))
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await
|
||||
.context("update failed")?
|
||||
> 0;
|
||||
.await?;
|
||||
|
||||
if let Some(delete_device_after) = context.get_config_delete_device_after().await? {
|
||||
let self_chat_id = ChatId::lookup_by_contact(context, ContactId::SELF)
|
||||
@@ -366,36 +372,81 @@ WHERE
|
||||
|
||||
let threshold_timestamp = now.saturating_sub(delete_device_after);
|
||||
|
||||
// Delete expired messages
|
||||
//
|
||||
// Only update the rows that have to be updated, to avoid emitting
|
||||
// unnecessary "chat modified" events.
|
||||
let rows_modified = context
|
||||
let rows_expired = context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE msgs \
|
||||
SET chat_id = ?, txt = '', subject='', txt_raw='', \
|
||||
mime_headers='', from_id=0, to_id=0, param='' \
|
||||
WHERE timestamp < ? \
|
||||
AND chat_id > ? \
|
||||
AND chat_id != ? \
|
||||
AND chat_id != ?",
|
||||
.query_map(
|
||||
r#"
|
||||
SELECT id, chat_id, type
|
||||
FROM msgs
|
||||
WHERE
|
||||
timestamp < ?
|
||||
AND chat_id > ?
|
||||
AND chat_id != ?
|
||||
AND chat_id != ?
|
||||
"#,
|
||||
paramsv![
|
||||
DC_CHAT_ID_TRASH,
|
||||
threshold_timestamp,
|
||||
DC_CHAT_ID_LAST_SPECIAL,
|
||||
self_chat_id,
|
||||
device_chat_id
|
||||
],
|
||||
|row| {
|
||||
let id: MsgId = row.get("id")?;
|
||||
let chat_id: ChatId = row.get("chat_id")?;
|
||||
let viewtype: Viewtype = row.get("type")?;
|
||||
Ok((id, chat_id, viewtype))
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await
|
||||
.context("deleted update failed")?;
|
||||
.await?;
|
||||
|
||||
updated |= rows_modified > 0;
|
||||
rows.extend(rows_expired);
|
||||
}
|
||||
|
||||
if updated {
|
||||
context.emit_msgs_changed_without_ids();
|
||||
Ok(rows)
|
||||
}
|
||||
|
||||
/// Deletes messages which are expired according to
|
||||
/// `delete_device_after` setting or `ephemeral_timestamp` column.
|
||||
///
|
||||
/// Emits relevant `MsgsChanged` and `WebxdcInstanceDeleted` events
|
||||
/// if messages are deleted.
|
||||
pub(crate) async fn delete_expired_messages(context: &Context, now: i64) -> Result<()> {
|
||||
let rows = select_expired_messages(context, now).await?;
|
||||
|
||||
if !rows.is_empty() {
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
// If you change which information is removed here, also change MsgId::trash() and
|
||||
// which information receive_imf::add_parts() still adds to the db if the chat_id is TRASH
|
||||
&format!(
|
||||
r#"
|
||||
UPDATE msgs
|
||||
SET
|
||||
chat_id=?, txt='', subject='', txt_raw='',
|
||||
mime_headers='', from_id=0, to_id=0, param=''
|
||||
WHERE id IN ({})
|
||||
"#,
|
||||
sql::repeat_vars(rows.len())
|
||||
),
|
||||
rusqlite::params_from_iter(
|
||||
std::iter::once(&DC_CHAT_ID_TRASH as &dyn crate::ToSql).chain(
|
||||
rows.iter()
|
||||
.map(|(msg_id, _chat_id, _viewtype)| msg_id as &dyn crate::ToSql),
|
||||
),
|
||||
),
|
||||
)
|
||||
.await
|
||||
.context("update failed")?;
|
||||
|
||||
for (msg_id, chat_id, viewtype) in rows {
|
||||
context.emit_msgs_changed(chat_id, msg_id);
|
||||
|
||||
if viewtype == Viewtype::Webxdc {
|
||||
context.emit_event(EventType::WebxdcInstanceDeleted { msg_id });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -587,7 +638,7 @@ mod tests {
|
||||
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(&context, Timer::Disabled, ContactId::SELF).await,
|
||||
"Message deletion timer is disabled by me."
|
||||
"You disabled message deletion timer."
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
@@ -597,7 +648,7 @@ mod tests {
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1 s by me."
|
||||
"You set message deletion timer to 1 s."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
@@ -606,7 +657,7 @@ mod tests {
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 30 s by me."
|
||||
"You set message deletion timer to 30 s."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
@@ -615,7 +666,7 @@ mod tests {
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1 minute by me."
|
||||
"You set message deletion timer to 1 minute."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
@@ -624,7 +675,7 @@ mod tests {
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1.5 minutes by me."
|
||||
"You set message deletion timer to 1.5 minutes."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
@@ -633,7 +684,7 @@ mod tests {
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 30 minutes by me."
|
||||
"You set message deletion timer to 30 minutes."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
@@ -642,7 +693,7 @@ mod tests {
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1 hour by me."
|
||||
"You set message deletion timer to 1 hour."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
@@ -651,7 +702,7 @@ mod tests {
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1.5 hours by me."
|
||||
"You set message deletion timer to 1.5 hours."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
@@ -662,7 +713,7 @@ mod tests {
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 2 hours by me."
|
||||
"You set message deletion timer to 2 hours."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
@@ -673,7 +724,7 @@ mod tests {
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1 day by me."
|
||||
"You set message deletion timer to 1 day."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
@@ -684,7 +735,7 @@ mod tests {
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 2 days by me."
|
||||
"You set message deletion timer to 2 days."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
@@ -695,7 +746,7 @@ mod tests {
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 1 week by me."
|
||||
"You set message deletion timer to 1 week."
|
||||
);
|
||||
assert_eq!(
|
||||
stock_ephemeral_timer_changed(
|
||||
@@ -706,7 +757,7 @@ mod tests {
|
||||
ContactId::SELF
|
||||
)
|
||||
.await,
|
||||
"Message deletion timer is set to 4 weeks by me."
|
||||
"You set message deletion timer to 4 weeks."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -950,6 +1001,19 @@ mod tests {
|
||||
|
||||
assert!(next_expiration < deleted_at);
|
||||
delete_expired_messages(t, deleted_at).await?;
|
||||
t.evtracker
|
||||
.get_matching(|evt| {
|
||||
if let EventType::MsgsChanged {
|
||||
msg_id: event_msg_id,
|
||||
..
|
||||
} = evt
|
||||
{
|
||||
*event_msg_id == msg_id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
let loaded = Message::load_from_db(t, msg_id).await?;
|
||||
assert_eq!(loaded.text.unwrap(), "");
|
||||
|
||||
@@ -302,4 +302,9 @@ pub enum EventType {
|
||||
msg_id: MsgId,
|
||||
status_update_serial: StatusUpdateSerial,
|
||||
},
|
||||
|
||||
/// Inform that a message containing a webxdc instance has been deleted
|
||||
WebxdcInstanceDeleted {
|
||||
msg_id: MsgId,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ impl HtmlMsgParser {
|
||||
|
||||
if parser.html.is_empty() {
|
||||
if let Some(plain) = &parser.plain {
|
||||
parser.html = plain.to_html().await;
|
||||
parser.html = plain.to_html();
|
||||
}
|
||||
} else {
|
||||
parser.cid_to_data_recursive(context, &parsedmail).await?;
|
||||
|
||||
@@ -237,7 +237,7 @@ impl Imap {
|
||||
/// Creates new disconnected IMAP client using the specific login parameters.
|
||||
///
|
||||
/// `addr` is used to renew token if OAuth2 authentication is used.
|
||||
pub async fn new(
|
||||
pub fn new(
|
||||
lp: &ServerLoginParam,
|
||||
socks5_config: Option<Socks5Config>,
|
||||
addr: &str,
|
||||
@@ -303,8 +303,7 @@ impl Imap {
|
||||
provider.strict_tls
|
||||
}),
|
||||
idle_interrupt,
|
||||
)
|
||||
.await?;
|
||||
)?;
|
||||
Ok(imap)
|
||||
}
|
||||
|
||||
|
||||
36
src/imex.rs
36
src/imex.rs
@@ -134,19 +134,9 @@ pub async fn has_backup(_context: &Context, dir_name: &Path) -> Result<String> {
|
||||
}
|
||||
|
||||
/// Initiates key transfer via Autocrypt Setup Message.
|
||||
///
|
||||
/// Returns setup code.
|
||||
pub async fn initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
let cancel = context.alloc_ongoing().await?;
|
||||
let res = do_initiate_key_transfer(context)
|
||||
.race(cancel.recv().map(|_| Err(format_err!("canceled"))))
|
||||
.await;
|
||||
|
||||
context.free_ongoing().await;
|
||||
res
|
||||
}
|
||||
|
||||
async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
let setup_code = create_setup_code(context);
|
||||
/* this may require a keypair to be created. this may take a second ... */
|
||||
let setup_file_content = render_setup_file(context, &setup_code).await?;
|
||||
@@ -171,17 +161,7 @@ async fn do_initiate_key_transfer(context: &Context) -> Result<String> {
|
||||
msg.force_plaintext();
|
||||
msg.param.set_int(Param::SkipAutocrypt, 1);
|
||||
|
||||
let msg_id = chat::send_msg(context, chat_id, &mut msg).await?;
|
||||
info!(context, "Wait for setup message being sent ...",);
|
||||
while !context.shall_stop_ongoing().await {
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
if let Ok(msg) = Message::load_from_db(context, msg_id).await {
|
||||
if msg.is_sent() {
|
||||
info!(context, "... setup message sent.",);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
chat::send_msg(context, chat_id, &mut msg).await?;
|
||||
// no maybe_add_bcc_self_device_msg() here.
|
||||
// the ui shows the dialog with the setup code on this device,
|
||||
// it would be too much noise to have two things popping up at the same time.
|
||||
@@ -980,16 +960,10 @@ mod tests {
|
||||
async fn test_key_transfer() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let alice_clone = alice.clone();
|
||||
let key_transfer_task = tokio::task::spawn(async move {
|
||||
let ctx = alice_clone;
|
||||
initiate_key_transfer(&ctx).await
|
||||
});
|
||||
let setup_code = initiate_key_transfer(&alice).await?;
|
||||
|
||||
// Wait for the message to be added to the queue.
|
||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
||||
// Get Autocrypt Setup Message.
|
||||
let sent = alice.pop_sent_msg().await;
|
||||
let setup_code = key_transfer_task.await??;
|
||||
|
||||
// Alice sets up a second device.
|
||||
let alice2 = TestContext::new().await;
|
||||
|
||||
@@ -69,7 +69,7 @@ pub enum Action {
|
||||
ResyncFolders = 300,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Job {
|
||||
pub job_id: u32,
|
||||
pub action: Action,
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
)]
|
||||
#![allow(
|
||||
clippy::match_bool,
|
||||
clippy::eval_order_dependence,
|
||||
clippy::mixed_read_write_in_expression,
|
||||
clippy::bool_assert_comparison,
|
||||
clippy::manual_split_once,
|
||||
clippy::format_push_string
|
||||
@@ -58,6 +58,7 @@ mod configure;
|
||||
pub mod constants;
|
||||
pub mod contact;
|
||||
pub mod context;
|
||||
mod decrypt;
|
||||
pub mod download;
|
||||
mod e2ee;
|
||||
pub mod ephemeral;
|
||||
|
||||
@@ -15,7 +15,7 @@ use crate::constants::{DC_LP_AUTH_FLAGS, DC_LP_AUTH_NORMAL, DC_LP_AUTH_OAUTH2};
|
||||
use crate::provider::{get_provider_by_id, Provider};
|
||||
use crate::{context::Context, provider::Socket};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Display, FromPrimitive, PartialEq, Eq)]
|
||||
#[derive(Copy, Clone, Debug, Display, FromPrimitive, ToPrimitive, PartialEq, Eq)]
|
||||
#[repr(u32)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum CertificateChecks {
|
||||
@@ -39,7 +39,7 @@ impl Default for CertificateChecks {
|
||||
}
|
||||
|
||||
/// Login parameters for a single server, either IMAP or SMTP
|
||||
#[derive(Default, Debug, Clone, PartialEq)]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct ServerLoginParam {
|
||||
pub server: String,
|
||||
pub user: String,
|
||||
@@ -53,7 +53,7 @@ pub struct ServerLoginParam {
|
||||
pub certificate_checks: CertificateChecks,
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq)]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Socks5Config {
|
||||
pub host: String,
|
||||
pub port: u16,
|
||||
@@ -128,7 +128,7 @@ impl fmt::Display for Socks5Config {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone, PartialEq)]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
pub struct LoginParam {
|
||||
pub addr: String,
|
||||
pub imap: ServerLoginParam,
|
||||
|
||||
@@ -19,7 +19,7 @@ use crate::download::DownloadState;
|
||||
use crate::ephemeral::{start_ephemeral_timers_msgids, Timer as EphemeralTimer};
|
||||
use crate::events::EventType;
|
||||
use crate::imap::markseen_on_imap_table;
|
||||
use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage};
|
||||
use crate::mimeparser::{parse_message_id, DeliveryReport, SystemMessage};
|
||||
use crate::param::{Param, Params};
|
||||
use crate::pgp::split_armored_data;
|
||||
use crate::scheduler::InterruptInfo;
|
||||
@@ -1236,6 +1236,11 @@ pub async fn delete_msgs(context: &Context, msg_ids: &[MsgId]) -> Result<()> {
|
||||
.trash(context)
|
||||
.await
|
||||
.with_context(|| format!("Unable to trash message {}", msg_id))?;
|
||||
|
||||
if msg.viewtype == Viewtype::Webxdc {
|
||||
context.emit_event(EventType::WebxdcInstanceDeleted { msg_id: *msg_id });
|
||||
}
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
@@ -1532,7 +1537,7 @@ pub async fn handle_mdn(
|
||||
/// Where appropriate, also adds an info message telling the user which of the recipients of a group message failed.
|
||||
pub(crate) async fn handle_ndn(
|
||||
context: &Context,
|
||||
failed: &FailureReport,
|
||||
failed: &DeliveryReport,
|
||||
error: Option<String>,
|
||||
) -> Result<()> {
|
||||
if failed.rfc724_mid.is_empty() {
|
||||
@@ -1588,7 +1593,7 @@ pub(crate) async fn handle_ndn(
|
||||
|
||||
async fn ndn_maybe_add_info_msg(
|
||||
context: &Context,
|
||||
failed: &FailureReport,
|
||||
failed: &DeliveryReport,
|
||||
chat_id: ChatId,
|
||||
chat_type: Chattype,
|
||||
) -> Result<()> {
|
||||
|
||||
@@ -445,6 +445,8 @@ impl<'a> MimeFactory<'a> {
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Consumes a `MimeFactory` and renders it into a message which is then stored in
|
||||
/// `smtp`-table to be used by the SMTP loop
|
||||
pub async fn render(mut self, context: &Context) -> Result<RenderedEmail> {
|
||||
let mut headers: MessageHeaders = Default::default();
|
||||
|
||||
@@ -1175,17 +1177,17 @@ impl<'a> MimeFactory<'a> {
|
||||
if command == SystemMessage::MultiDeviceSync && self.is_e2ee_guaranteed() {
|
||||
let json = self.msg.param.get(Param::Arg).unwrap_or_default();
|
||||
let ids = self.msg.param.get(Param::Arg2).unwrap_or_default();
|
||||
parts.push(context.build_sync_part(json.to_string()).await);
|
||||
parts.push(context.build_sync_part(json.to_string()));
|
||||
self.sync_ids_to_delete = Some(ids.to_string());
|
||||
} else if command == SystemMessage::WebxdcStatusUpdate {
|
||||
let json = self.msg.param.get(Param::Arg).unwrap_or_default();
|
||||
parts.push(context.build_status_update_part(json).await);
|
||||
parts.push(context.build_status_update_part(json));
|
||||
} else if self.msg.viewtype == Viewtype::Webxdc {
|
||||
if let Some(json) = context
|
||||
.render_webxdc_status_update_object(self.msg.id, None)
|
||||
.await?
|
||||
{
|
||||
parts.push(context.build_status_update_part(&json).await);
|
||||
parts.push(context.build_status_update_part(&json));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -12,11 +12,11 @@ use once_cell::sync::Lazy;
|
||||
|
||||
use crate::aheader::Aheader;
|
||||
use crate::blob::BlobObject;
|
||||
use crate::constants::{DC_DESIRED_TEXT_LEN, DC_ELLIPSIS};
|
||||
use crate::constants::{DC_DESIRED_TEXT_LINES, DC_DESIRED_TEXT_LINE_LEN};
|
||||
use crate::contact::{addr_cmp, addr_normalize, ContactId};
|
||||
use crate::context::Context;
|
||||
use crate::decrypt::{create_decryption_info, try_decrypt};
|
||||
use crate::dehtml::dehtml;
|
||||
use crate::e2ee;
|
||||
use crate::events::EventType;
|
||||
use crate::format_flowed::unformat_flowed;
|
||||
use crate::headerdef::{HeaderDef, HeaderDefMap};
|
||||
@@ -28,7 +28,7 @@ use crate::peerstate::Peerstate;
|
||||
use crate::simplify::{simplify, SimplifiedText};
|
||||
use crate::stock_str;
|
||||
use crate::sync::SyncItems;
|
||||
use crate::tools::{get_filemeta, parse_receive_headers, truncate};
|
||||
use crate::tools::{get_filemeta, parse_receive_headers, truncate_by_lines};
|
||||
|
||||
/// A parsed MIME message.
|
||||
///
|
||||
@@ -73,7 +73,7 @@ pub struct MimeMessage {
|
||||
pub(crate) user_avatar: Option<AvatarAction>,
|
||||
pub(crate) group_avatar: Option<AvatarAction>,
|
||||
pub(crate) mdn_reports: Vec<Report>,
|
||||
pub(crate) failure_report: Option<FailureReport>,
|
||||
pub(crate) delivery_report: Option<DeliveryReport>,
|
||||
|
||||
/// Standard USENET signature, if any.
|
||||
pub(crate) footer: Option<String>,
|
||||
@@ -220,12 +220,11 @@ impl MimeMessage {
|
||||
let mut mail_raw = Vec::new();
|
||||
let mut gossiped_addr = Default::default();
|
||||
let mut from_is_signed = false;
|
||||
let mut decryption_info =
|
||||
e2ee::create_decryption_info(context, &mail, message_time).await?;
|
||||
let mut decryption_info = create_decryption_info(context, &mail, message_time).await?;
|
||||
|
||||
// `signatures` is non-empty exactly if the message was encrypted and correctly signed.
|
||||
let (mail, signatures, warn_empty_signature) =
|
||||
match e2ee::try_decrypt(context, &mail, &decryption_info).await {
|
||||
match try_decrypt(context, &mail, &decryption_info).await {
|
||||
Ok(Some((raw, signatures))) => {
|
||||
// Encrypted, but maybe unsigned message. Only if
|
||||
// `signatures` set is non-empty, it is a valid
|
||||
@@ -332,7 +331,7 @@ impl MimeMessage {
|
||||
webxdc_status_update: None,
|
||||
user_avatar: None,
|
||||
group_avatar: None,
|
||||
failure_report: None,
|
||||
delivery_report: None,
|
||||
footer: None,
|
||||
is_mime_modified: false,
|
||||
decoded_data: Vec::new(),
|
||||
@@ -393,15 +392,10 @@ impl MimeMessage {
|
||||
/// Parses system messages.
|
||||
fn parse_system_message_headers(&mut self, context: &Context) {
|
||||
if self.get_header(HeaderDef::AutocryptSetupMessage).is_some() {
|
||||
self.parts = self
|
||||
.parts
|
||||
.iter()
|
||||
.filter(|part| {
|
||||
part.mimetype.is_none()
|
||||
|| part.mimetype.as_ref().unwrap().as_ref() == MIME_AC_SETUP_FILE
|
||||
})
|
||||
.cloned()
|
||||
.collect();
|
||||
self.parts.retain(|part| {
|
||||
part.mimetype.is_none()
|
||||
|| part.mimetype.as_ref().unwrap().as_ref() == MIME_AC_SETUP_FILE
|
||||
});
|
||||
|
||||
if self.parts.len() == 1 {
|
||||
self.is_system_message = SystemMessage::AutocryptSetupMessage;
|
||||
@@ -417,6 +411,8 @@ impl MimeMessage {
|
||||
self.is_system_message = SystemMessage::ChatProtectionEnabled;
|
||||
} else if value == "protection-disabled" {
|
||||
self.is_system_message = SystemMessage::ChatProtectionDisabled;
|
||||
} else if value == "group-avatar-changed" {
|
||||
self.is_system_message = SystemMessage::GroupImageChanged;
|
||||
}
|
||||
} else if self.get_header(HeaderDef::ChatGroupMemberRemoved).is_some() {
|
||||
self.is_system_message = SystemMessage::MemberRemovedFromGroup;
|
||||
@@ -424,10 +420,6 @@ impl MimeMessage {
|
||||
self.is_system_message = SystemMessage::MemberAddedToGroup;
|
||||
} else if self.get_header(HeaderDef::ChatGroupNameChanged).is_some() {
|
||||
self.is_system_message = SystemMessage::GroupNameChanged;
|
||||
} else if let Some(value) = self.get_header(HeaderDef::ChatContent) {
|
||||
if value == "group-avatar-changed" {
|
||||
self.is_system_message = SystemMessage::GroupImageChanged;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -535,7 +527,9 @@ impl MimeMessage {
|
||||
self.parse_system_message_headers(context);
|
||||
self.parse_avatar_headers(context).await;
|
||||
self.parse_videochat_headers();
|
||||
self.squash_attachment_parts();
|
||||
if self.delivery_report.is_none() {
|
||||
self.squash_attachment_parts();
|
||||
}
|
||||
|
||||
if let Some(ref subject) = self.get_subject() {
|
||||
let mut prepend_subject = true;
|
||||
@@ -552,7 +546,7 @@ impl MimeMessage {
|
||||
|
||||
// For mailing lists, always add the subject because sometimes there are different topics
|
||||
// and otherwise it might be hard to keep track:
|
||||
if self.is_mailinglist_message() {
|
||||
if self.is_mailinglist_message() && !self.has_chat_version() {
|
||||
prepend_subject = true;
|
||||
}
|
||||
|
||||
@@ -747,7 +741,7 @@ impl MimeMessage {
|
||||
MimeS::Single
|
||||
}
|
||||
} else if mimetype.starts_with("message") {
|
||||
if mimetype == "message/rfc822" {
|
||||
if mimetype == "message/rfc822" && !is_attachment_disposition(mail) {
|
||||
MimeS::Message
|
||||
} else {
|
||||
MimeS::Single
|
||||
@@ -867,7 +861,7 @@ impl MimeMessage {
|
||||
// Some providers, e.g. Tiscali, forget to set the report-type. So, if it's None, assume that it might be delivery-status
|
||||
Some("delivery-status") | None => {
|
||||
if let Some(report) = self.process_delivery_status(context, mail)? {
|
||||
self.failure_report = Some(report);
|
||||
self.delivery_report = Some(report);
|
||||
}
|
||||
|
||||
// Add all parts (we need another part, preferably text/plain, to show as an error message)
|
||||
@@ -1013,14 +1007,15 @@ impl MimeMessage {
|
||||
(simplified_txt, top_quote)
|
||||
};
|
||||
|
||||
let simplified_txt = if simplified_txt.chars().count()
|
||||
> DC_DESIRED_TEXT_LEN + DC_ELLIPSIS.len()
|
||||
{
|
||||
self.is_mime_modified = true;
|
||||
truncate(&*simplified_txt, DC_DESIRED_TEXT_LEN).to_string()
|
||||
} else {
|
||||
simplified_txt
|
||||
};
|
||||
// Truncate text if it has too many lines
|
||||
let (simplified_txt, was_truncated) = truncate_by_lines(
|
||||
simplified_txt,
|
||||
DC_DESIRED_TEXT_LINES,
|
||||
DC_DESIRED_TEXT_LINE_LEN,
|
||||
);
|
||||
if was_truncated {
|
||||
self.is_mime_modified = was_truncated;
|
||||
}
|
||||
|
||||
if !simplified_txt.is_empty() || simplified_quote.is_some() {
|
||||
let mut part = Part {
|
||||
@@ -1278,9 +1273,46 @@ impl MimeMessage {
|
||||
&self,
|
||||
context: &Context,
|
||||
report: &mailparse::ParsedMail<'_>,
|
||||
) -> Result<Option<FailureReport>> {
|
||||
) -> Result<Option<DeliveryReport>> {
|
||||
// Assume failure.
|
||||
let mut failure = true;
|
||||
|
||||
if let Some(status_part) = report.subparts.get(1) {
|
||||
// RFC 3464 defines `message/delivery-status`
|
||||
// RFC 6533 defines `message/global-delivery-status`
|
||||
if status_part.ctype.mimetype != "message/delivery-status"
|
||||
&& status_part.ctype.mimetype != "message/global-delivery-status"
|
||||
{
|
||||
warn!(context, "Second part of Delivery Status Notification is not message/delivery-status or message/global-delivery-status, ignoring");
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let status_body = status_part.get_body_raw()?;
|
||||
|
||||
// Skip per-message fields.
|
||||
let (_, sz) = mailparse::parse_headers(&status_body)?;
|
||||
|
||||
// Parse first set of per-recipient fields
|
||||
if let Some(status_body) = status_body.get(sz..) {
|
||||
let (status_fields, _) = mailparse::parse_headers(status_body)?;
|
||||
if let Some(action) = status_fields.get_first_value("action") {
|
||||
if action != "failed" {
|
||||
info!(context, "DSN with {:?} action", action);
|
||||
failure = false;
|
||||
}
|
||||
} else {
|
||||
warn!(context, "DSN without action");
|
||||
}
|
||||
} else {
|
||||
warn!(context, "DSN without per-recipient fields");
|
||||
}
|
||||
} else {
|
||||
// No message/delivery-status part.
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
// parse as mailheaders
|
||||
if let Some(original_msg) = report.subparts.iter().find(|p| {
|
||||
if let Some(original_msg) = report.subparts.get(2).filter(|p| {
|
||||
p.ctype.mimetype.contains("rfc822")
|
||||
|| p.ctype.mimetype == "message/global"
|
||||
|| p.ctype.mimetype == "message/global-headers"
|
||||
@@ -1301,9 +1333,10 @@ impl MimeMessage {
|
||||
None // We do not know which recipient failed
|
||||
};
|
||||
|
||||
return Ok(Some(FailureReport {
|
||||
return Ok(Some(DeliveryReport {
|
||||
rfc724_mid: original_message_id,
|
||||
failed_recipient: to.map(|s| s.addr),
|
||||
failure,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -1384,7 +1417,7 @@ impl MimeMessage {
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if maybe_ndn && self.failure_report.is_none() {
|
||||
if maybe_ndn && self.delivery_report.is_none() {
|
||||
static RE: Lazy<regex::Regex> =
|
||||
Lazy::new(|| regex::Regex::new(r"Message-ID:(.*)").unwrap());
|
||||
for captures in self
|
||||
@@ -1398,9 +1431,10 @@ impl MimeMessage {
|
||||
if let Ok(Some(_)) =
|
||||
message::rfc724_mid_exists(context, &original_message_id).await
|
||||
{
|
||||
self.failure_report = Some(FailureReport {
|
||||
self.delivery_report = Some(DeliveryReport {
|
||||
rfc724_mid: original_message_id,
|
||||
failed_recipient: None,
|
||||
failure: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1438,13 +1472,15 @@ impl MimeMessage {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(failure_report) = &self.failure_report {
|
||||
let error = parts
|
||||
.iter()
|
||||
.find(|p| p.typ == Viewtype::Text)
|
||||
.map(|p| p.msg.clone());
|
||||
if let Err(e) = message::handle_ndn(context, failure_report, error).await {
|
||||
warn!(context, "Could not handle ndn: {}", e);
|
||||
if let Some(delivery_report) = &self.delivery_report {
|
||||
if delivery_report.failure {
|
||||
let error = parts
|
||||
.iter()
|
||||
.find(|p| p.typ == Viewtype::Text)
|
||||
.map(|p| p.msg.clone());
|
||||
if let Err(e) = message::handle_ndn(context, delivery_report, error).await {
|
||||
warn!(context, "Could not handle ndn: {}", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1524,6 +1560,7 @@ async fn update_gossip_peerstates(
|
||||
Ok(gossiped_addr)
|
||||
}
|
||||
|
||||
/// Message Disposition Notification (RFC 8098)
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct Report {
|
||||
/// Original-Message-ID header
|
||||
@@ -1535,10 +1572,12 @@ pub(crate) struct Report {
|
||||
additional_message_ids: Vec<String>,
|
||||
}
|
||||
|
||||
/// Delivery Status Notification (RFC 3464, RFC 6533)
|
||||
#[derive(Debug)]
|
||||
pub(crate) struct FailureReport {
|
||||
pub(crate) struct DeliveryReport {
|
||||
pub rfc724_mid: String,
|
||||
pub failed_recipient: Option<String>,
|
||||
pub failure: bool,
|
||||
}
|
||||
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
@@ -1631,14 +1670,18 @@ fn get_mime_type(mail: &mailparse::ParsedMail<'_>) -> Result<(Mime, Viewtype)> {
|
||||
mime::VIDEO => Viewtype::Video,
|
||||
mime::MULTIPART => Viewtype::Unknown,
|
||||
mime::MESSAGE => {
|
||||
// Enacapsulated messages, see <https://www.w3.org/Protocols/rfc1341/7_3_Message.html>
|
||||
// Also used as part "message/disposition-notification" of "multipart/report", which, however, will
|
||||
// be handled separatedly.
|
||||
// I've not seen any messages using this, so we do not attach these parts (maybe they're used to attach replies,
|
||||
// which are unwanted at all).
|
||||
// For now, we skip these parts at all; if desired, we could return DcMimeType::File/DC_MSG_File
|
||||
// for selected and known subparts.
|
||||
Viewtype::Unknown
|
||||
if is_attachment_disposition(mail) {
|
||||
Viewtype::File
|
||||
} else {
|
||||
// Enacapsulated messages, see <https://www.w3.org/Protocols/rfc1341/7_3_Message.html>
|
||||
// Also used as part "message/disposition-notification" of "multipart/report", which, however, will
|
||||
// be handled separatedly.
|
||||
// I've not seen any messages using this, so we do not attach these parts (maybe they're used to attach replies,
|
||||
// which are unwanted at all).
|
||||
// For now, we skip these parts at all; if desired, we could return DcMimeType::File/DC_MSG_File
|
||||
// for selected and known subparts.
|
||||
Viewtype::Unknown
|
||||
}
|
||||
}
|
||||
mime::APPLICATION => Viewtype::File,
|
||||
_ => Viewtype::Unknown,
|
||||
@@ -1770,7 +1813,7 @@ mod tests {
|
||||
use crate::{
|
||||
chatlist::Chatlist,
|
||||
config::Config,
|
||||
constants::Blocked,
|
||||
constants::{Blocked, DC_DESIRED_TEXT_LEN, DC_ELLIPSIS},
|
||||
message::{Message, MessageState, MessengerMessage},
|
||||
receive_imf::receive_imf,
|
||||
test_utils::TestContext,
|
||||
@@ -3256,4 +3299,34 @@ Message.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn test_receive_eml() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
let mime_message = MimeMessage::from_bytes(
|
||||
&alice,
|
||||
include_bytes!("../test-data/message/attached-eml.eml"),
|
||||
)
|
||||
.await?;
|
||||
|
||||
assert_eq!(mime_message.parts.len(), 1);
|
||||
assert_eq!(mime_message.parts[0].typ, Viewtype::File);
|
||||
assert_eq!(
|
||||
mime_message.parts[0].mimetype,
|
||||
Some("message/rfc822".parse().unwrap(),)
|
||||
);
|
||||
assert_eq!(
|
||||
mime_message.parts[0].msg,
|
||||
"this is a classic email – I attached the .EML file".to_string()
|
||||
);
|
||||
assert_eq!(
|
||||
mime_message.parts[0].param.get(Param::File),
|
||||
Some("$BLOBDIR/.eml")
|
||||
);
|
||||
|
||||
assert_eq!(mime_message.parts[0].org_filename, Some(".eml".to_string()));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +163,7 @@ pub enum Param {
|
||||
/// For Chats: timestamp of group name update.
|
||||
GroupNameTimestamp = b'g',
|
||||
|
||||
/// For Chats: timestamp of group name update.
|
||||
/// For Chats: timestamp of member list update.
|
||||
MemberListTimestamp = b'k',
|
||||
|
||||
/// For Chats: timestamp of protection settings update.
|
||||
|
||||
@@ -9,6 +9,7 @@ use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::contact::{addr_cmp, Contact, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::decrypt::DecryptionInfo;
|
||||
use crate::events::EventType;
|
||||
use crate::key::{DcKey, Fingerprint, SignedPublicKey};
|
||||
use crate::message::Message;
|
||||
@@ -165,7 +166,7 @@ impl Peerstate {
|
||||
Self::from_stmt(context, query, paramsv![fp, fp, fp]).await
|
||||
}
|
||||
|
||||
pub async fn from_nongossiped_fingerprint_or_addr(
|
||||
pub async fn from_verified_fingerprint_or_addr(
|
||||
context: &Context,
|
||||
fingerprint: &Fingerprint,
|
||||
addr: &str,
|
||||
@@ -174,9 +175,9 @@ impl Peerstate {
|
||||
gossip_timestamp, gossip_key, public_key_fingerprint, gossip_key_fingerprint, \
|
||||
verified_key, verified_key_fingerprint \
|
||||
FROM acpeerstates \
|
||||
WHERE public_key_fingerprint=? \
|
||||
WHERE verified_key_fingerprint=? \
|
||||
OR addr=? COLLATE NOCASE \
|
||||
ORDER BY public_key_fingerprint=? DESC, last_seen DESC LIMIT 1;";
|
||||
ORDER BY verified_key_fingerprint=? DESC, last_seen DESC LIMIT 1;";
|
||||
let fp = fingerprint.hex();
|
||||
Self::from_stmt(context, query, paramsv![fp, addr, fp]).await
|
||||
}
|
||||
@@ -516,22 +517,22 @@ impl Peerstate {
|
||||
.with_context(|| format!("contact with peerstate.addr {:?} not found", &self.addr))?;
|
||||
|
||||
let chats = Chatlist::try_load(context, 0, None, Some(contact_id)).await?;
|
||||
let msg = match &change {
|
||||
PeerstateChange::FingerprintChange => {
|
||||
stock_str::contact_setup_changed(context, self.addr.clone()).await
|
||||
}
|
||||
PeerstateChange::Aeap(new_addr) => {
|
||||
let old_contact = Contact::load_from_db(context, contact_id).await?;
|
||||
stock_str::aeap_addr_changed(
|
||||
context,
|
||||
old_contact.get_display_name(),
|
||||
&self.addr,
|
||||
new_addr,
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
for (chat_id, msg_id) in chats.iter() {
|
||||
let msg = match &change {
|
||||
PeerstateChange::FingerprintChange => {
|
||||
stock_str::contact_setup_changed(context, self.addr.clone()).await
|
||||
}
|
||||
PeerstateChange::Aeap(new_addr) => {
|
||||
let old_contact = Contact::load_from_db(context, contact_id).await?;
|
||||
stock_str::aeap_addr_changed(
|
||||
context,
|
||||
old_contact.get_display_name(),
|
||||
&self.addr,
|
||||
new_addr,
|
||||
)
|
||||
.await
|
||||
}
|
||||
};
|
||||
let timestamp_sort = if let Some(msg_id) = msg_id {
|
||||
let lastmsg = Message::load_from_db(context, *msg_id).await?;
|
||||
lastmsg.timestamp_sort
|
||||
@@ -545,21 +546,23 @@ impl Peerstate {
|
||||
.await?
|
||||
.unwrap_or(0)
|
||||
};
|
||||
chat::add_info_msg_with_cmd(
|
||||
context,
|
||||
*chat_id,
|
||||
&msg,
|
||||
SystemMessage::Unknown,
|
||||
timestamp_sort,
|
||||
Some(timestamp),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
|
||||
if let PeerstateChange::Aeap(new_addr) = &change {
|
||||
let chat = Chat::load_from_db(context, *chat_id).await?;
|
||||
if chat.typ == Chattype::Group || chat.typ == Chattype::Broadcast {
|
||||
|
||||
if chat.typ == Chattype::Group && !chat.is_protected() {
|
||||
// Don't add an info_msg to the group, in order not to make the user think
|
||||
// that the address was automatically replaced in the group.
|
||||
continue;
|
||||
}
|
||||
|
||||
// For security reasons, for now, we only do the AEAP transition if the fingerprint
|
||||
// is verified (that's what from_verified_fingerprint_or_addr() does).
|
||||
// In order to not have inconsistent group membership state, we then only do the
|
||||
// transition in verified groups and in broadcast lists.
|
||||
if (chat.typ == Chattype::Group && chat.is_protected())
|
||||
|| chat.typ == Chattype::Broadcast
|
||||
{
|
||||
chat::remove_from_chat_contacts_table(context, *chat_id, contact_id).await?;
|
||||
|
||||
let (new_contact_id, _) =
|
||||
@@ -572,6 +575,18 @@ impl Peerstate {
|
||||
context.emit_event(EventType::ChatModified(*chat_id));
|
||||
}
|
||||
}
|
||||
|
||||
chat::add_info_msg_with_cmd(
|
||||
context,
|
||||
*chat_id,
|
||||
&msg,
|
||||
SystemMessage::Unknown,
|
||||
timestamp_sort,
|
||||
Some(timestamp),
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
@@ -597,7 +612,7 @@ impl Peerstate {
|
||||
/// In `drafts/aeap_mvp.md` there is a "big picture" overview over AEAP.
|
||||
pub async fn maybe_do_aeap_transition(
|
||||
context: &Context,
|
||||
info: &mut crate::e2ee::DecryptionInfo,
|
||||
info: &mut DecryptionInfo,
|
||||
mime_parser: &crate::mimeparser::MimeMessage,
|
||||
) -> Result<()> {
|
||||
if let Some(peerstate) = &mut info.peerstate {
|
||||
@@ -621,7 +636,7 @@ pub async fn maybe_do_aeap_transition(
|
||||
&& mime_parser.from_is_signed
|
||||
&& info.message_time > peerstate.last_seen
|
||||
{
|
||||
// Add an info messages to all chats with this contact
|
||||
// Add info messages to chats with this (verified) contact
|
||||
//
|
||||
peerstate
|
||||
.handle_setup_change(
|
||||
|
||||
@@ -21,7 +21,7 @@ pub struct PlainText {
|
||||
impl PlainText {
|
||||
/// Convert plain text to HTML.
|
||||
/// The function handles quotes, links, fixed and floating text paragraphs.
|
||||
pub async fn to_html(&self) -> String {
|
||||
pub fn to_html(&self) -> String {
|
||||
static LINKIFY_MAIL_RE: Lazy<regex::Regex> =
|
||||
Lazy::new(|| regex::Regex::new(r#"\b([\w.\-+]+@[\w.\-]+)\b"#).unwrap());
|
||||
|
||||
@@ -111,8 +111,7 @@ http://link-at-start-of-line.org
|
||||
flowed: false,
|
||||
delsp: false,
|
||||
}
|
||||
.to_html()
|
||||
.await;
|
||||
.to_html();
|
||||
assert_eq!(
|
||||
html,
|
||||
r##"<!DOCTYPE html>
|
||||
@@ -134,8 +133,7 @@ line with <a href="https://link-mid-of-line.org">https://link-mid-of-line.org</a
|
||||
flowed: false,
|
||||
delsp: false,
|
||||
}
|
||||
.to_html()
|
||||
.await;
|
||||
.to_html();
|
||||
assert_eq!(
|
||||
html,
|
||||
r#"<!DOCTYPE html>
|
||||
@@ -153,8 +151,7 @@ line with <<a href="http://encapsulated.link/?foo=_bar">http://encapsulated.l
|
||||
flowed: false,
|
||||
delsp: false,
|
||||
}
|
||||
.to_html()
|
||||
.await;
|
||||
.to_html();
|
||||
assert_eq!(
|
||||
html,
|
||||
r#"<!DOCTYPE html>
|
||||
@@ -172,8 +169,7 @@ line with nohttp://no.link here<br/>
|
||||
flowed: false,
|
||||
delsp: false,
|
||||
}
|
||||
.to_html()
|
||||
.await;
|
||||
.to_html();
|
||||
assert_eq!(
|
||||
html,
|
||||
r#"<!DOCTYPE html>
|
||||
@@ -191,8 +187,7 @@ just an address: <a href="mailto:foo@bar.org">foo@bar.org</a> <a href="mailto:an
|
||||
flowed: true,
|
||||
delsp: false,
|
||||
}
|
||||
.to_html()
|
||||
.await;
|
||||
.to_html();
|
||||
assert_eq!(
|
||||
html,
|
||||
r#"<!DOCTYPE html>
|
||||
@@ -213,8 +208,7 @@ line still line<br/>
|
||||
flowed: true,
|
||||
delsp: true,
|
||||
}
|
||||
.to_html()
|
||||
.await;
|
||||
.to_html();
|
||||
assert_eq!(
|
||||
html,
|
||||
r#"<!DOCTYPE html>
|
||||
@@ -235,8 +229,7 @@ linestill line<br/>
|
||||
flowed: false,
|
||||
delsp: false,
|
||||
}
|
||||
.to_html()
|
||||
.await;
|
||||
.to_html();
|
||||
assert_eq!(
|
||||
html,
|
||||
r#"<!DOCTYPE html>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user