mirror of
https://github.com/chatmail/core.git
synced 2026-04-10 09:32:11 +03:00
Compare commits
32 Commits
1.80.0
...
fix_node_p
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
008fd1cf5f | ||
|
|
33b10fa719 | ||
|
|
6d189dab98 | ||
|
|
380d7e66b5 | ||
|
|
eb80fa4eb0 | ||
|
|
88b470e8e5 | ||
|
|
99f8785475 | ||
|
|
47db1349a9 | ||
|
|
1f8b04bd42 | ||
|
|
d790a5fba9 | ||
|
|
2fc0a0964b | ||
|
|
715664273b | ||
|
|
a9f707c398 | ||
|
|
8168bcece5 | ||
|
|
c9beaa2aa1 | ||
|
|
b238c7e743 | ||
|
|
e9511ebfc3 | ||
|
|
a786a1427d | ||
|
|
961612370d | ||
|
|
bd5b9573f6 | ||
|
|
e603a10ab4 | ||
|
|
984346d682 | ||
|
|
f4cecb61bc | ||
|
|
cffa101419 | ||
|
|
1d7d201b5b | ||
|
|
c0e4e12138 | ||
|
|
5a85255be9 | ||
|
|
60d3960f3a | ||
|
|
7bcb03f1ec | ||
|
|
01ef053a11 | ||
|
|
8988c775fe | ||
|
|
c8bfa98b6b |
32
.github/workflows/node-delete-preview.yml
vendored
Normal file
32
.github/workflows/node-delete-preview.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# documentation: https://github.com/deltachat/sysadmin/tree/master/download.delta.chat
|
||||
name: Delete node PR previews
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [closed]
|
||||
|
||||
jobs:
|
||||
delete:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Get Pullrequest ID
|
||||
id: getid
|
||||
run: |
|
||||
export PULLREQUEST_ID=$(jq .number < $GITHUB_EVENT_PATH)
|
||||
echo ::set-output name=prid::$PULLREQUEST_ID
|
||||
- name: Renaming
|
||||
run: |
|
||||
# create empty file to copy it over the outdated deliverable on download.delta.chat
|
||||
echo "This preview build is outdated and has been removed." > empty
|
||||
cp empty deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz
|
||||
- name: Replace builds with dummy files
|
||||
uses: horochx/deploy-via-scp@v1.0.1
|
||||
with:
|
||||
user: ${{ secrets.USERNAME }}
|
||||
key: ${{ secrets.SSH_KEY }}
|
||||
host: "download.delta.chat"
|
||||
port: 22
|
||||
local: "deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz"
|
||||
remote: "/var/www/html/download/node/"
|
||||
34
.github/workflows/node-docs.yml
vendored
Normal file
34
.github/workflows/node-docs.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: Generate & upload node.js documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
generate:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
|
||||
- name: Use Node.js 16.x
|
||||
uses: actions/setup-node@v1
|
||||
with:
|
||||
node-version: 16.x
|
||||
|
||||
- name: npm install and generate documentation
|
||||
run: |
|
||||
cd node
|
||||
npm i --ignore-scripts
|
||||
npx typedoc
|
||||
mv docs js
|
||||
|
||||
- name: Upload
|
||||
uses: horochx/deploy-via-scp@v1.0.1
|
||||
with:
|
||||
user: ${{ secrets.USERNAME }}
|
||||
key: ${{ secrets.KEY }}
|
||||
host: "delta.chat"
|
||||
port: 22
|
||||
local: "node/js"
|
||||
remote: "/var/www/html/"
|
||||
140
.github/workflows/node-package.yml
vendored
Normal file
140
.github/workflows/node-package.yml
vendored
Normal file
@@ -0,0 +1,140 @@
|
||||
name: 'node.js'
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
tags:
|
||||
- '*'
|
||||
|
||||
|
||||
jobs:
|
||||
prebuild:
|
||||
name: 'Prebuild'
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-18.04, macos-latest, windows-latest]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: System info
|
||||
run: |
|
||||
rustc -vV
|
||||
rustup -vV
|
||||
cargo -vV
|
||||
npm --version
|
||||
node --version
|
||||
|
||||
- name: Cache node modules
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
${{ env.APPDATA }}/npm-cache
|
||||
~/.npm
|
||||
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry/
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ matrix.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }}-2
|
||||
|
||||
- name: Install dependencies & build
|
||||
if: steps.cache.outputs.cache-hit != 'true'
|
||||
run: |
|
||||
cd node
|
||||
npm install --verbose
|
||||
|
||||
- name: Build Prebuild
|
||||
run: |
|
||||
cd node
|
||||
npm run prebuildify
|
||||
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
|
||||
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: ${{ matrix.os }}
|
||||
path: node/${{ matrix.os }}.tar.gz
|
||||
|
||||
pack-module:
|
||||
needs: prebuild
|
||||
name: 'Package deltachat-node and upload to download.delta.chat'
|
||||
runs-on: ubuntu-18.04
|
||||
steps:
|
||||
- name: install tree
|
||||
run: sudo apt install tree
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: '16'
|
||||
- name: System info
|
||||
run: |
|
||||
rustc -vV
|
||||
rustup -vV
|
||||
cargo -vV
|
||||
npm --version
|
||||
node --version
|
||||
- name: Download ubuntu prebuild
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: ubuntu-18.04
|
||||
- name: Download macos prebuild
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: macos-latest
|
||||
- name: Download windows prebuild
|
||||
uses: actions/download-artifact@v1
|
||||
with:
|
||||
name: windows-latest
|
||||
- shell: bash
|
||||
run: |
|
||||
mkdir node/prebuilds
|
||||
tar -xvzf ubuntu-18.04/ubuntu-18.04.tar.gz -C node/prebuilds
|
||||
tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
|
||||
tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
|
||||
tree node/prebuilds
|
||||
- name: install dependencies without running scripts
|
||||
run: |
|
||||
npm install --ignore-scripts
|
||||
- name: build typescript part
|
||||
run: |
|
||||
npm run build:bindings:ts
|
||||
- name: Set DELTACHAT_NODE_TAR_GZ env variable
|
||||
run: |
|
||||
echo "DELTACHAT_NODE_TAR_GZ=deltachat-node-${{ github.ref_name }}" >> $GITHUB_ENV
|
||||
- name: package
|
||||
shell: bash
|
||||
run: |
|
||||
npm pack .
|
||||
ls -lah
|
||||
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
|
||||
- name: Upload Prebuild
|
||||
uses: actions/upload-artifact@v1
|
||||
with:
|
||||
name: deltachat-node.tgz
|
||||
path: $DELTACHAT_NODE_TAR_GZ
|
||||
# Upload to download.delta.chat/node/preview/
|
||||
- name: Upload deltachat-node preview to download.delta.chat/node/preview/
|
||||
id: upload-preview
|
||||
shell: bash
|
||||
run: |
|
||||
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
|
||||
chmod 600 __TEMP_INPUT_KEY_FILE
|
||||
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r $DELTACHAT_NODE_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
|
||||
continue-on-error: true
|
||||
# Upload to download.delta.chat/node/
|
||||
- name: Upload deltachat-node build to download.delta.chat/node/
|
||||
if: ${{ steps.tag.outputs.tag }}
|
||||
id: upload
|
||||
shell: bash
|
||||
run: |
|
||||
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
|
||||
chmod 600 __TEMP_INPUT_KEY_FILE
|
||||
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r $DELTACHAT_NODE_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/"
|
||||
7
.gitignore
vendored
7
.gitignore
vendored
@@ -33,3 +33,10 @@ coverage/
|
||||
python/accounts.txt
|
||||
python/all-testaccounts.txt
|
||||
tmp/
|
||||
|
||||
# from deltachat-node
|
||||
node_modules/
|
||||
node/build/
|
||||
node/dist/
|
||||
node/prebuilds/
|
||||
node/.nyc_output/
|
||||
|
||||
42
.npmignore
Normal file
42
.npmignore
Normal file
@@ -0,0 +1,42 @@
|
||||
.circleci/
|
||||
.gitmodules
|
||||
node/.nyc_output/
|
||||
.travis.yml
|
||||
appveyor.yml
|
||||
node/build/
|
||||
node/README.md
|
||||
rustfmt.toml
|
||||
spec.md
|
||||
test-data/
|
||||
build2/
|
||||
node_modules
|
||||
.git
|
||||
.idea/
|
||||
.pytest_cache
|
||||
CMakeLists.txt
|
||||
README.md
|
||||
contrib/
|
||||
node/ci_scripts/
|
||||
coverage/
|
||||
node/.circleci
|
||||
node/appveyor.yml
|
||||
ci/
|
||||
ci_scripts/
|
||||
python/
|
||||
target
|
||||
proptest-regressions
|
||||
deltachat-ffi/Doxyfile
|
||||
scripts
|
||||
webxdc.md
|
||||
standards.md
|
||||
draft/
|
||||
node/examples/
|
||||
# deltachat-core-rust/assets # don't exclude assets, otherwise it won't build
|
||||
node/images/
|
||||
node/test/
|
||||
node/windows.md
|
||||
node/*.tar.gz
|
||||
node/old_docs.md
|
||||
.vscode/
|
||||
.github/
|
||||
node/.prettierrc.yml
|
||||
31
CHANGELOG.md
31
CHANGELOG.md
@@ -1,5 +1,36 @@
|
||||
# Changelog
|
||||
|
||||
## 1.81.0
|
||||
|
||||
### API-Changes
|
||||
- deprecate unused `marker1before` argument of `dc_get_chat_msgs`
|
||||
and remove `DC_MSG_ID_MARKER1` constant #3274
|
||||
|
||||
### Changes
|
||||
- now the node-bindings are also part of this repository 🎉 #3283
|
||||
- support `source_code_url` from Webxdc manifests #3314
|
||||
- support Webxdc document names and add `document` to `dc_msg_get_webxdc_info()` #3317 #3324
|
||||
- improve chat encryption info, make it easier to find contacts without keys #3318
|
||||
- improve error reporting when creating a folder fails #3325
|
||||
- node: remove unmaintained coverage scripts
|
||||
- send normal messages with higher priority than MDNs #3243
|
||||
- make Scheduler stateless #3302
|
||||
- abort instead of unwinding on panic #3259
|
||||
- improve python bindings #3297 #3298
|
||||
- improve documentation #3307 #3306 #3309 #3319 #3321
|
||||
- refactorings #3304 #3303 #3323
|
||||
|
||||
### Fixes
|
||||
- node: throw error when getting context with an invalid account id
|
||||
- node: throw error when instanciating a wrapper class on `null` (Context, Message, Chat, ChatList and so on)
|
||||
- use same contact-color if email address differ only in upper-/lowercase #3327
|
||||
- fix race condition in ongoing process (import/export, configuration) allocation
|
||||
- repair encrypted mails "mixed up" by Google Workspace "Append footer" function #3315
|
||||
|
||||
### Removed
|
||||
- node: remove unmaintained coverage scripts
|
||||
|
||||
|
||||
## 1.80.0
|
||||
|
||||
### Changes
|
||||
|
||||
4
Cargo.lock
generated
4
Cargo.lock
generated
@@ -1067,7 +1067,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat"
|
||||
version = "1.80.0"
|
||||
version = "1.81.0"
|
||||
dependencies = [
|
||||
"ansi_term",
|
||||
"anyhow",
|
||||
@@ -1146,7 +1146,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.80.0"
|
||||
version = "1.81.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-std",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat"
|
||||
version = "1.80.0"
|
||||
version = "1.81.0"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
@@ -8,9 +8,11 @@ rust-version = "1.56"
|
||||
|
||||
[profile.dev]
|
||||
debug = 0
|
||||
panic = 'abort'
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
panic = 'abort'
|
||||
|
||||
[dependencies]
|
||||
deltachat_derive = { path = "./deltachat_derive" }
|
||||
|
||||
@@ -128,7 +128,7 @@ $ cargo test -- --ignored
|
||||
Language bindings are available for:
|
||||
|
||||
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
||||
- **Node.js** \[[📂 source](https://github.com/deltachat/deltachat-node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
|
||||
- **Node.js** \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
|
||||
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
|
||||
- **Go** \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
|
||||
- **Free Pascal** \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
|
||||
|
||||
@@ -12,7 +12,7 @@ async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
|
||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
||||
|
||||
for c in chats.iter().take(10) {
|
||||
black_box(chat::get_chat_msgs(&context, *c, 0, None).await.ok());
|
||||
black_box(chat::get_chat_msgs(&context, *c, 0).await.ok());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "deltachat_ffi"
|
||||
version = "1.80.0"
|
||||
version = "1.81.0"
|
||||
description = "Deltachat FFI"
|
||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||
edition = "2018"
|
||||
|
||||
@@ -1174,8 +1174,7 @@ dc_msg_t* dc_get_draft (dc_context_t* context, uint32_t ch
|
||||
* be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour.
|
||||
* To get the concrete time of the marker, use dc_array_get_timestamp().
|
||||
* If set to DC_GCM_INFO_ONLY, only system messages will be returned, can be combined with DC_GCM_ADDDAYMARKER.
|
||||
* @param marker1before An optional message ID. If set, the ID DC_MSG_ID_MARKER1 will be added just
|
||||
* before the given ID in the returned array. Set this to 0 if you do not want this behaviour.
|
||||
* @param marker1before Deprecated, set this to 0.
|
||||
* @return Array of message IDs, must be dc_array_unref()'d when no longer used.
|
||||
*/
|
||||
dc_array_t* dc_get_chat_msgs (dc_context_t* context, uint32_t chat_id, uint32_t flags, uint32_t marker1before);
|
||||
@@ -3447,7 +3446,7 @@ int64_t dc_chat_get_remaining_mute_duration (const dc_chat_t* chat);
|
||||
*/
|
||||
|
||||
|
||||
#define DC_MSG_ID_MARKER1 1
|
||||
#define DC_MSG_ID_MARKER1 1 // this is used by iOS to mark things in the message list
|
||||
#define DC_MSG_ID_DAYMARKER 9
|
||||
#define DC_MSG_ID_LAST_SPECIAL 9
|
||||
|
||||
@@ -3730,9 +3729,15 @@ char* dc_msg_get_webxdc_blob (const dc_msg_t* msg, const char*
|
||||
* To get the file, use dc_msg_get_webxdc_blob().
|
||||
* App icons should should be square,
|
||||
* the implementations will add round corners etc. as needed.
|
||||
* - document: if the Webxdc represents a document, this is the name of the document,
|
||||
* otherwise, this is an empty string.
|
||||
* - summary: short string describing the state of the app,
|
||||
* sth. as "2 votes", "Highscore: 123",
|
||||
* can be changed by the apps and defaults to an empty string.
|
||||
* - source_code_url:
|
||||
* 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.
|
||||
*
|
||||
* @memberof dc_msg_t
|
||||
* @param msg The webxdc instance.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::chat::ChatItem;
|
||||
use crate::constants::{DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1};
|
||||
use crate::constants::DC_MSG_ID_DAYMARKER;
|
||||
use crate::location::Location;
|
||||
use crate::message::MsgId;
|
||||
|
||||
@@ -18,7 +18,6 @@ impl dc_array_t {
|
||||
Self::MsgIds(array) => array[index].to_u32(),
|
||||
Self::Chat(array) => match array[index] {
|
||||
ChatItem::Message { msg_id } => msg_id.to_u32(),
|
||||
ChatItem::Marker1 => DC_MSG_ID_MARKER1,
|
||||
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
|
||||
},
|
||||
Self::Locations(array) => array[index].location_id,
|
||||
@@ -31,7 +30,6 @@ impl dc_array_t {
|
||||
Self::MsgIds(_) => None,
|
||||
Self::Chat(array) => array.get(index).and_then(|item| match item {
|
||||
ChatItem::Message { .. } => None,
|
||||
ChatItem::Marker1 { .. } => None,
|
||||
ChatItem::DayMarker { timestamp } => Some(*timestamp),
|
||||
}),
|
||||
Self::Locations(array) => array.get(index).map(|location| location.timestamp),
|
||||
|
||||
@@ -1027,22 +1027,17 @@ pub unsafe extern "C" fn dc_get_chat_msgs(
|
||||
context: *mut dc_context_t,
|
||||
chat_id: u32,
|
||||
flags: u32,
|
||||
marker1before: u32,
|
||||
_marker1before: u32,
|
||||
) -> *mut dc_array::dc_array_t {
|
||||
if context.is_null() {
|
||||
eprintln!("ignoring careless call to dc_get_chat_msgs()");
|
||||
return ptr::null_mut();
|
||||
}
|
||||
let ctx = &*context;
|
||||
let marker_flag = if marker1before <= DC_MSG_ID_LAST_SPECIAL {
|
||||
None
|
||||
} else {
|
||||
Some(MsgId::new(marker1before))
|
||||
};
|
||||
|
||||
block_on(async move {
|
||||
Box::into_raw(Box::new(
|
||||
chat::get_chat_msgs(ctx, ChatId::new(chat_id), flags, marker_flag)
|
||||
chat::get_chat_msgs(ctx, ChatId::new(chat_id), flags)
|
||||
.await
|
||||
.unwrap_or_log_default(ctx, "failed to get chat msgs")
|
||||
.into(),
|
||||
@@ -1919,7 +1914,7 @@ pub unsafe extern "C" fn dc_get_contacts(
|
||||
let query = to_opt_string_lossy(query);
|
||||
|
||||
block_on(async move {
|
||||
match Contact::get_all(ctx, flags, query).await {
|
||||
match Contact::get_all(ctx, flags, query.as_deref()).await {
|
||||
Ok(contacts) => Box::into_raw(Box::new(dc_array_t::from(
|
||||
contacts.iter().map(|id| id.to_u32()).collect::<Vec<u32>>(),
|
||||
))),
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
# Webxdc Developer Reference
|
||||
|
||||
(This document may eventually be merged with the [webxdc guidebook](https://deltachat.github.io/webxdc_docs/), where you may currently find other useful information.)
|
||||
|
||||
## Webxdc File Format
|
||||
|
||||
- a **Webxdc app** is a **ZIP-file** with the extension `.xdc`
|
||||
- a **Webxdc** is a **ZIP-file** with the extension `.xdc`
|
||||
- the ZIP-file must use the default compression methods as of RFC 1950,
|
||||
this is "Deflate" or "Store"
|
||||
- the ZIP-file must contain at least the file `index.html`
|
||||
- if the Webxdc app is started, `index.html` is opened in a restricted webview
|
||||
- if the Webxdc is started, `index.html` is opened in a restricted webview
|
||||
that allow accessing resources only from the ZIP-file
|
||||
|
||||
|
||||
@@ -26,7 +28,7 @@ no need to add `webxdc.js` to your ZIP-file):
|
||||
window.webxdc.sendUpdate(update, descr);
|
||||
```
|
||||
|
||||
Webxdc apps are usually shared in a chat and run independently on each peer.
|
||||
A Webxdc is usually shared in a chat and run independently on each peer.
|
||||
To get a shared state, the peers use `sendUpdate()` to send updates to each other.
|
||||
|
||||
- `update`: an object with the following properties:
|
||||
@@ -35,7 +37,9 @@ To get a shared state, the peers use `sendUpdate()` to send updates to each othe
|
||||
eg. "Alice voted" or "Bob scored 123 in MyGame";
|
||||
usually only one line of text is shown,
|
||||
use this option sparingly to not spam the chat.
|
||||
- `update.summary`: optional, short text, shown beside app icon;
|
||||
- `update.document`: optional, name of the document in edit,
|
||||
must not be used eg. in games where the Webxdc does not create documents
|
||||
- `update.summary`: optional, short text, shown beside Webxdc icon;
|
||||
it is recommended to use some aggregated value, eg. "8 votes", "Highscore: 123"
|
||||
|
||||
- `descr`: short, human-readable description what this update is about.
|
||||
@@ -74,9 +78,12 @@ Each `update` which is passed to the callback comes with the following propertie
|
||||
- `update.max_serial`: the maximum serial currently known.
|
||||
If `max_serial` equals `serial` this update is the last update (until new network messages arrive).
|
||||
|
||||
- `update.info`: optional, short, informational message (see `send_update`)
|
||||
- `update.info`: optional, short, informational message (see `sendUpdate()`)
|
||||
|
||||
- `update.summary`: optional, short text, shown beside app icon (see `send_update`)
|
||||
- `update.document`: optional, document name as set by the sender, (see `sendUpdate()`),
|
||||
implementations show the document name eg. beside the app icon or in the title bar
|
||||
|
||||
- `update.summary`: optional, short text, shown beside icon (see `sendUpdate()`)
|
||||
|
||||
|
||||
### selfAddr
|
||||
@@ -110,17 +117,21 @@ some basic information are read and used from there.
|
||||
the `manifest.toml` has the following format
|
||||
|
||||
```toml
|
||||
name = "My App Name"
|
||||
name = "My Name"
|
||||
source_code_url = "https://example.org/orga/repo"
|
||||
```
|
||||
|
||||
- **name** - The name of the app.
|
||||
If no name is set or if there is no manifest, the filename is used as the app name.
|
||||
- `name` - The name of the Webxdc.
|
||||
If no name is set or if there is no manifest, the filename is used as the Webxdc name.
|
||||
|
||||
- `source_code_url` - Optional URL where the source code of the Webxdc and maybe other information can be found.
|
||||
UI may make the url accessible via a "Help" menu in the Webxdc window.
|
||||
|
||||
|
||||
## App Icon
|
||||
## Webxdc Icon
|
||||
|
||||
If the ZIP-root contains an `icon.png` or `icon.jpg`,
|
||||
these files are used as the icon for the app.
|
||||
these files are used as the icon for the Webxdc.
|
||||
The icon should be a square at reasonable width/height;
|
||||
round corners etc. will be added by the implementations as needed.
|
||||
If no icon is set, a default icon will be used.
|
||||
@@ -160,7 +171,7 @@ The following example shows an input field and every input is show on all peers
|
||||
|
||||
[Webxdc Development Tool](https://github.com/deltachat/webxdc-dev)
|
||||
offers an **Webxdc Simulator** that can be used in many browsers without any installation needed.
|
||||
You can also use that repository as a template for your own app -
|
||||
You can also use that repository as a template for your own Webxdc -
|
||||
just clone and start adapting things to your need.
|
||||
|
||||
|
||||
@@ -170,7 +181,7 @@ just clone and start adapting things to your need.
|
||||
- [Draw](https://github.com/adbenitez/draw.xdc)
|
||||
- [Poll](https://github.com/r10s/webxdc-poll/)
|
||||
- [Tic Tac Toe](https://github.com/Simon-Laux/tictactoe.xdc)
|
||||
- Even more with [Topic #webxdc on Github](https://github.com/topics/webxdc)
|
||||
- Even more with [Topic #webxdc on Github](https://github.com/topics/webxdc) or in the [webxdc GitHub organization](https://github.com/webxdc)
|
||||
|
||||
|
||||
## Closing Remarks
|
||||
@@ -180,6 +191,8 @@ just clone and start adapting things to your need.
|
||||
- viewport and scaling features are implementation specific,
|
||||
if you want to have an explicit behavior, you can add eg.
|
||||
`<meta name="viewport" content="initial-scale=1; user-scalable=no">` to your Webxdc
|
||||
- the `<title>` tag should not be used and its content is usually not displayed;
|
||||
instead, use the `name` property from `manifest.toml`
|
||||
- there are tons of ideas for enhancements of the API and the file format,
|
||||
eg. in the future, we will may define icon- and manifest-files,
|
||||
allow to aggregate the state or add metadata.
|
||||
|
||||
@@ -639,14 +639,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
||||
|
||||
let time_start = std::time::SystemTime::now();
|
||||
let msglist =
|
||||
chat::get_chat_msgs(&context, sel_chat.get_id(), DC_GCM_ADDDAYMARKER, None).await?;
|
||||
chat::get_chat_msgs(&context, sel_chat.get_id(), DC_GCM_ADDDAYMARKER).await?;
|
||||
let time_needed = time_start.elapsed().unwrap_or_default();
|
||||
|
||||
let msglist: Vec<MsgId> = msglist
|
||||
.into_iter()
|
||||
.map(|x| match x {
|
||||
ChatItem::Message { msg_id } => msg_id,
|
||||
ChatItem::Marker1 => MsgId::new(DC_MSG_ID_MARKER1),
|
||||
ChatItem::DayMarker { .. } => MsgId::new(DC_MSG_ID_DAYMARKER),
|
||||
})
|
||||
.collect();
|
||||
|
||||
6
node/.prettierrc.yml
Normal file
6
node/.prettierrc.yml
Normal file
@@ -0,0 +1,6 @@
|
||||
# .prettierrc
|
||||
trailingComma: es5
|
||||
tabWidth: 2
|
||||
semi: false
|
||||
singleQuote: true
|
||||
jsxSingleQuote: true
|
||||
21
node/CONTRIBUTORS.md
Normal file
21
node/CONTRIBUTORS.md
Normal file
@@ -0,0 +1,21 @@
|
||||
# Contributors
|
||||
|
||||
| Name | GitHub |
|
||||
| :-------------------- | :----------------------------------------------- |
|
||||
| **Lars-Magnus Skog** | |
|
||||
| **jikstra** | |
|
||||
| **Simon Laux** | [**@Simon-Laux**](https://github.com/Simon-Laux) |
|
||||
| **Jikstra** | [**@Jikstra**](https://github.com/Jikstra) |
|
||||
| **Nico de Haen** | |
|
||||
| **B. Petersen** | |
|
||||
| **Karissa McKelvey** | [**@karissa**](https://github.com/karissa) |
|
||||
| **developer** | |
|
||||
| **Alexander Krotov** | |
|
||||
| **Floris Bruynooghe** | |
|
||||
| **lefherz** | |
|
||||
| **Pablo** | [**@pabzm**](https://github.com/pabzm) |
|
||||
| **pabzm** | |
|
||||
| **holger krekel** | |
|
||||
| **Robert Schütz** | |
|
||||
| **bb** | |
|
||||
| **Charles Paul** | |
|
||||
674
node/LICENSE
Normal file
674
node/LICENSE
Normal file
@@ -0,0 +1,674 @@
|
||||
GNU GENERAL PUBLIC LICENSE
|
||||
Version 3, 29 June 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU General Public License is a free, copyleft license for
|
||||
software and other kinds of works.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
the GNU General Public License is intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users. We, the Free Software Foundation, use the
|
||||
GNU General Public License for most of our software; it applies also to
|
||||
any other work released this way by its authors. You can apply it to
|
||||
your programs, too.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
To protect your rights, we need to prevent others from denying you
|
||||
these rights or asking you to surrender the rights. Therefore, you have
|
||||
certain responsibilities if you distribute copies of the software, or if
|
||||
you modify it: responsibilities to respect the freedom of others.
|
||||
|
||||
For example, if you distribute copies of such a program, whether
|
||||
gratis or for a fee, you must pass on to the recipients the same
|
||||
freedoms that you received. You must make sure that they, too, receive
|
||||
or can get the source code. And you must show them these terms so they
|
||||
know their rights.
|
||||
|
||||
Developers that use the GNU GPL protect your rights with two steps:
|
||||
(1) assert copyright on the software, and (2) offer you this License
|
||||
giving you legal permission to copy, distribute and/or modify it.
|
||||
|
||||
For the developers' and authors' protection, the GPL clearly explains
|
||||
that there is no warranty for this free software. For both users' and
|
||||
authors' sake, the GPL requires that modified versions be marked as
|
||||
changed, so that their problems will not be attributed erroneously to
|
||||
authors of previous versions.
|
||||
|
||||
Some devices are designed to deny users access to install or run
|
||||
modified versions of the software inside them, although the manufacturer
|
||||
can do so. This is fundamentally incompatible with the aim of
|
||||
protecting users' freedom to change the software. The systematic
|
||||
pattern of such abuse occurs in the area of products for individuals to
|
||||
use, which is precisely where it is most unacceptable. Therefore, we
|
||||
have designed this version of the GPL to prohibit the practice for those
|
||||
products. If such problems arise substantially in other domains, we
|
||||
stand ready to extend this provision to those domains in future versions
|
||||
of the GPL, as needed to protect the freedom of users.
|
||||
|
||||
Finally, every program is threatened constantly by software patents.
|
||||
States should not allow patents to restrict development and use of
|
||||
software on general-purpose computers, but in those that do, we wish to
|
||||
avoid the special danger that patents applied to a free program could
|
||||
make it effectively proprietary. To prevent this, the GPL assures that
|
||||
patents cannot be used to render the program non-free.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Use with the GNU Affero General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU Affero General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the special requirements of the GNU Affero General Public License,
|
||||
section 13, concerning interaction through a network will apply to the
|
||||
combination as such.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU General Public License from time to time. Such new versions will
|
||||
be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If the program does terminal interaction, make it output a short
|
||||
notice like this when it starts in an interactive mode:
|
||||
|
||||
<program> Copyright (C) <year> <name of author>
|
||||
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||
This is free software, and you are welcome to redistribute it
|
||||
under certain conditions; type `show c' for details.
|
||||
|
||||
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||
parts of the General Public License. Of course, your program's commands
|
||||
might be different; for a GUI interface, you would use an "about box".
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU GPL, see
|
||||
<http://www.gnu.org/licenses/>.
|
||||
|
||||
The GNU General Public License does not permit incorporating your program
|
||||
into proprietary programs. If your program is a subroutine library, you
|
||||
may consider it more useful to permit linking proprietary applications with
|
||||
the library. If this is what you want to do, use the GNU Lesser General
|
||||
Public License instead of this License. But first, please read
|
||||
<http://www.gnu.org/philosophy/why-not-lgpl.html>.
|
||||
247
node/README.md
Normal file
247
node/README.md
Normal file
@@ -0,0 +1,247 @@
|
||||
# deltachat-node
|
||||
|
||||
> node.js bindings for [`deltachat-core-rust`](..)
|
||||
|
||||
[](https://www.npmjs.com/package/deltachat-node)
|
||||

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

|
||||
|
||||
The coverage report from `nyc` in the console is rather limited. To get a more detailed coverage report you can run `npm run coverage-html-report`. This will produce a html report from the `nyc` data and display it in a browser on your local machine.
|
||||
|
||||
To run the integration tests you need to set the `DCC_NEW_TMP_EMAIL` environment variables. E.g.:
|
||||
|
||||
```
|
||||
$ export DCC_NEW_TMP_EMAIL=https://testrun.org/new_email?t=[token]
|
||||
$ npm run test
|
||||
```
|
||||
|
||||
### Scripts
|
||||
|
||||
We have the following scripts for building, testing and coverage:
|
||||
|
||||
- `npm run coverage` Creates a coverage report and passes it to `coveralls`. Only done by `Travis`.
|
||||
- `npm run coverage-html-report` Generates a html report from the coverage data and opens it in a browser on the local machine.
|
||||
- `npm run generate-constants` Generates `constants.js` and `events.js` based on the `deltachat-core-rust/deltachat-ffi/deltachat.h` header file.
|
||||
- `npm install` After dependencies are installed, runs `node-gyp-build` to see if the native code needs to be rebuilt.
|
||||
- `npm run build` Rebuilds all code.
|
||||
- `npm run build:core` Rebuilds code in `deltachat-core-rust`.
|
||||
- `npm run build:bindings` Rebuilds the bindings and links with `deltachat-core-rust`.
|
||||
- `ǹpm run clean` Removes all built code
|
||||
- `npm run prebuildify` Builds prebuilt binary to `prebuilds/$PLATFORM-$ARCH`. Copies `deltachat.dll` from `deltachat-core-rust` for windows.
|
||||
- `npm run download-prebuilds` Downloads all prebuilt binaries from github before `npm publish`.
|
||||
- `npm test` Runs `standard` and then the tests in `test/index.js`.
|
||||
- `npm run test-integration` Runs the integration tests.
|
||||
- `npm run hallmark` Runs `hallmark` on all markdown files.
|
||||
|
||||
### Releases
|
||||
|
||||
The following steps are needed to make a release:
|
||||
|
||||
1. Wait until `pack-module` github action is completed
|
||||
2. Run `npm publish https://download.delta.chat/node/deltachat-node-v1.x.x.tar.gz` to publish it to npm. You probably need write rights to npm.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under `GPL-3.0-or-later`, see [LICENSE](./LICENSE) file for details.
|
||||
|
||||
> Copyright © 2018 `DeltaChat` contributors.
|
||||
>
|
||||
> This program is free software: you can redistribute it and/or modify
|
||||
> it under the terms of the GNU General Public License as published by
|
||||
> the Free Software Foundation, either version 3 of the License, or
|
||||
> (at your option) any later version.
|
||||
>
|
||||
> This program is distributed in the hope that it will be useful,
|
||||
> but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
> MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
> GNU General Public License for more details.
|
||||
>
|
||||
> You should have received a copy of the GNU General Public License
|
||||
> along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
[appveyor-shield]: https://ci.appveyor.com/api/projects/status/t0narp672wpbl6pd?svg=true
|
||||
|
||||
[appveyor]: https://ci.appveyor.com/project/ralphtheninja/deltachat-node-d4bf8
|
||||
78
node/binding.gyp
Normal file
78
node/binding.gyp
Normal file
@@ -0,0 +1,78 @@
|
||||
{
|
||||
# documentation about the format of this file can be found under https://gyp.gsrc.io/docs/InputFormatReference.md
|
||||
# Variables can be specified when calling node-gyp as so:
|
||||
# node-gyp configure -- -Dvarname=value
|
||||
"variables": {
|
||||
# Whether to use a system-wide installation of deltachat-core
|
||||
# using pkg-config. Set to either "true" or "false".
|
||||
"USE_SYSTEM_LIBDELTACHAT%": "<!(echo $USE_SYSTEM_LIBDELTACHAT)",
|
||||
},
|
||||
"targets": [
|
||||
{
|
||||
"target_name": "deltachat",
|
||||
"sources": ["./src/module.c"],
|
||||
"include_dirs": ["<!(node -e \"require('napi-macros')\")"],
|
||||
"conditions": [
|
||||
[
|
||||
"OS == 'win'",
|
||||
{
|
||||
"include_dirs": ["../deltachat-ffi"],
|
||||
"libraries": [
|
||||
"../../target/release/deltachat.dll.lib",
|
||||
],
|
||||
"conditions": [
|
||||
[
|
||||
"USE_SYSTEM_LIBDELTACHAT == 'true'",
|
||||
{
|
||||
"cflags": ["<!(pkg-config --cflags deltachat)"],
|
||||
"libraries": ["<!(pkg-config --libs deltachat)"],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
[
|
||||
"OS == 'linux' or OS == 'mac'",
|
||||
{
|
||||
"libraries": ["-lpthread"],
|
||||
"cflags": ["-std=gnu99"],
|
||||
"conditions": [
|
||||
[
|
||||
"USE_SYSTEM_LIBDELTACHAT != 'true'",
|
||||
{
|
||||
"include_dirs": ["../deltachat-ffi"],
|
||||
"ldflags": ["-Wl,-Bsymbolic"], # Prevent sqlite3 from electron from overriding sqlcipher
|
||||
"libraries": [
|
||||
"../../target/release/libdeltachat.a",
|
||||
"-ldl",
|
||||
],
|
||||
"conditions": [],
|
||||
},
|
||||
{
|
||||
# USE_SYSTEM_LIBDELTACHAT == 'true'
|
||||
"cflags": ["<!(pkg-config --cflags deltachat)"],
|
||||
"libraries": ["<!(pkg-config --libs deltachat)"],
|
||||
},
|
||||
],
|
||||
[
|
||||
"OS == 'mac'",
|
||||
{
|
||||
"libraries": [
|
||||
"-framework CoreFoundation",
|
||||
"-framework CoreServices",
|
||||
"-framework Security",
|
||||
"-lresolv",
|
||||
],
|
||||
},
|
||||
{
|
||||
# OS == 'linux'
|
||||
"libraries": ["-lm", "-lrt"],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
1
node/binding.js
Normal file
1
node/binding.js
Normal file
@@ -0,0 +1 @@
|
||||
module.exports = require('node-gyp-build')(__dirname)
|
||||
226
node/constants.js
Normal file
226
node/constants.js
Normal file
@@ -0,0 +1,226 @@
|
||||
// Generated!
|
||||
|
||||
module.exports = {
|
||||
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES: 3,
|
||||
DC_CERTCK_AUTO: 0,
|
||||
DC_CERTCK_STRICT: 1,
|
||||
DC_CHAT_ID_ALLDONE_HINT: 7,
|
||||
DC_CHAT_ID_ARCHIVED_LINK: 6,
|
||||
DC_CHAT_ID_LAST_SPECIAL: 9,
|
||||
DC_CHAT_ID_TRASH: 3,
|
||||
DC_CHAT_TYPE_BROADCAST: 160,
|
||||
DC_CHAT_TYPE_GROUP: 120,
|
||||
DC_CHAT_TYPE_MAILINGLIST: 140,
|
||||
DC_CHAT_TYPE_SINGLE: 100,
|
||||
DC_CHAT_TYPE_UNDEFINED: 0,
|
||||
DC_CHAT_VISIBILITY_ARCHIVED: 1,
|
||||
DC_CHAT_VISIBILITY_NORMAL: 0,
|
||||
DC_CHAT_VISIBILITY_PINNED: 2,
|
||||
DC_CONNECTIVITY_CONNECTED: 4000,
|
||||
DC_CONNECTIVITY_CONNECTING: 2000,
|
||||
DC_CONNECTIVITY_NOT_CONNECTED: 1000,
|
||||
DC_CONNECTIVITY_WORKING: 3000,
|
||||
DC_CONTACT_ID_DEVICE: 5,
|
||||
DC_CONTACT_ID_INFO: 2,
|
||||
DC_CONTACT_ID_LAST_SPECIAL: 9,
|
||||
DC_CONTACT_ID_SELF: 1,
|
||||
DC_DOWNLOAD_AVAILABLE: 10,
|
||||
DC_DOWNLOAD_DONE: 0,
|
||||
DC_DOWNLOAD_FAILURE: 20,
|
||||
DC_DOWNLOAD_IN_PROGRESS: 1000,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED: 2021,
|
||||
DC_EVENT_CHAT_MODIFIED: 2020,
|
||||
DC_EVENT_CONFIGURE_PROGRESS: 2041,
|
||||
DC_EVENT_CONNECTIVITY_CHANGED: 2100,
|
||||
DC_EVENT_CONTACTS_CHANGED: 2030,
|
||||
DC_EVENT_DELETED_BLOB_FILE: 151,
|
||||
DC_EVENT_ERROR: 400,
|
||||
DC_EVENT_ERROR_SELF_NOT_IN_GROUP: 410,
|
||||
DC_EVENT_IMAP_CONNECTED: 102,
|
||||
DC_EVENT_IMAP_MESSAGE_DELETED: 104,
|
||||
DC_EVENT_IMAP_MESSAGE_MOVED: 105,
|
||||
DC_EVENT_IMEX_FILE_WRITTEN: 2052,
|
||||
DC_EVENT_IMEX_PROGRESS: 2051,
|
||||
DC_EVENT_INCOMING_MSG: 2005,
|
||||
DC_EVENT_INFO: 100,
|
||||
DC_EVENT_LOCATION_CHANGED: 2035,
|
||||
DC_EVENT_MSGS_CHANGED: 2000,
|
||||
DC_EVENT_MSGS_NOTICED: 2008,
|
||||
DC_EVENT_MSG_DELIVERED: 2010,
|
||||
DC_EVENT_MSG_FAILED: 2012,
|
||||
DC_EVENT_MSG_READ: 2015,
|
||||
DC_EVENT_NEW_BLOB_FILE: 150,
|
||||
DC_EVENT_SECUREJOIN_INVITER_PROGRESS: 2060,
|
||||
DC_EVENT_SECUREJOIN_JOINER_PROGRESS: 2061,
|
||||
DC_EVENT_SELFAVATAR_CHANGED: 2110,
|
||||
DC_EVENT_SMTP_CONNECTED: 101,
|
||||
DC_EVENT_SMTP_MESSAGE_SENT: 103,
|
||||
DC_EVENT_WARNING: 300,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE: 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT: 4,
|
||||
DC_GCL_ADD_SELF: 2,
|
||||
DC_GCL_ARCHIVED_ONLY: 1,
|
||||
DC_GCL_FOR_FORWARDING: 8,
|
||||
DC_GCL_NO_SPECIALS: 2,
|
||||
DC_GCL_VERIFIED_ONLY: 1,
|
||||
DC_GCM_ADDDAYMARKER: 1,
|
||||
DC_GCM_INFO_ONLY: 2,
|
||||
DC_IMEX_EXPORT_BACKUP: 11,
|
||||
DC_IMEX_EXPORT_SELF_KEYS: 1,
|
||||
DC_IMEX_IMPORT_BACKUP: 12,
|
||||
DC_IMEX_IMPORT_SELF_KEYS: 2,
|
||||
DC_INFO_PROTECTION_DISABLED: 12,
|
||||
DC_INFO_PROTECTION_ENABLED: 11,
|
||||
DC_KEY_GEN_DEFAULT: 0,
|
||||
DC_KEY_GEN_ED25519: 2,
|
||||
DC_KEY_GEN_RSA2048: 1,
|
||||
DC_LP_AUTH_NORMAL: 4,
|
||||
DC_LP_AUTH_OAUTH2: 2,
|
||||
DC_MEDIA_QUALITY_BALANCED: 0,
|
||||
DC_MEDIA_QUALITY_WORSE: 1,
|
||||
DC_MSG_AUDIO: 40,
|
||||
DC_MSG_FILE: 60,
|
||||
DC_MSG_GIF: 21,
|
||||
DC_MSG_ID_DAYMARKER: 9,
|
||||
DC_MSG_ID_LAST_SPECIAL: 9,
|
||||
DC_MSG_ID_MARKER1: 1,
|
||||
DC_MSG_IMAGE: 20,
|
||||
DC_MSG_STICKER: 23,
|
||||
DC_MSG_TEXT: 10,
|
||||
DC_MSG_VIDEO: 50,
|
||||
DC_MSG_VIDEOCHAT_INVITATION: 70,
|
||||
DC_MSG_VOICE: 41,
|
||||
DC_MSG_WEBXDC: 80,
|
||||
DC_PROVIDER_STATUS_BROKEN: 3,
|
||||
DC_PROVIDER_STATUS_OK: 1,
|
||||
DC_PROVIDER_STATUS_PREPARATION: 2,
|
||||
DC_QR_ACCOUNT: 250,
|
||||
DC_QR_ADDR: 320,
|
||||
DC_QR_ASK_VERIFYCONTACT: 200,
|
||||
DC_QR_ASK_VERIFYGROUP: 202,
|
||||
DC_QR_ERROR: 400,
|
||||
DC_QR_FPR_MISMATCH: 220,
|
||||
DC_QR_FPR_OK: 210,
|
||||
DC_QR_FPR_WITHOUT_ADDR: 230,
|
||||
DC_QR_REVIVE_VERIFYCONTACT: 510,
|
||||
DC_QR_REVIVE_VERIFYGROUP: 512,
|
||||
DC_QR_TEXT: 330,
|
||||
DC_QR_URL: 332,
|
||||
DC_QR_WEBRTC_INSTANCE: 260,
|
||||
DC_QR_WITHDRAW_VERIFYCONTACT: 500,
|
||||
DC_QR_WITHDRAW_VERIFYGROUP: 502,
|
||||
DC_SHOW_EMAILS_ACCEPTED_CONTACTS: 1,
|
||||
DC_SHOW_EMAILS_ALL: 2,
|
||||
DC_SHOW_EMAILS_OFF: 0,
|
||||
DC_SOCKET_AUTO: 0,
|
||||
DC_SOCKET_PLAIN: 3,
|
||||
DC_SOCKET_SSL: 1,
|
||||
DC_SOCKET_STARTTLS: 2,
|
||||
DC_STATE_IN_FRESH: 10,
|
||||
DC_STATE_IN_NOTICED: 13,
|
||||
DC_STATE_IN_SEEN: 16,
|
||||
DC_STATE_OUT_DELIVERED: 26,
|
||||
DC_STATE_OUT_DRAFT: 19,
|
||||
DC_STATE_OUT_FAILED: 24,
|
||||
DC_STATE_OUT_MDN_RCVD: 28,
|
||||
DC_STATE_OUT_PENDING: 20,
|
||||
DC_STATE_OUT_PREPARING: 18,
|
||||
DC_STATE_UNDEFINED: 0,
|
||||
DC_STR_AC_SETUP_MSG_BODY: 43,
|
||||
DC_STR_AC_SETUP_MSG_SUBJECT: 42,
|
||||
DC_STR_ARCHIVEDCHATS: 40,
|
||||
DC_STR_AUDIO: 11,
|
||||
DC_STR_BAD_TIME_MSG_BODY: 85,
|
||||
DC_STR_BROADCAST_LIST: 115,
|
||||
DC_STR_CANNOT_LOGIN: 60,
|
||||
DC_STR_CANTDECRYPT_MSG_BODY: 29,
|
||||
DC_STR_CONFIGURATION_FAILED: 84,
|
||||
DC_STR_CONNECTED: 107,
|
||||
DC_STR_CONNTECTING: 108,
|
||||
DC_STR_CONTACT_NOT_VERIFIED: 36,
|
||||
DC_STR_CONTACT_SETUP_CHANGED: 37,
|
||||
DC_STR_CONTACT_VERIFIED: 35,
|
||||
DC_STR_DEVICE_MESSAGES: 68,
|
||||
DC_STR_DEVICE_MESSAGES_HINT: 70,
|
||||
DC_STR_DOWNLOAD_AVAILABILITY: 100,
|
||||
DC_STR_DRAFT: 3,
|
||||
DC_STR_E2E_AVAILABLE: 25,
|
||||
DC_STR_E2E_PREFERRED: 34,
|
||||
DC_STR_ENCRYPTEDMSG: 24,
|
||||
DC_STR_ENCR_NONE: 28,
|
||||
DC_STR_ENCR_TRANSP: 27,
|
||||
DC_STR_EPHEMERAL_DAY: 79,
|
||||
DC_STR_EPHEMERAL_DAYS: 95,
|
||||
DC_STR_EPHEMERAL_DISABLED: 75,
|
||||
DC_STR_EPHEMERAL_FOUR_WEEKS: 81,
|
||||
DC_STR_EPHEMERAL_HOUR: 78,
|
||||
DC_STR_EPHEMERAL_HOURS: 94,
|
||||
DC_STR_EPHEMERAL_MINUTE: 77,
|
||||
DC_STR_EPHEMERAL_MINUTES: 93,
|
||||
DC_STR_EPHEMERAL_SECONDS: 76,
|
||||
DC_STR_EPHEMERAL_WEEK: 80,
|
||||
DC_STR_EPHEMERAL_WEEKS: 96,
|
||||
DC_STR_ERROR: 112,
|
||||
DC_STR_ERROR_NO_NETWORK: 87,
|
||||
DC_STR_FAILED_SENDING_TO: 74,
|
||||
DC_STR_FILE: 12,
|
||||
DC_STR_FINGERPRINTS: 30,
|
||||
DC_STR_FORWARDED: 97,
|
||||
DC_STR_GIF: 23,
|
||||
DC_STR_IMAGE: 9,
|
||||
DC_STR_INCOMING_MESSAGES: 103,
|
||||
DC_STR_LAST_MSG_SENT_SUCCESSFULLY: 111,
|
||||
DC_STR_LOCATION: 66,
|
||||
DC_STR_MESSAGES: 114,
|
||||
DC_STR_MSGACTIONBYME: 63,
|
||||
DC_STR_MSGACTIONBYUSER: 62,
|
||||
DC_STR_MSGADDMEMBER: 17,
|
||||
DC_STR_MSGDELMEMBER: 18,
|
||||
DC_STR_MSGGROUPLEFT: 19,
|
||||
DC_STR_MSGGRPIMGCHANGED: 16,
|
||||
DC_STR_MSGGRPIMGDELETED: 33,
|
||||
DC_STR_MSGGRPNAME: 15,
|
||||
DC_STR_MSGLOCATIONDISABLED: 65,
|
||||
DC_STR_MSGLOCATIONENABLED: 64,
|
||||
DC_STR_NOMESSAGES: 1,
|
||||
DC_STR_NOT_CONNECTED: 121,
|
||||
DC_STR_NOT_SUPPORTED_BY_PROVIDER: 113,
|
||||
DC_STR_ONE_MOMENT: 106,
|
||||
DC_STR_OUTGOING_MESSAGES: 104,
|
||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
|
||||
DC_STR_PART_OF_TOTAL_USED: 116,
|
||||
DC_STR_PROTECTION_DISABLED: 89,
|
||||
DC_STR_PROTECTION_ENABLED: 88,
|
||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
|
||||
DC_STR_READRCPT: 31,
|
||||
DC_STR_READRCPT_MAILBODY: 32,
|
||||
DC_STR_REPLY_NOUN: 90,
|
||||
DC_STR_SAVED_MESSAGES: 69,
|
||||
DC_STR_SECURE_JOIN_GROUP_QR_DESC: 120,
|
||||
DC_STR_SECURE_JOIN_REPLIES: 118,
|
||||
DC_STR_SECURE_JOIN_STARTED: 117,
|
||||
DC_STR_SELF: 2,
|
||||
DC_STR_SELF_DELETED_MSG_BODY: 91,
|
||||
DC_STR_SENDING: 110,
|
||||
DC_STR_SERVER_TURNED_OFF: 92,
|
||||
DC_STR_SETUP_CONTACT_QR_DESC: 119,
|
||||
DC_STR_STICKER: 67,
|
||||
DC_STR_STORAGE_ON_DOMAIN: 105,
|
||||
DC_STR_SUBJECT_FOR_NEW_CONTACT: 73,
|
||||
DC_STR_SYNC_MSG_BODY: 102,
|
||||
DC_STR_SYNC_MSG_SUBJECT: 101,
|
||||
DC_STR_UNKNOWN_SENDER_FOR_CHAT: 72,
|
||||
DC_STR_UPDATE_REMINDER_MSG_BODY: 86,
|
||||
DC_STR_UPDATING: 109,
|
||||
DC_STR_VIDEO: 10,
|
||||
DC_STR_VIDEOCHAT_INVITATION: 82,
|
||||
DC_STR_VIDEOCHAT_INVITE_MSG_BODY: 83,
|
||||
DC_STR_VOICEMESSAGE: 7,
|
||||
DC_STR_WELCOME_MESSAGE: 71,
|
||||
DC_TEXT1_DRAFT: 1,
|
||||
DC_TEXT1_SELF: 3,
|
||||
DC_TEXT1_USERNAME: 2,
|
||||
DC_VIDEOCHATTYPE_BASICWEBRTC: 1,
|
||||
DC_VIDEOCHATTYPE_JITSI: 2,
|
||||
DC_VIDEOCHATTYPE_UNKNOWN: 0
|
||||
}
|
||||
34
node/events.js
Normal file
34
node/events.js
Normal file
@@ -0,0 +1,34 @@
|
||||
/* eslint-disable quotes */
|
||||
// Generated!
|
||||
|
||||
module.exports = {
|
||||
100: 'DC_EVENT_INFO',
|
||||
101: 'DC_EVENT_SMTP_CONNECTED',
|
||||
102: 'DC_EVENT_IMAP_CONNECTED',
|
||||
103: 'DC_EVENT_SMTP_MESSAGE_SENT',
|
||||
104: 'DC_EVENT_IMAP_MESSAGE_DELETED',
|
||||
105: 'DC_EVENT_IMAP_MESSAGE_MOVED',
|
||||
150: 'DC_EVENT_NEW_BLOB_FILE',
|
||||
151: 'DC_EVENT_DELETED_BLOB_FILE',
|
||||
300: 'DC_EVENT_WARNING',
|
||||
400: 'DC_EVENT_ERROR',
|
||||
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||
2005: 'DC_EVENT_INCOMING_MSG',
|
||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||
2010: 'DC_EVENT_MSG_DELIVERED',
|
||||
2012: 'DC_EVENT_MSG_FAILED',
|
||||
2015: 'DC_EVENT_MSG_READ',
|
||||
2020: 'DC_EVENT_CHAT_MODIFIED',
|
||||
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
|
||||
2030: 'DC_EVENT_CONTACTS_CHANGED',
|
||||
2035: 'DC_EVENT_LOCATION_CHANGED',
|
||||
2041: 'DC_EVENT_CONFIGURE_PROGRESS',
|
||||
2051: 'DC_EVENT_IMEX_PROGRESS',
|
||||
2052: 'DC_EVENT_IMEX_FILE_WRITTEN',
|
||||
2060: 'DC_EVENT_SECUREJOIN_INVITER_PROGRESS',
|
||||
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
|
||||
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
|
||||
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE'
|
||||
}
|
||||
40
node/examples/send_message.js
Normal file
40
node/examples/send_message.js
Normal file
@@ -0,0 +1,40 @@
|
||||
//@ts-check
|
||||
const { Context } = require('../dist')
|
||||
|
||||
const opts = {
|
||||
addr: '[email]',
|
||||
mail_pw: '[password]',
|
||||
}
|
||||
|
||||
const contact = '[email]'
|
||||
|
||||
async function main() {
|
||||
const dc = Context.open('./')
|
||||
dc.on('ALL', console.log.bind(null, 'core |'))
|
||||
|
||||
try {
|
||||
await dc.configure(opts)
|
||||
} catch (err) {
|
||||
console.error('Failed to configure because of: ', err)
|
||||
dc.unref()
|
||||
return
|
||||
}
|
||||
|
||||
dc.startIO()
|
||||
console.log('fully configured')
|
||||
|
||||
const contactId = dc.createContact('Test', contact)
|
||||
const chatId = dc.createChatByContactId(contactId)
|
||||
dc.sendMessage(chatId, 'Hi!')
|
||||
|
||||
console.log('sent message')
|
||||
|
||||
dc.once('DC_EVENT_SMTP_MESSAGE_SENT', async () => {
|
||||
console.log('Message sent, shutting down...')
|
||||
dc.stopIO()
|
||||
console.log('stopped io')
|
||||
dc.unref()
|
||||
})
|
||||
}
|
||||
|
||||
main()
|
||||
BIN
node/images/tests.png
Normal file
BIN
node/images/tests.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
9
node/lib/binding.ts
Normal file
9
node/lib/binding.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { join } from 'path'
|
||||
|
||||
/**
|
||||
* bindings are not typed yet.
|
||||
* if the availible function names are required they can be found inside of `../src/module.c`
|
||||
*/
|
||||
export const bindings: any = require('node-gyp-build')(join(__dirname, '../'))
|
||||
|
||||
export default bindings
|
||||
106
node/lib/chat.ts
Normal file
106
node/lib/chat.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import binding from './binding'
|
||||
import rawDebug from 'debug'
|
||||
const debug = rawDebug('deltachat:node:chat')
|
||||
import { C } from './constants'
|
||||
import { integerToHexColor } from './util'
|
||||
import { ChatJSON } from './types'
|
||||
|
||||
interface NativeChat {}
|
||||
/**
|
||||
* Wrapper around dc_chat_t*
|
||||
*/
|
||||
|
||||
export class Chat {
|
||||
constructor(public dc_chat: NativeChat) {
|
||||
debug('Chat constructor')
|
||||
if (dc_chat === null) {
|
||||
throw new Error('native chat can not be null')
|
||||
}
|
||||
}
|
||||
|
||||
getVisibility():
|
||||
| C.DC_CHAT_VISIBILITY_NORMAL
|
||||
| C.DC_CHAT_VISIBILITY_ARCHIVED
|
||||
| C.DC_CHAT_VISIBILITY_PINNED {
|
||||
return binding.dcn_chat_get_visibility(this.dc_chat)
|
||||
}
|
||||
|
||||
get color(): string {
|
||||
return integerToHexColor(binding.dcn_chat_get_color(this.dc_chat))
|
||||
}
|
||||
|
||||
getId(): number {
|
||||
return binding.dcn_chat_get_id(this.dc_chat)
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return binding.dcn_chat_get_name(this.dc_chat)
|
||||
}
|
||||
|
||||
getProfileImage(): string {
|
||||
return binding.dcn_chat_get_profile_image(this.dc_chat)
|
||||
}
|
||||
|
||||
getType(): number {
|
||||
return binding.dcn_chat_get_type(this.dc_chat)
|
||||
}
|
||||
|
||||
isSelfTalk(): boolean {
|
||||
return Boolean(binding.dcn_chat_is_self_talk(this.dc_chat))
|
||||
}
|
||||
|
||||
isContactRequest(): boolean {
|
||||
return Boolean(binding.dcn_chat_is_contact_request(this.dc_chat))
|
||||
}
|
||||
|
||||
isUnpromoted(): boolean {
|
||||
return Boolean(binding.dcn_chat_is_unpromoted(this.dc_chat))
|
||||
}
|
||||
|
||||
isProtected(): boolean {
|
||||
return Boolean(binding.dcn_chat_is_protected(this.dc_chat))
|
||||
}
|
||||
|
||||
get canSend(): boolean {
|
||||
return Boolean(binding.dcn_chat_can_send(this.dc_chat))
|
||||
}
|
||||
|
||||
isDeviceTalk(): boolean {
|
||||
return Boolean(binding.dcn_chat_is_device_talk(this.dc_chat))
|
||||
}
|
||||
|
||||
isSingle(): boolean {
|
||||
return this.getType() === C.DC_CHAT_TYPE_SINGLE
|
||||
}
|
||||
|
||||
isGroup(): boolean {
|
||||
return this.getType() === C.DC_CHAT_TYPE_GROUP
|
||||
}
|
||||
|
||||
isMuted(): boolean {
|
||||
return Boolean(binding.dcn_chat_is_muted(this.dc_chat))
|
||||
}
|
||||
|
||||
toJson(): ChatJSON {
|
||||
debug('toJson')
|
||||
const visibility = this.getVisibility()
|
||||
return {
|
||||
archived: visibility === C.DC_CHAT_VISIBILITY_ARCHIVED,
|
||||
pinned: visibility === C.DC_CHAT_VISIBILITY_PINNED,
|
||||
color: this.color,
|
||||
id: this.getId(),
|
||||
name: this.getName(),
|
||||
profileImage: this.getProfileImage(),
|
||||
type: this.getType(),
|
||||
isSelfTalk: this.isSelfTalk(),
|
||||
isUnpromoted: this.isUnpromoted(),
|
||||
isProtected: this.isProtected(),
|
||||
canSend: this.canSend,
|
||||
isDeviceTalk: this.isDeviceTalk(),
|
||||
isContactRequest: this.isContactRequest(),
|
||||
muted: this.isMuted(),
|
||||
}
|
||||
}
|
||||
}
|
||||
42
node/lib/chatlist.ts
Normal file
42
node/lib/chatlist.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import binding from './binding'
|
||||
import { Lot } from './lot'
|
||||
import { Chat } from './chat'
|
||||
const debug = require('debug')('deltachat:node:chatlist')
|
||||
|
||||
interface NativeChatList {}
|
||||
/**
|
||||
* Wrapper around dc_chatlist_t*
|
||||
*/
|
||||
export class ChatList {
|
||||
constructor(private dc_chatlist: NativeChatList) {
|
||||
debug('ChatList constructor')
|
||||
if (dc_chatlist === null) {
|
||||
throw new Error('native chat list can not be null')
|
||||
}
|
||||
}
|
||||
|
||||
getChatId(index: number): number {
|
||||
debug(`getChatId ${index}`)
|
||||
return binding.dcn_chatlist_get_chat_id(this.dc_chatlist, index)
|
||||
}
|
||||
|
||||
getCount(): number {
|
||||
debug('getCount')
|
||||
return binding.dcn_chatlist_get_cnt(this.dc_chatlist)
|
||||
}
|
||||
|
||||
getMessageId(index: number): number {
|
||||
debug(`getMessageId ${index}`)
|
||||
return binding.dcn_chatlist_get_msg_id(this.dc_chatlist, index)
|
||||
}
|
||||
|
||||
getSummary(index: number, chat?: Chat): Lot {
|
||||
debug(`getSummary ${index}`)
|
||||
const dc_chat = (chat && chat.dc_chat) || null
|
||||
return new Lot(
|
||||
binding.dcn_chatlist_get_summary(this.dc_chatlist, index, dc_chat)
|
||||
)
|
||||
}
|
||||
}
|
||||
260
node/lib/constants.ts
Normal file
260
node/lib/constants.ts
Normal file
@@ -0,0 +1,260 @@
|
||||
// Generated!
|
||||
|
||||
export enum C {
|
||||
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3,
|
||||
DC_CERTCK_AUTO = 0,
|
||||
DC_CERTCK_STRICT = 1,
|
||||
DC_CHAT_ID_ALLDONE_HINT = 7,
|
||||
DC_CHAT_ID_ARCHIVED_LINK = 6,
|
||||
DC_CHAT_ID_LAST_SPECIAL = 9,
|
||||
DC_CHAT_ID_TRASH = 3,
|
||||
DC_CHAT_TYPE_BROADCAST = 160,
|
||||
DC_CHAT_TYPE_GROUP = 120,
|
||||
DC_CHAT_TYPE_MAILINGLIST = 140,
|
||||
DC_CHAT_TYPE_SINGLE = 100,
|
||||
DC_CHAT_TYPE_UNDEFINED = 0,
|
||||
DC_CHAT_VISIBILITY_ARCHIVED = 1,
|
||||
DC_CHAT_VISIBILITY_NORMAL = 0,
|
||||
DC_CHAT_VISIBILITY_PINNED = 2,
|
||||
DC_CONNECTIVITY_CONNECTED = 4000,
|
||||
DC_CONNECTIVITY_CONNECTING = 2000,
|
||||
DC_CONNECTIVITY_NOT_CONNECTED = 1000,
|
||||
DC_CONNECTIVITY_WORKING = 3000,
|
||||
DC_CONTACT_ID_DEVICE = 5,
|
||||
DC_CONTACT_ID_INFO = 2,
|
||||
DC_CONTACT_ID_LAST_SPECIAL = 9,
|
||||
DC_CONTACT_ID_SELF = 1,
|
||||
DC_DOWNLOAD_AVAILABLE = 10,
|
||||
DC_DOWNLOAD_DONE = 0,
|
||||
DC_DOWNLOAD_FAILURE = 20,
|
||||
DC_DOWNLOAD_IN_PROGRESS = 1000,
|
||||
DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED = 2021,
|
||||
DC_EVENT_CHAT_MODIFIED = 2020,
|
||||
DC_EVENT_CONFIGURE_PROGRESS = 2041,
|
||||
DC_EVENT_CONNECTIVITY_CHANGED = 2100,
|
||||
DC_EVENT_CONTACTS_CHANGED = 2030,
|
||||
DC_EVENT_DELETED_BLOB_FILE = 151,
|
||||
DC_EVENT_ERROR = 400,
|
||||
DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410,
|
||||
DC_EVENT_IMAP_CONNECTED = 102,
|
||||
DC_EVENT_IMAP_MESSAGE_DELETED = 104,
|
||||
DC_EVENT_IMAP_MESSAGE_MOVED = 105,
|
||||
DC_EVENT_IMEX_FILE_WRITTEN = 2052,
|
||||
DC_EVENT_IMEX_PROGRESS = 2051,
|
||||
DC_EVENT_INCOMING_MSG = 2005,
|
||||
DC_EVENT_INFO = 100,
|
||||
DC_EVENT_LOCATION_CHANGED = 2035,
|
||||
DC_EVENT_MSGS_CHANGED = 2000,
|
||||
DC_EVENT_MSGS_NOTICED = 2008,
|
||||
DC_EVENT_MSG_DELIVERED = 2010,
|
||||
DC_EVENT_MSG_FAILED = 2012,
|
||||
DC_EVENT_MSG_READ = 2015,
|
||||
DC_EVENT_NEW_BLOB_FILE = 150,
|
||||
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060,
|
||||
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061,
|
||||
DC_EVENT_SELFAVATAR_CHANGED = 2110,
|
||||
DC_EVENT_SMTP_CONNECTED = 101,
|
||||
DC_EVENT_SMTP_MESSAGE_SENT = 103,
|
||||
DC_EVENT_WARNING = 300,
|
||||
DC_EVENT_WEBXDC_STATUS_UPDATE = 2120,
|
||||
DC_GCL_ADD_ALLDONE_HINT = 4,
|
||||
DC_GCL_ADD_SELF = 2,
|
||||
DC_GCL_ARCHIVED_ONLY = 1,
|
||||
DC_GCL_FOR_FORWARDING = 8,
|
||||
DC_GCL_NO_SPECIALS = 2,
|
||||
DC_GCL_VERIFIED_ONLY = 1,
|
||||
DC_GCM_ADDDAYMARKER = 1,
|
||||
DC_GCM_INFO_ONLY = 2,
|
||||
DC_IMEX_EXPORT_BACKUP = 11,
|
||||
DC_IMEX_EXPORT_SELF_KEYS = 1,
|
||||
DC_IMEX_IMPORT_BACKUP = 12,
|
||||
DC_IMEX_IMPORT_SELF_KEYS = 2,
|
||||
DC_INFO_PROTECTION_DISABLED = 12,
|
||||
DC_INFO_PROTECTION_ENABLED = 11,
|
||||
DC_KEY_GEN_DEFAULT = 0,
|
||||
DC_KEY_GEN_ED25519 = 2,
|
||||
DC_KEY_GEN_RSA2048 = 1,
|
||||
DC_LP_AUTH_NORMAL = 4,
|
||||
DC_LP_AUTH_OAUTH2 = 2,
|
||||
DC_MEDIA_QUALITY_BALANCED = 0,
|
||||
DC_MEDIA_QUALITY_WORSE = 1,
|
||||
DC_MSG_AUDIO = 40,
|
||||
DC_MSG_FILE = 60,
|
||||
DC_MSG_GIF = 21,
|
||||
DC_MSG_ID_DAYMARKER = 9,
|
||||
DC_MSG_ID_LAST_SPECIAL = 9,
|
||||
DC_MSG_ID_MARKER1 = 1,
|
||||
DC_MSG_IMAGE = 20,
|
||||
DC_MSG_STICKER = 23,
|
||||
DC_MSG_TEXT = 10,
|
||||
DC_MSG_VIDEO = 50,
|
||||
DC_MSG_VIDEOCHAT_INVITATION = 70,
|
||||
DC_MSG_VOICE = 41,
|
||||
DC_MSG_WEBXDC = 80,
|
||||
DC_PROVIDER_STATUS_BROKEN = 3,
|
||||
DC_PROVIDER_STATUS_OK = 1,
|
||||
DC_PROVIDER_STATUS_PREPARATION = 2,
|
||||
DC_QR_ACCOUNT = 250,
|
||||
DC_QR_ADDR = 320,
|
||||
DC_QR_ASK_VERIFYCONTACT = 200,
|
||||
DC_QR_ASK_VERIFYGROUP = 202,
|
||||
DC_QR_ERROR = 400,
|
||||
DC_QR_FPR_MISMATCH = 220,
|
||||
DC_QR_FPR_OK = 210,
|
||||
DC_QR_FPR_WITHOUT_ADDR = 230,
|
||||
DC_QR_REVIVE_VERIFYCONTACT = 510,
|
||||
DC_QR_REVIVE_VERIFYGROUP = 512,
|
||||
DC_QR_TEXT = 330,
|
||||
DC_QR_URL = 332,
|
||||
DC_QR_WEBRTC_INSTANCE = 260,
|
||||
DC_QR_WITHDRAW_VERIFYCONTACT = 500,
|
||||
DC_QR_WITHDRAW_VERIFYGROUP = 502,
|
||||
DC_SHOW_EMAILS_ACCEPTED_CONTACTS = 1,
|
||||
DC_SHOW_EMAILS_ALL = 2,
|
||||
DC_SHOW_EMAILS_OFF = 0,
|
||||
DC_SOCKET_AUTO = 0,
|
||||
DC_SOCKET_PLAIN = 3,
|
||||
DC_SOCKET_SSL = 1,
|
||||
DC_SOCKET_STARTTLS = 2,
|
||||
DC_STATE_IN_FRESH = 10,
|
||||
DC_STATE_IN_NOTICED = 13,
|
||||
DC_STATE_IN_SEEN = 16,
|
||||
DC_STATE_OUT_DELIVERED = 26,
|
||||
DC_STATE_OUT_DRAFT = 19,
|
||||
DC_STATE_OUT_FAILED = 24,
|
||||
DC_STATE_OUT_MDN_RCVD = 28,
|
||||
DC_STATE_OUT_PENDING = 20,
|
||||
DC_STATE_OUT_PREPARING = 18,
|
||||
DC_STATE_UNDEFINED = 0,
|
||||
DC_STR_AC_SETUP_MSG_BODY = 43,
|
||||
DC_STR_AC_SETUP_MSG_SUBJECT = 42,
|
||||
DC_STR_ARCHIVEDCHATS = 40,
|
||||
DC_STR_AUDIO = 11,
|
||||
DC_STR_BAD_TIME_MSG_BODY = 85,
|
||||
DC_STR_BROADCAST_LIST = 115,
|
||||
DC_STR_CANNOT_LOGIN = 60,
|
||||
DC_STR_CANTDECRYPT_MSG_BODY = 29,
|
||||
DC_STR_CONFIGURATION_FAILED = 84,
|
||||
DC_STR_CONNECTED = 107,
|
||||
DC_STR_CONNTECTING = 108,
|
||||
DC_STR_CONTACT_NOT_VERIFIED = 36,
|
||||
DC_STR_CONTACT_SETUP_CHANGED = 37,
|
||||
DC_STR_CONTACT_VERIFIED = 35,
|
||||
DC_STR_DEVICE_MESSAGES = 68,
|
||||
DC_STR_DEVICE_MESSAGES_HINT = 70,
|
||||
DC_STR_DOWNLOAD_AVAILABILITY = 100,
|
||||
DC_STR_DRAFT = 3,
|
||||
DC_STR_E2E_AVAILABLE = 25,
|
||||
DC_STR_E2E_PREFERRED = 34,
|
||||
DC_STR_ENCRYPTEDMSG = 24,
|
||||
DC_STR_ENCR_NONE = 28,
|
||||
DC_STR_ENCR_TRANSP = 27,
|
||||
DC_STR_EPHEMERAL_DAY = 79,
|
||||
DC_STR_EPHEMERAL_DAYS = 95,
|
||||
DC_STR_EPHEMERAL_DISABLED = 75,
|
||||
DC_STR_EPHEMERAL_FOUR_WEEKS = 81,
|
||||
DC_STR_EPHEMERAL_HOUR = 78,
|
||||
DC_STR_EPHEMERAL_HOURS = 94,
|
||||
DC_STR_EPHEMERAL_MINUTE = 77,
|
||||
DC_STR_EPHEMERAL_MINUTES = 93,
|
||||
DC_STR_EPHEMERAL_SECONDS = 76,
|
||||
DC_STR_EPHEMERAL_WEEK = 80,
|
||||
DC_STR_EPHEMERAL_WEEKS = 96,
|
||||
DC_STR_ERROR = 112,
|
||||
DC_STR_ERROR_NO_NETWORK = 87,
|
||||
DC_STR_FAILED_SENDING_TO = 74,
|
||||
DC_STR_FILE = 12,
|
||||
DC_STR_FINGERPRINTS = 30,
|
||||
DC_STR_FORWARDED = 97,
|
||||
DC_STR_GIF = 23,
|
||||
DC_STR_IMAGE = 9,
|
||||
DC_STR_INCOMING_MESSAGES = 103,
|
||||
DC_STR_LAST_MSG_SENT_SUCCESSFULLY = 111,
|
||||
DC_STR_LOCATION = 66,
|
||||
DC_STR_MESSAGES = 114,
|
||||
DC_STR_MSGACTIONBYME = 63,
|
||||
DC_STR_MSGACTIONBYUSER = 62,
|
||||
DC_STR_MSGADDMEMBER = 17,
|
||||
DC_STR_MSGDELMEMBER = 18,
|
||||
DC_STR_MSGGROUPLEFT = 19,
|
||||
DC_STR_MSGGRPIMGCHANGED = 16,
|
||||
DC_STR_MSGGRPIMGDELETED = 33,
|
||||
DC_STR_MSGGRPNAME = 15,
|
||||
DC_STR_MSGLOCATIONDISABLED = 65,
|
||||
DC_STR_MSGLOCATIONENABLED = 64,
|
||||
DC_STR_NOMESSAGES = 1,
|
||||
DC_STR_NOT_CONNECTED = 121,
|
||||
DC_STR_NOT_SUPPORTED_BY_PROVIDER = 113,
|
||||
DC_STR_ONE_MOMENT = 106,
|
||||
DC_STR_OUTGOING_MESSAGES = 104,
|
||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
|
||||
DC_STR_PART_OF_TOTAL_USED = 116,
|
||||
DC_STR_PROTECTION_DISABLED = 89,
|
||||
DC_STR_PROTECTION_ENABLED = 88,
|
||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
|
||||
DC_STR_READRCPT = 31,
|
||||
DC_STR_READRCPT_MAILBODY = 32,
|
||||
DC_STR_REPLY_NOUN = 90,
|
||||
DC_STR_SAVED_MESSAGES = 69,
|
||||
DC_STR_SECURE_JOIN_GROUP_QR_DESC = 120,
|
||||
DC_STR_SECURE_JOIN_REPLIES = 118,
|
||||
DC_STR_SECURE_JOIN_STARTED = 117,
|
||||
DC_STR_SELF = 2,
|
||||
DC_STR_SELF_DELETED_MSG_BODY = 91,
|
||||
DC_STR_SENDING = 110,
|
||||
DC_STR_SERVER_TURNED_OFF = 92,
|
||||
DC_STR_SETUP_CONTACT_QR_DESC = 119,
|
||||
DC_STR_STICKER = 67,
|
||||
DC_STR_STORAGE_ON_DOMAIN = 105,
|
||||
DC_STR_SUBJECT_FOR_NEW_CONTACT = 73,
|
||||
DC_STR_SYNC_MSG_BODY = 102,
|
||||
DC_STR_SYNC_MSG_SUBJECT = 101,
|
||||
DC_STR_UNKNOWN_SENDER_FOR_CHAT = 72,
|
||||
DC_STR_UPDATE_REMINDER_MSG_BODY = 86,
|
||||
DC_STR_UPDATING = 109,
|
||||
DC_STR_VIDEO = 10,
|
||||
DC_STR_VIDEOCHAT_INVITATION = 82,
|
||||
DC_STR_VIDEOCHAT_INVITE_MSG_BODY = 83,
|
||||
DC_STR_VOICEMESSAGE = 7,
|
||||
DC_STR_WELCOME_MESSAGE = 71,
|
||||
DC_TEXT1_DRAFT = 1,
|
||||
DC_TEXT1_SELF = 3,
|
||||
DC_TEXT1_USERNAME = 2,
|
||||
DC_VIDEOCHATTYPE_BASICWEBRTC = 1,
|
||||
DC_VIDEOCHATTYPE_JITSI = 2,
|
||||
DC_VIDEOCHATTYPE_UNKNOWN = 0,
|
||||
}
|
||||
|
||||
// Generated!
|
||||
|
||||
export const EventId2EventName: { [key: number]: string } = {
|
||||
100: 'DC_EVENT_INFO',
|
||||
101: 'DC_EVENT_SMTP_CONNECTED',
|
||||
102: 'DC_EVENT_IMAP_CONNECTED',
|
||||
103: 'DC_EVENT_SMTP_MESSAGE_SENT',
|
||||
104: 'DC_EVENT_IMAP_MESSAGE_DELETED',
|
||||
105: 'DC_EVENT_IMAP_MESSAGE_MOVED',
|
||||
150: 'DC_EVENT_NEW_BLOB_FILE',
|
||||
151: 'DC_EVENT_DELETED_BLOB_FILE',
|
||||
300: 'DC_EVENT_WARNING',
|
||||
400: 'DC_EVENT_ERROR',
|
||||
410: 'DC_EVENT_ERROR_SELF_NOT_IN_GROUP',
|
||||
2000: 'DC_EVENT_MSGS_CHANGED',
|
||||
2005: 'DC_EVENT_INCOMING_MSG',
|
||||
2008: 'DC_EVENT_MSGS_NOTICED',
|
||||
2010: 'DC_EVENT_MSG_DELIVERED',
|
||||
2012: 'DC_EVENT_MSG_FAILED',
|
||||
2015: 'DC_EVENT_MSG_READ',
|
||||
2020: 'DC_EVENT_CHAT_MODIFIED',
|
||||
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
|
||||
2030: 'DC_EVENT_CONTACTS_CHANGED',
|
||||
2035: 'DC_EVENT_LOCATION_CHANGED',
|
||||
2041: 'DC_EVENT_CONFIGURE_PROGRESS',
|
||||
2051: 'DC_EVENT_IMEX_PROGRESS',
|
||||
2052: 'DC_EVENT_IMEX_FILE_WRITTEN',
|
||||
2060: 'DC_EVENT_SECUREJOIN_INVITER_PROGRESS',
|
||||
2061: 'DC_EVENT_SECUREJOIN_JOINER_PROGRESS',
|
||||
2100: 'DC_EVENT_CONNECTIVITY_CHANGED',
|
||||
2110: 'DC_EVENT_SELFAVATAR_CHANGED',
|
||||
2120: 'DC_EVENT_WEBXDC_STATUS_UPDATE',
|
||||
}
|
||||
94
node/lib/contact.ts
Normal file
94
node/lib/contact.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { integerToHexColor } from './util'
|
||||
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import binding from './binding'
|
||||
const debug = require('debug')('deltachat:node:contact')
|
||||
|
||||
interface NativeContact {}
|
||||
/**
|
||||
* Wrapper around dc_contact_t*
|
||||
*/
|
||||
export class Contact {
|
||||
constructor(public dc_contact: NativeContact) {
|
||||
debug('Contact constructor')
|
||||
if (dc_contact === null) {
|
||||
throw new Error('native contact can not be null')
|
||||
}
|
||||
}
|
||||
|
||||
toJson() {
|
||||
debug('toJson')
|
||||
return {
|
||||
address: this.getAddress(),
|
||||
color: this.color,
|
||||
authName: this.authName,
|
||||
status: this.status,
|
||||
displayName: this.getDisplayName(),
|
||||
id: this.getId(),
|
||||
lastSeen: this.lastSeen,
|
||||
name: this.getName(),
|
||||
profileImage: this.getProfileImage(),
|
||||
nameAndAddr: this.getNameAndAddress(),
|
||||
isBlocked: this.isBlocked(),
|
||||
isVerified: this.isVerified(),
|
||||
}
|
||||
}
|
||||
|
||||
getAddress(): string {
|
||||
return binding.dcn_contact_get_addr(this.dc_contact)
|
||||
}
|
||||
|
||||
/** Get original contact name.
|
||||
* This is the name of the contact as defined by the contact themself.
|
||||
* If the contact themself does not define such a name,
|
||||
* an empty string is returned. */
|
||||
get authName(): string {
|
||||
return binding.dcn_contact_get_auth_name(this.dc_contact)
|
||||
}
|
||||
|
||||
get color(): string {
|
||||
return integerToHexColor(binding.dcn_contact_get_color(this.dc_contact))
|
||||
}
|
||||
|
||||
/**
|
||||
* contact's status
|
||||
*
|
||||
* Status is the last signature received in a message from this contact.
|
||||
*/
|
||||
get status(): string {
|
||||
return binding.dcn_contact_get_status(this.dc_contact)
|
||||
}
|
||||
|
||||
getDisplayName(): string {
|
||||
return binding.dcn_contact_get_display_name(this.dc_contact)
|
||||
}
|
||||
|
||||
getId(): number {
|
||||
return binding.dcn_contact_get_id(this.dc_contact)
|
||||
}
|
||||
|
||||
get lastSeen(): number {
|
||||
return binding.dcn_contact_get_last_seen(this.dc_contact)
|
||||
}
|
||||
|
||||
getName(): string {
|
||||
return binding.dcn_contact_get_name(this.dc_contact)
|
||||
}
|
||||
|
||||
getNameAndAddress(): string {
|
||||
return binding.dcn_contact_get_name_n_addr(this.dc_contact)
|
||||
}
|
||||
|
||||
getProfileImage(): string {
|
||||
return binding.dcn_contact_get_profile_image(this.dc_contact)
|
||||
}
|
||||
|
||||
isBlocked() {
|
||||
return Boolean(binding.dcn_contact_is_blocked(this.dc_contact))
|
||||
}
|
||||
|
||||
isVerified() {
|
||||
return Boolean(binding.dcn_contact_is_verified(this.dc_contact))
|
||||
}
|
||||
}
|
||||
955
node/lib/context.ts
Normal file
955
node/lib/context.ts
Normal file
@@ -0,0 +1,955 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import binding from './binding'
|
||||
import { C, EventId2EventName } from './constants'
|
||||
import { Chat } from './chat'
|
||||
import { ChatList } from './chatlist'
|
||||
import { Contact } from './contact'
|
||||
import { Message } from './message'
|
||||
import { Lot } from './lot'
|
||||
import { Locations } from './locations'
|
||||
import rawDebug from 'debug'
|
||||
import { AccountManager } from './deltachat'
|
||||
import { join } from 'path'
|
||||
import { EventEmitter } from 'stream'
|
||||
const debug = rawDebug('deltachat:node:index')
|
||||
|
||||
const noop = function () {}
|
||||
interface NativeContext {}
|
||||
|
||||
/**
|
||||
* Wrapper around dcn_context_t*
|
||||
*
|
||||
* only acts as event emitter when created in standalone mode (without account manager)
|
||||
* with `Context.open`
|
||||
*/
|
||||
export class Context extends EventEmitter {
|
||||
constructor(
|
||||
readonly manager: AccountManager | null,
|
||||
private inner_dcn_context: NativeContext,
|
||||
readonly account_id: number | null
|
||||
) {
|
||||
super()
|
||||
debug('DeltaChat constructor')
|
||||
if (inner_dcn_context === null) {
|
||||
throw new Error('inner_dcn_context can not be null')
|
||||
}
|
||||
}
|
||||
|
||||
/** Opens a stanalone context (without an account manager)
|
||||
* automatically starts the event handler */
|
||||
static open(cwd: string): Context {
|
||||
const dbFile = join(cwd, 'db.sqlite')
|
||||
const context = new Context(null, binding.dcn_context_new(dbFile), null)
|
||||
debug('Opened context')
|
||||
function handleCoreEvent(
|
||||
eventId: number,
|
||||
data1: number,
|
||||
data2: number | string
|
||||
) {
|
||||
const eventString = EventId2EventName[eventId]
|
||||
debug(eventString, data1, data2)
|
||||
if (!context.emit) {
|
||||
console.log('Received an event but EventEmitter is already destroyed.')
|
||||
console.log(eventString, data1, data2)
|
||||
return
|
||||
}
|
||||
context.emit(eventString, data1, data2)
|
||||
context.emit('ALL', eventString, data1, data2)
|
||||
}
|
||||
binding.dcn_start_event_handler(
|
||||
context.dcn_context,
|
||||
handleCoreEvent.bind(this)
|
||||
)
|
||||
debug('Started event handler')
|
||||
return context
|
||||
}
|
||||
|
||||
get dcn_context() {
|
||||
return this.inner_dcn_context
|
||||
}
|
||||
|
||||
get is_open() {
|
||||
return Boolean(binding.dcn_context_is_open())
|
||||
}
|
||||
|
||||
open(passphrase?: string) {
|
||||
return Boolean(
|
||||
binding.dcn_context_open(this.dcn_context, passphrase ? passphrase : '')
|
||||
)
|
||||
}
|
||||
|
||||
unref() {
|
||||
binding.dcn_context_unref(this.dcn_context)
|
||||
;(this.inner_dcn_context as any) = null
|
||||
}
|
||||
|
||||
acceptChat(chatId: number) {
|
||||
binding.dcn_accept_chat(this.dcn_context, chatId)
|
||||
}
|
||||
|
||||
blockChat(chatId: number) {
|
||||
binding.dcn_block_chat(this.dcn_context, chatId)
|
||||
}
|
||||
|
||||
addAddressBook(addressBook: string) {
|
||||
debug(`addAddressBook ${addressBook}`)
|
||||
return binding.dcn_add_address_book(this.dcn_context, addressBook)
|
||||
}
|
||||
|
||||
addContactToChat(chatId: number, contactId: number) {
|
||||
debug(`addContactToChat ${chatId} ${contactId}`)
|
||||
return Boolean(
|
||||
binding.dcn_add_contact_to_chat(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
Number(contactId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
addDeviceMessage(label: string, msg: Message | string) {
|
||||
debug(`addDeviceMessage ${label} ${msg}`)
|
||||
if (!msg) {
|
||||
throw new Error('invalid msg parameter')
|
||||
}
|
||||
if (typeof label !== 'string') {
|
||||
throw new Error('invalid label parameter, must be a string')
|
||||
}
|
||||
if (typeof msg === 'string') {
|
||||
const msgObj = this.messageNew()
|
||||
msgObj.setText(msg)
|
||||
msg = msgObj
|
||||
}
|
||||
if (!msg.dc_msg) {
|
||||
throw new Error('invalid msg object')
|
||||
}
|
||||
return binding.dcn_add_device_msg(this.dcn_context, label, msg.dc_msg)
|
||||
}
|
||||
|
||||
setChatVisibility(
|
||||
chatId: number,
|
||||
visibility:
|
||||
| C.DC_CHAT_VISIBILITY_NORMAL
|
||||
| C.DC_CHAT_VISIBILITY_ARCHIVED
|
||||
| C.DC_CHAT_VISIBILITY_PINNED
|
||||
) {
|
||||
debug(`setChatVisibility ${chatId} ${visibility}`)
|
||||
binding.dcn_set_chat_visibility(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
visibility
|
||||
)
|
||||
}
|
||||
|
||||
blockContact(contactId: number, block: boolean) {
|
||||
debug(`blockContact ${contactId} ${block}`)
|
||||
binding.dcn_block_contact(
|
||||
this.dcn_context,
|
||||
Number(contactId),
|
||||
block ? 1 : 0
|
||||
)
|
||||
}
|
||||
|
||||
checkQrCode(qrCode: string) {
|
||||
debug(`checkQrCode ${qrCode}`)
|
||||
const dc_lot = binding.dcn_check_qr(this.dcn_context, qrCode)
|
||||
let result = dc_lot ? new Lot(dc_lot) : null
|
||||
if (result) {
|
||||
return { id: result.getId(), ...result.toJson() }
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
configure(opts: any): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
debug('configure')
|
||||
|
||||
const onSuccess = () => {
|
||||
removeListeners()
|
||||
resolve()
|
||||
}
|
||||
const onFail = (error: string) => {
|
||||
removeListeners()
|
||||
reject(new Error(error))
|
||||
}
|
||||
|
||||
let onConfigure: (...args: any[]) => void
|
||||
if (this.account_id === null) {
|
||||
onConfigure = (data1: number, data2: string) => {
|
||||
if (data1 === 0) return onFail(data2)
|
||||
else if (data1 === 1000) return onSuccess()
|
||||
}
|
||||
} else {
|
||||
onConfigure = (accountId: number, data1: number, data2: string) => {
|
||||
if (this.account_id !== accountId) {
|
||||
return
|
||||
}
|
||||
if (data1 === 0) return onFail(data2)
|
||||
else if (data1 === 1000) return onSuccess()
|
||||
}
|
||||
}
|
||||
|
||||
const removeListeners = () => {
|
||||
;(this.manager || this).removeListener(
|
||||
'DC_EVENT_CONFIGURE_PROGRESS',
|
||||
onConfigure
|
||||
)
|
||||
}
|
||||
|
||||
const registerListeners = () => {
|
||||
;(this.manager || this).on('DC_EVENT_CONFIGURE_PROGRESS', onConfigure)
|
||||
}
|
||||
|
||||
registerListeners()
|
||||
|
||||
if (!opts) opts = {}
|
||||
Object.keys(opts).forEach((key) => {
|
||||
const value = opts[key]
|
||||
this.setConfig(key, value)
|
||||
})
|
||||
|
||||
binding.dcn_configure(this.dcn_context)
|
||||
})
|
||||
}
|
||||
|
||||
continueKeyTransfer(messageId: number, setupCode: string) {
|
||||
debug(`continueKeyTransfer ${messageId}`)
|
||||
return new Promise((resolve, reject) => {
|
||||
binding.dcn_continue_key_transfer(
|
||||
this.dcn_context,
|
||||
Number(messageId),
|
||||
setupCode,
|
||||
(result: number) => resolve(result === 1)
|
||||
)
|
||||
})
|
||||
}
|
||||
/** @returns chatId */
|
||||
createBroadcastList(): number {
|
||||
debug(`createBroadcastList`)
|
||||
return binding.dcn_create_broadcast_list(this.dcn_context)
|
||||
}
|
||||
|
||||
/** @returns chatId */
|
||||
createChatByContactId(contactId: number): number {
|
||||
debug(`createChatByContactId ${contactId}`)
|
||||
return binding.dcn_create_chat_by_contact_id(
|
||||
this.dcn_context,
|
||||
Number(contactId)
|
||||
)
|
||||
}
|
||||
|
||||
/** @returns contactId */
|
||||
createContact(name: string, addr: string): number {
|
||||
debug(`createContact ${name} ${addr}`)
|
||||
return binding.dcn_create_contact(this.dcn_context, name, addr)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param chatName The name of the chat that should be created
|
||||
* @param is_protected Whether the chat should be protected at creation time
|
||||
* @returns chatId
|
||||
*/
|
||||
createGroupChat(chatName: string, is_protected: boolean = false): number {
|
||||
debug(`createGroupChat ${chatName} [protected:${is_protected}]`)
|
||||
return binding.dcn_create_group_chat(
|
||||
this.dcn_context,
|
||||
is_protected ? 1 : 0,
|
||||
chatName
|
||||
)
|
||||
}
|
||||
|
||||
deleteChat(chatId: number) {
|
||||
debug(`deleteChat ${chatId}`)
|
||||
binding.dcn_delete_chat(this.dcn_context, Number(chatId))
|
||||
}
|
||||
|
||||
deleteContact(contactId: number) {
|
||||
debug(`deleteContact ${contactId}`)
|
||||
return Boolean(
|
||||
binding.dcn_delete_contact(this.dcn_context, Number(contactId))
|
||||
)
|
||||
}
|
||||
|
||||
deleteMessages(messageIds: number[]) {
|
||||
if (!Array.isArray(messageIds)) {
|
||||
messageIds = [messageIds]
|
||||
}
|
||||
messageIds = messageIds.map((id) => Number(id))
|
||||
debug('deleteMessages', messageIds)
|
||||
binding.dcn_delete_msgs(this.dcn_context, messageIds)
|
||||
}
|
||||
|
||||
forwardMessages(messageIds: number[], chatId: number) {
|
||||
if (!Array.isArray(messageIds)) {
|
||||
messageIds = [messageIds]
|
||||
}
|
||||
messageIds = messageIds.map((id) => Number(id))
|
||||
debug('forwardMessages', messageIds)
|
||||
binding.dcn_forward_msgs(this.dcn_context, messageIds, chatId)
|
||||
}
|
||||
|
||||
getBlobdir(): string {
|
||||
debug('getBlobdir')
|
||||
return binding.dcn_get_blobdir(this.dcn_context)
|
||||
}
|
||||
|
||||
getBlockedCount(): number {
|
||||
debug('getBlockedCount')
|
||||
return binding.dcn_get_blocked_cnt(this.dcn_context)
|
||||
}
|
||||
|
||||
getBlockedContacts(): number[] {
|
||||
debug('getBlockedContacts')
|
||||
return binding.dcn_get_blocked_contacts(this.dcn_context)
|
||||
}
|
||||
|
||||
getChat(chatId: number) {
|
||||
debug(`getChat ${chatId}`)
|
||||
const dc_chat = binding.dcn_get_chat(this.dcn_context, Number(chatId))
|
||||
return dc_chat ? new Chat(dc_chat) : null
|
||||
}
|
||||
|
||||
getChatContacts(chatId: number): number[] {
|
||||
debug(`getChatContacts ${chatId}`)
|
||||
return binding.dcn_get_chat_contacts(this.dcn_context, Number(chatId))
|
||||
}
|
||||
|
||||
getChatIdByContactId(contactId: number): number {
|
||||
debug(`getChatIdByContactId ${contactId}`)
|
||||
return binding.dcn_get_chat_id_by_contact_id(
|
||||
this.dcn_context,
|
||||
Number(contactId)
|
||||
)
|
||||
}
|
||||
|
||||
getChatMedia(
|
||||
chatId: number,
|
||||
msgType1: number,
|
||||
msgType2: number,
|
||||
msgType3: number
|
||||
): number[] {
|
||||
debug(`getChatMedia ${chatId}`)
|
||||
return binding.dcn_get_chat_media(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
msgType1,
|
||||
msgType2 || 0,
|
||||
msgType3 || 0
|
||||
)
|
||||
}
|
||||
|
||||
getMimeHeaders(messageId: number): string {
|
||||
debug(`getMimeHeaders ${messageId}`)
|
||||
return binding.dcn_get_mime_headers(this.dcn_context, Number(messageId))
|
||||
}
|
||||
|
||||
getChatlistItemSummary(chatId: number, messageId: number) {
|
||||
debug(`getChatlistItemSummary ${chatId} ${messageId}`)
|
||||
return new Lot(
|
||||
binding.dcn_chatlist_get_summary2(this.dcn_context, chatId, messageId)
|
||||
)
|
||||
}
|
||||
|
||||
getChatMessages(chatId: number, flags: number, marker1before: number) {
|
||||
debug(`getChatMessages ${chatId} ${flags} ${marker1before}`)
|
||||
return binding.dcn_get_chat_msgs(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
flags,
|
||||
marker1before
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get encryption info for a chat.
|
||||
* Get a multi-line encryption info, containing encryption preferences of all members.
|
||||
* Can be used to find out why messages sent to group are not encrypted.
|
||||
*
|
||||
* @param chatId ID of the chat to get the encryption info for.
|
||||
* @return Multi-line text, must be released using dc_str_unref() after usage.
|
||||
*/
|
||||
getChatEncrytionInfo(chatId: number): string {
|
||||
return binding.dcn_get_chat_encrinfo(this.dcn_context, chatId)
|
||||
}
|
||||
|
||||
getChats(listFlags: number, queryStr: string, queryContactId: number) {
|
||||
debug('getChats')
|
||||
const result = []
|
||||
const list = this.getChatList(listFlags, queryStr, queryContactId)
|
||||
const count = list.getCount()
|
||||
for (let i = 0; i < count; i++) {
|
||||
result.push(list.getChatId(i))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
getChatList(listFlags: number, queryStr: string, queryContactId: number) {
|
||||
listFlags = listFlags || 0
|
||||
queryStr = queryStr || ''
|
||||
queryContactId = queryContactId || 0
|
||||
debug(`getChatList ${listFlags} ${queryStr} ${queryContactId}`)
|
||||
return new ChatList(
|
||||
binding.dcn_get_chatlist(
|
||||
this.dcn_context,
|
||||
listFlags,
|
||||
queryStr,
|
||||
Number(queryContactId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
getConfig(key: string): string {
|
||||
debug(`getConfig ${key}`)
|
||||
return binding.dcn_get_config(this.dcn_context, key)
|
||||
}
|
||||
|
||||
getContact(contactId: number) {
|
||||
debug(`getContact ${contactId}`)
|
||||
const dc_contact = binding.dcn_get_contact(
|
||||
this.dcn_context,
|
||||
Number(contactId)
|
||||
)
|
||||
return dc_contact ? new Contact(dc_contact) : null
|
||||
}
|
||||
|
||||
getContactEncryptionInfo(contactId: number) {
|
||||
debug(`getContactEncryptionInfo ${contactId}`)
|
||||
return binding.dcn_get_contact_encrinfo(this.dcn_context, Number(contactId))
|
||||
}
|
||||
|
||||
getContacts(listFlags: number, query: string) {
|
||||
listFlags = listFlags || 0
|
||||
query = query || ''
|
||||
debug(`getContacts ${listFlags} ${query}`)
|
||||
return binding.dcn_get_contacts(this.dcn_context, listFlags, query)
|
||||
}
|
||||
|
||||
wasDeviceMessageEverAdded(label: string) {
|
||||
debug(`wasDeviceMessageEverAdded ${label}`)
|
||||
const added = binding.dcn_was_device_msg_ever_added(this.dcn_context, label)
|
||||
return added === 1
|
||||
}
|
||||
|
||||
getDraft(chatId: number) {
|
||||
debug(`getDraft ${chatId}`)
|
||||
const dc_msg = binding.dcn_get_draft(this.dcn_context, Number(chatId))
|
||||
return dc_msg ? new Message(dc_msg) : null
|
||||
}
|
||||
|
||||
getFreshMessageCount(chatId: number): number {
|
||||
debug(`getFreshMessageCount ${chatId}`)
|
||||
return binding.dcn_get_fresh_msg_cnt(this.dcn_context, Number(chatId))
|
||||
}
|
||||
|
||||
getFreshMessages() {
|
||||
debug('getFreshMessages')
|
||||
return binding.dcn_get_fresh_msgs(this.dcn_context)
|
||||
}
|
||||
|
||||
getInfo() {
|
||||
debug('getInfo')
|
||||
const info = binding.dcn_get_info(this.dcn_context)
|
||||
return AccountManager.parseGetInfo(info)
|
||||
}
|
||||
|
||||
getMessage(messageId: number) {
|
||||
debug(`getMessage ${messageId}`)
|
||||
const dc_msg = binding.dcn_get_msg(this.dcn_context, Number(messageId))
|
||||
return dc_msg ? new Message(dc_msg) : null
|
||||
}
|
||||
|
||||
getMessageCount(chatId: number): number {
|
||||
debug(`getMessageCount ${chatId}`)
|
||||
return binding.dcn_get_msg_cnt(this.dcn_context, Number(chatId))
|
||||
}
|
||||
|
||||
getMessageInfo(messageId: number): string {
|
||||
debug(`getMessageInfo ${messageId}`)
|
||||
return binding.dcn_get_msg_info(this.dcn_context, Number(messageId))
|
||||
}
|
||||
|
||||
getMessageHTML(messageId: number): string {
|
||||
debug(`getMessageHTML ${messageId}`)
|
||||
return binding.dcn_get_msg_html(this.dcn_context, Number(messageId))
|
||||
}
|
||||
|
||||
getNextMediaMessage(
|
||||
messageId: number,
|
||||
msgType1: number,
|
||||
msgType2: number,
|
||||
msgType3: number
|
||||
) {
|
||||
debug(
|
||||
`getNextMediaMessage ${messageId} ${msgType1} ${msgType2} ${msgType3}`
|
||||
)
|
||||
return this._getNextMedia(messageId, 1, msgType1, msgType2, msgType3)
|
||||
}
|
||||
|
||||
getPreviousMediaMessage(
|
||||
messageId: number,
|
||||
msgType1: number,
|
||||
msgType2: number,
|
||||
msgType3: number
|
||||
) {
|
||||
debug(
|
||||
`getPreviousMediaMessage ${messageId} ${msgType1} ${msgType2} ${msgType3}`
|
||||
)
|
||||
return this._getNextMedia(messageId, -1, msgType1, msgType2, msgType3)
|
||||
}
|
||||
|
||||
_getNextMedia(
|
||||
messageId: number,
|
||||
dir: number,
|
||||
msgType1: number,
|
||||
msgType2: number,
|
||||
msgType3: number
|
||||
): number {
|
||||
return binding.dcn_get_next_media(
|
||||
this.dcn_context,
|
||||
Number(messageId),
|
||||
dir,
|
||||
msgType1 || 0,
|
||||
msgType2 || 0,
|
||||
msgType3 || 0
|
||||
)
|
||||
}
|
||||
|
||||
getSecurejoinQrCode(chatId: number): string {
|
||||
debug(`getSecurejoinQrCode ${chatId}`)
|
||||
return binding.dcn_get_securejoin_qr(this.dcn_context, Number(chatId))
|
||||
}
|
||||
|
||||
getSecurejoinQrCodeSVG(chatId: number): string {
|
||||
debug(`getSecurejoinQrCodeSVG ${chatId}`)
|
||||
return binding.dcn_get_securejoin_qr_svg(this.dcn_context, chatId)
|
||||
}
|
||||
|
||||
startIO(): void {
|
||||
debug(`startIO`)
|
||||
binding.dcn_start_io(this.dcn_context)
|
||||
}
|
||||
|
||||
stopIO(): void {
|
||||
debug(`stopIO`)
|
||||
binding.dcn_stop_io(this.dcn_context)
|
||||
}
|
||||
|
||||
stopOngoingProcess(): void {
|
||||
debug(`stopOngoingProcess`)
|
||||
binding.dcn_stop_ongoing_process(this.dcn_context)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @deprectated please use `AccountManager.getSystemInfo()` instead
|
||||
*/
|
||||
static getSystemInfo() {
|
||||
return AccountManager.getSystemInfo()
|
||||
}
|
||||
|
||||
getConnectivity(): number {
|
||||
return binding.dcn_get_connectivity(this.dcn_context)
|
||||
}
|
||||
|
||||
getConnectivityHTML(): String {
|
||||
return binding.dcn_get_connectivity_html(this.dcn_context)
|
||||
}
|
||||
|
||||
importExport(what: number, param1: string, param2 = '') {
|
||||
debug(`importExport ${what} ${param1} ${param2}`)
|
||||
binding.dcn_imex(this.dcn_context, what, param1, param2)
|
||||
}
|
||||
|
||||
importExportHasBackup(dir: string) {
|
||||
debug(`importExportHasBackup ${dir}`)
|
||||
return binding.dcn_imex_has_backup(this.dcn_context, dir)
|
||||
}
|
||||
|
||||
initiateKeyTransfer(): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
debug('initiateKeyTransfer2')
|
||||
binding.dcn_initiate_key_transfer(this.dcn_context, resolve)
|
||||
})
|
||||
}
|
||||
|
||||
isConfigured() {
|
||||
debug('isConfigured')
|
||||
return Boolean(binding.dcn_is_configured(this.dcn_context))
|
||||
}
|
||||
|
||||
isContactInChat(chatId: number, contactId: number) {
|
||||
debug(`isContactInChat ${chatId} ${contactId}`)
|
||||
return Boolean(
|
||||
binding.dcn_is_contact_in_chat(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
Number(contactId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns resulting chat id or 0 on error
|
||||
*/
|
||||
joinSecurejoin(qrCode: string): number {
|
||||
debug(`joinSecurejoin ${qrCode}`)
|
||||
return binding.dcn_join_securejoin(this.dcn_context, qrCode)
|
||||
}
|
||||
|
||||
lookupContactIdByAddr(addr: string): number {
|
||||
debug(`lookupContactIdByAddr ${addr}`)
|
||||
return binding.dcn_lookup_contact_id_by_addr(this.dcn_context, addr)
|
||||
}
|
||||
|
||||
markNoticedChat(chatId: number) {
|
||||
debug(`markNoticedChat ${chatId}`)
|
||||
binding.dcn_marknoticed_chat(this.dcn_context, Number(chatId))
|
||||
}
|
||||
|
||||
markSeenMessages(messageIds: number[]) {
|
||||
if (!Array.isArray(messageIds)) {
|
||||
messageIds = [messageIds]
|
||||
}
|
||||
messageIds = messageIds.map((id) => Number(id))
|
||||
debug('markSeenMessages', messageIds)
|
||||
binding.dcn_markseen_msgs(this.dcn_context, messageIds)
|
||||
}
|
||||
|
||||
maybeNetwork() {
|
||||
debug('maybeNetwork')
|
||||
binding.dcn_maybe_network(this.dcn_context)
|
||||
}
|
||||
|
||||
messageNew(viewType = C.DC_MSG_TEXT) {
|
||||
debug(`messageNew ${viewType}`)
|
||||
return new Message(binding.dcn_msg_new(this.dcn_context, viewType))
|
||||
}
|
||||
|
||||
removeContactFromChat(chatId: number, contactId: number) {
|
||||
debug(`removeContactFromChat ${chatId} ${contactId}`)
|
||||
return Boolean(
|
||||
binding.dcn_remove_contact_from_chat(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
Number(contactId)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param chatId ID of the chat to search messages in. Set this to 0 for a global search.
|
||||
* @param query The query to search for.
|
||||
*/
|
||||
searchMessages(chatId: number, query: string): number[] {
|
||||
debug(`searchMessages ${chatId} ${query}`)
|
||||
return binding.dcn_search_msgs(this.dcn_context, Number(chatId), query)
|
||||
}
|
||||
|
||||
sendMessage(chatId: number, msg: string | Message) {
|
||||
debug(`sendMessage ${chatId}`)
|
||||
if (!msg) {
|
||||
throw new Error('invalid msg parameter')
|
||||
}
|
||||
if (typeof msg === 'string') {
|
||||
const msgObj = this.messageNew()
|
||||
msgObj.setText(msg)
|
||||
msg = msgObj
|
||||
}
|
||||
if (!msg.dc_msg) {
|
||||
throw new Error('invalid msg object')
|
||||
}
|
||||
return binding.dcn_send_msg(this.dcn_context, Number(chatId), msg.dc_msg)
|
||||
}
|
||||
|
||||
downloadFullMessage(messageId: number) {
|
||||
binding.dcn_download_full_msg(this.dcn_context, messageId)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @returns {Promise<number>} Promise that resolves into the resulting message id
|
||||
*/
|
||||
sendVideochatInvitation(chatId: number): Promise<number> {
|
||||
debug(`sendVideochatInvitation ${chatId}`)
|
||||
return new Promise((resolve, reject) => {
|
||||
binding.dcn_send_videochat_invitation(
|
||||
this.dcn_context,
|
||||
chatId,
|
||||
(result: number) => {
|
||||
if (result !== 0) {
|
||||
resolve(result)
|
||||
} else {
|
||||
reject(
|
||||
'Videochatinvitation failed to send, see error events for detailed info'
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
setChatName(chatId: number, name: string) {
|
||||
debug(`setChatName ${chatId} ${name}`)
|
||||
return Boolean(
|
||||
binding.dcn_set_chat_name(this.dcn_context, Number(chatId), name)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param chatId
|
||||
* @param protect
|
||||
* @returns success boolean
|
||||
*/
|
||||
setChatProtection(chatId: number, protect: boolean) {
|
||||
debug(`setChatProtection ${chatId} ${protect}`)
|
||||
return Boolean(
|
||||
binding.dcn_set_chat_protection(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
protect ? 1 : 0
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
getChatEphemeralTimer(chatId: number): number {
|
||||
debug(`getChatEphemeralTimer ${chatId}`)
|
||||
return binding.dcn_get_chat_ephemeral_timer(
|
||||
this.dcn_context,
|
||||
Number(chatId)
|
||||
)
|
||||
}
|
||||
|
||||
setChatEphemeralTimer(chatId: number, timer: number) {
|
||||
debug(`setChatEphemeralTimer ${chatId} ${timer}`)
|
||||
return Boolean(
|
||||
binding.dcn_set_chat_ephemeral_timer(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
Number(timer)
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
setChatProfileImage(chatId: number, image: string) {
|
||||
debug(`setChatProfileImage ${chatId} ${image}`)
|
||||
return Boolean(
|
||||
binding.dcn_set_chat_profile_image(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
image || ''
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
setConfig(key: string, value: string | boolean | number): number {
|
||||
debug(`setConfig (string) ${key} ${value}`)
|
||||
if (value === null) {
|
||||
return binding.dcn_set_config_null(this.dcn_context, key)
|
||||
} else {
|
||||
if (typeof value === 'boolean') {
|
||||
value = value === true ? '1' : '0'
|
||||
} else if (typeof value === 'number') {
|
||||
value = String(value)
|
||||
}
|
||||
return binding.dcn_set_config(this.dcn_context, key, value)
|
||||
}
|
||||
}
|
||||
|
||||
setConfigFromQr(qrcodeContent: string): boolean {
|
||||
return Boolean(
|
||||
binding.dcn_set_config_from_qr(this.dcn_context, qrcodeContent)
|
||||
)
|
||||
}
|
||||
|
||||
estimateDeletionCount(fromServer: boolean, seconds: number): number {
|
||||
debug(`estimateDeletionCount fromServer: ${fromServer} seconds: ${seconds}`)
|
||||
return binding.dcn_estimate_deletion_cnt(
|
||||
this.dcn_context,
|
||||
fromServer === true ? 1 : 0,
|
||||
seconds
|
||||
)
|
||||
}
|
||||
|
||||
setStockTranslation(stockId: number, stockMsg: string) {
|
||||
debug(`setStockTranslation ${stockId} ${stockMsg}`)
|
||||
return Boolean(
|
||||
binding.dcn_set_stock_translation(
|
||||
this.dcn_context,
|
||||
Number(stockId),
|
||||
stockMsg
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
setDraft(chatId: number, msg: Message | null) {
|
||||
debug(`setDraft ${chatId}`)
|
||||
binding.dcn_set_draft(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
msg ? msg.dc_msg : null
|
||||
)
|
||||
}
|
||||
|
||||
setLocation(latitude: number, longitude: number, accuracy: number) {
|
||||
debug(`setLocation ${latitude}`)
|
||||
binding.dcn_set_location(
|
||||
this.dcn_context,
|
||||
Number(latitude),
|
||||
Number(longitude),
|
||||
Number(accuracy)
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* @param chatId Chat-id to get location information for.
|
||||
* 0 to get locations independently of the chat.
|
||||
* @param contactId Contact id to get location information for.
|
||||
* If also a chat-id is given, this should be a member of the given chat.
|
||||
* 0 to get locations independently of the contact.
|
||||
* @param timestampFrom Start of timespan to return.
|
||||
* Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC.
|
||||
* 0 for "start from the beginning".
|
||||
* @param timestampTo End of timespan to return.
|
||||
* Must be given in number of seconds since 00:00 hours, Jan 1, 1970 UTC.
|
||||
* 0 for "all up to now".
|
||||
* @return Array of locations, NULL is never returned.
|
||||
* The array is sorted decending;
|
||||
* the first entry in the array is the location with the newest timestamp.
|
||||
*
|
||||
* Examples:
|
||||
* // get locations from the last hour for a global map
|
||||
* getLocations(0, 0, time(NULL)-60*60, 0);
|
||||
*
|
||||
* // get locations from a contact for a global map
|
||||
* getLocations(0, contact_id, 0, 0);
|
||||
*
|
||||
* // get all locations known for a given chat
|
||||
* getLocations(chat_id, 0, 0, 0);
|
||||
*
|
||||
* // get locations from a single contact for a given chat
|
||||
* getLocations(chat_id, contact_id, 0, 0);
|
||||
*/
|
||||
|
||||
getLocations(
|
||||
chatId: number,
|
||||
contactId: number,
|
||||
timestampFrom = 0,
|
||||
timestampTo = 0
|
||||
) {
|
||||
const locations = new Locations(
|
||||
binding.dcn_get_locations(
|
||||
this.dcn_context,
|
||||
Number(chatId),
|
||||
Number(contactId),
|
||||
timestampFrom,
|
||||
timestampTo
|
||||
)
|
||||
)
|
||||
return locations.toJson()
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param duration The duration (0 for no mute, -1 for forever mute, everything else is is the relative mute duration from now in seconds)
|
||||
*/
|
||||
setChatMuteDuration(chatId: number, duration: number) {
|
||||
return Boolean(
|
||||
binding.dcn_set_chat_mute_duration(this.dcn_context, chatId, duration)
|
||||
)
|
||||
}
|
||||
|
||||
/** get information about the provider */
|
||||
getProviderFromEmail(email: string) {
|
||||
debug('DeltaChat.getProviderFromEmail')
|
||||
const provider = binding.dcn_provider_new_from_email(
|
||||
this.dcn_context,
|
||||
email
|
||||
)
|
||||
if (!provider) {
|
||||
return undefined
|
||||
}
|
||||
return {
|
||||
before_login_hint: binding.dcn_provider_get_before_login_hint(provider),
|
||||
overview_page: binding.dcn_provider_get_overview_page(provider),
|
||||
status: binding.dcn_provider_get_status(provider),
|
||||
}
|
||||
}
|
||||
|
||||
sendWebxdcStatusUpdate<T>(
|
||||
msgId: number,
|
||||
json: WebxdcSendingStatusUpdate<T>,
|
||||
descr: string
|
||||
) {
|
||||
return Boolean(
|
||||
binding.dcn_send_webxdc_status_update(
|
||||
this.dcn_context,
|
||||
msgId,
|
||||
JSON.stringify(json),
|
||||
descr
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
getWebxdcStatusUpdates<T>(
|
||||
msgId: number,
|
||||
serial = 0
|
||||
): WebxdcReceivedStatusUpdate<T>[] {
|
||||
return JSON.parse(
|
||||
binding.dcn_get_webxdc_status_updates(this.dcn_context, msgId, serial)
|
||||
)
|
||||
}
|
||||
|
||||
/** the string contains the binary data, it is an "u8 string", maybe we will use a more efficient type in the future. */
|
||||
getWebxdcBlob(message: Message, filename: string): Buffer | null {
|
||||
return binding.dcn_msg_get_webxdc_blob(message.dc_msg, filename)
|
||||
}
|
||||
}
|
||||
|
||||
export type WebxdcInfo = {
|
||||
name: string
|
||||
icon: string
|
||||
summary: string
|
||||
/**
|
||||
* if set by the webxdc, name of the document in edit
|
||||
*/
|
||||
document?: string
|
||||
}
|
||||
|
||||
type WebxdcSendingStatusUpdate<T> = {
|
||||
/** the payload, deserialized json:
|
||||
* any javascript primitive, array or object. */
|
||||
payload: T
|
||||
/** optional, short, informational message that will be added to the chat,
|
||||
* eg. "Alice voted" or "Bob scored 123 in MyGame";
|
||||
* usually only one line of text is shown,
|
||||
* use this option sparingly to not spam the chat. */
|
||||
info?: string
|
||||
/** optional, short text, shown beside app icon;
|
||||
* it is recommended to use some aggregated value,
|
||||
* eg. "8 votes", "Highscore: 123" */
|
||||
summary?: string
|
||||
/**
|
||||
* optional, name of the document in edit,
|
||||
* must not be used eg. in games where the Webxdc does not create documents
|
||||
*/
|
||||
document?: string
|
||||
}
|
||||
|
||||
type WebxdcReceivedStatusUpdate<T> = {
|
||||
/** the payload, deserialized json */
|
||||
payload: T
|
||||
/** the serial number of this update. Serials are larger `0` and newer serials have higher numbers. */
|
||||
serial: number
|
||||
/** the maximum serial currently known.
|
||||
* If `max_serial` equals `serial` this update is the last update (until new network messages arrive). */
|
||||
max_serial: number
|
||||
/** optional, short, informational message. */
|
||||
info?: string
|
||||
/** optional, short text, shown beside app icon. If there are no updates, an empty JSON-array is returned. */
|
||||
summary?: string
|
||||
}
|
||||
205
node/lib/deltachat.ts
Normal file
205
node/lib/deltachat.ts
Normal file
@@ -0,0 +1,205 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import binding from './binding'
|
||||
import { EventId2EventName } from './constants'
|
||||
import { EventEmitter } from 'events'
|
||||
import { existsSync } from 'fs'
|
||||
import rawDebug from 'debug'
|
||||
import { tmpdir } from 'os'
|
||||
import { join } from 'path'
|
||||
import { Context } from './context'
|
||||
const debug = rawDebug('deltachat:node:index')
|
||||
|
||||
const noop = function () {}
|
||||
interface NativeAccount {}
|
||||
|
||||
/**
|
||||
* Wrapper around dcn_account_t*
|
||||
*/
|
||||
export class AccountManager extends EventEmitter {
|
||||
dcn_accounts: NativeAccount
|
||||
accountDir: string
|
||||
|
||||
constructor(cwd: string, os = 'deltachat-node') {
|
||||
debug('DeltaChat constructor')
|
||||
super()
|
||||
|
||||
this.accountDir = cwd
|
||||
this.dcn_accounts = binding.dcn_accounts_new(os, this.accountDir)
|
||||
}
|
||||
|
||||
getAllAccountIds() {
|
||||
return binding.dcn_accounts_get_all(this.dcn_accounts)
|
||||
}
|
||||
|
||||
selectAccount(account_id: number) {
|
||||
return binding.dcn_accounts_select_account(this.dcn_accounts, account_id)
|
||||
}
|
||||
|
||||
selectedAccount(): number {
|
||||
return binding.dcn_accounts_get_selected_account(this.dcn_accounts)
|
||||
}
|
||||
|
||||
addAccount(): number {
|
||||
return binding.dcn_accounts_add_account(this.dcn_accounts)
|
||||
}
|
||||
|
||||
addClosedAccount(): number {
|
||||
return binding.dcn_accounts_add_closed_account(this.dcn_accounts)
|
||||
}
|
||||
|
||||
removeAccount(account_id: number) {
|
||||
return binding.dcn_accounts_remove_account(this.dcn_accounts, account_id)
|
||||
}
|
||||
|
||||
accountContext(account_id: number) {
|
||||
const native_context = binding.dcn_accounts_get_account(
|
||||
this.dcn_accounts,
|
||||
account_id
|
||||
)
|
||||
if (native_context === null) {
|
||||
throw new Error(
|
||||
`could not get context with id ${account_id}, does it even exist? please check your ids`
|
||||
)
|
||||
}
|
||||
return new Context(this, native_context, account_id)
|
||||
}
|
||||
|
||||
migrateAccount(dbfile: string): number {
|
||||
return binding.dcn_accounts_migrate_account(this.dcn_accounts, dbfile)
|
||||
}
|
||||
|
||||
close() {
|
||||
this.stopIO()
|
||||
debug('unrefing context')
|
||||
binding.dcn_accounts_unref(this.dcn_accounts)
|
||||
debug('Unref end')
|
||||
}
|
||||
|
||||
emit(
|
||||
event: string | symbol,
|
||||
account_id: number,
|
||||
data1: any,
|
||||
data2: any
|
||||
): boolean {
|
||||
super.emit('ALL', event, account_id, data1, data2)
|
||||
return super.emit(event, account_id, data1, data2)
|
||||
}
|
||||
|
||||
handleCoreEvent(
|
||||
eventId: number,
|
||||
accountId: number,
|
||||
data1: number,
|
||||
data2: number | string
|
||||
) {
|
||||
const eventString = EventId2EventName[eventId]
|
||||
debug('event', eventString, accountId, data1, data2)
|
||||
debug(eventString, data1, data2)
|
||||
if (!this.emit) {
|
||||
console.log('Received an event but EventEmitter is already destroyed.')
|
||||
console.log(eventString, data1, data2)
|
||||
return
|
||||
}
|
||||
this.emit(eventString, accountId, data1, data2)
|
||||
}
|
||||
|
||||
startEvents() {
|
||||
if (this.dcn_accounts === null) {
|
||||
throw new Error('dcn_account is null')
|
||||
}
|
||||
binding.dcn_accounts_start_event_handler(
|
||||
this.dcn_accounts,
|
||||
this.handleCoreEvent.bind(this)
|
||||
)
|
||||
debug('Started event handler')
|
||||
}
|
||||
|
||||
startIO() {
|
||||
binding.dcn_accounts_start_io(this.dcn_accounts)
|
||||
}
|
||||
|
||||
stopIO() {
|
||||
binding.dcn_accounts_stop_io(this.dcn_accounts)
|
||||
}
|
||||
|
||||
static maybeValidAddr(addr: string) {
|
||||
debug('DeltaChat.maybeValidAddr')
|
||||
if (addr === null) return false
|
||||
return Boolean(binding.dcn_maybe_valid_addr(addr))
|
||||
}
|
||||
|
||||
static parseGetInfo(info: string) {
|
||||
debug('static _getInfo')
|
||||
const result: { [key: string]: string } = {}
|
||||
|
||||
const regex = /^(\w+)=(.*)$/i
|
||||
info
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.forEach((line) => {
|
||||
const match = regex.exec(line)
|
||||
if (match) {
|
||||
result[match[1]] = match[2]
|
||||
}
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
static newTemporary() {
|
||||
let directory = null
|
||||
while (true) {
|
||||
const randomString = Math.random().toString(36).substr(2, 5)
|
||||
directory = join(tmpdir(), 'deltachat-' + randomString)
|
||||
if (!existsSync(directory)) break
|
||||
}
|
||||
const dc = new AccountManager(directory)
|
||||
const accountId = dc.addAccount()
|
||||
const context = dc.accountContext(accountId)
|
||||
return { dc, context, accountId, directory }
|
||||
}
|
||||
|
||||
static getSystemInfo() {
|
||||
debug('DeltaChat.getSystemInfo')
|
||||
const { dc, context } = AccountManager.newTemporary()
|
||||
const info = AccountManager.parseGetInfo(
|
||||
binding.dcn_get_info(context.dcn_context)
|
||||
)
|
||||
const {
|
||||
deltachat_core_version,
|
||||
sqlite_version,
|
||||
sqlite_thread_safe,
|
||||
libetpan_version,
|
||||
openssl_version,
|
||||
compile_date,
|
||||
arch,
|
||||
} = info
|
||||
const result = {
|
||||
deltachat_core_version,
|
||||
sqlite_version,
|
||||
sqlite_thread_safe,
|
||||
libetpan_version,
|
||||
openssl_version,
|
||||
compile_date,
|
||||
arch,
|
||||
}
|
||||
context.unref()
|
||||
dc.close()
|
||||
return result
|
||||
}
|
||||
|
||||
/** get information about the provider
|
||||
*
|
||||
* This function creates a temporary context to be standalone,
|
||||
* if posible use `Context.getProviderFromEmail` instead. (otherwise potential proxy settings are not used)
|
||||
* @deprecated
|
||||
*/
|
||||
static getProviderFromEmail(email: string) {
|
||||
debug('DeltaChat.getProviderFromEmail')
|
||||
const { dc, context } = AccountManager.newTemporary()
|
||||
const provider = context.getProviderFromEmail(email)
|
||||
context.unref()
|
||||
dc.close()
|
||||
return provider
|
||||
}
|
||||
}
|
||||
20
node/lib/index.ts
Normal file
20
node/lib/index.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { AccountManager } from './deltachat'
|
||||
|
||||
export default AccountManager
|
||||
|
||||
export { Context } from './context'
|
||||
export { Chat } from './chat'
|
||||
export { ChatList } from './chatlist'
|
||||
export { C } from './constants'
|
||||
export { Contact } from './contact'
|
||||
export { AccountManager as DeltaChat }
|
||||
export { Locations } from './locations'
|
||||
export { Lot } from './lot'
|
||||
export {
|
||||
Message,
|
||||
MessageState,
|
||||
MessageViewType,
|
||||
MessageDownloadState,
|
||||
} from './message'
|
||||
|
||||
export * from './types'
|
||||
82
node/lib/locations.ts
Normal file
82
node/lib/locations.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
const binding = require('../binding')
|
||||
const debug = require('debug')('deltachat:node:locations')
|
||||
|
||||
interface NativeLocations {}
|
||||
/**
|
||||
* Wrapper around dc_location_t*
|
||||
*/
|
||||
export class Locations {
|
||||
constructor(public dc_locations: NativeLocations) {
|
||||
debug('Locations constructor')
|
||||
if (dc_locations === null) {
|
||||
throw new Error('dc_locations can not be null')
|
||||
}
|
||||
}
|
||||
|
||||
locationToJson(index: number) {
|
||||
debug('locationToJson')
|
||||
return {
|
||||
accuracy: this.getAccuracy(index),
|
||||
latitude: this.getLatitude(index),
|
||||
longitude: this.getLongitude(index),
|
||||
timestamp: this.getTimestamp(index),
|
||||
contactId: this.getContactId(index),
|
||||
msgId: this.getMsgId(index),
|
||||
chatId: this.getChatId(index),
|
||||
isIndependent: this.isIndependent(index),
|
||||
marker: this.getMarker(index),
|
||||
}
|
||||
}
|
||||
|
||||
toJson(): ReturnType<Locations['locationToJson']>[] {
|
||||
debug('toJson')
|
||||
const locations = []
|
||||
const count = this.getCount()
|
||||
for (let index = 0; index < count; index++) {
|
||||
locations.push(this.locationToJson(index))
|
||||
}
|
||||
return locations
|
||||
}
|
||||
|
||||
getCount(): number {
|
||||
return binding.dcn_array_get_cnt(this.dc_locations)
|
||||
}
|
||||
|
||||
getAccuracy(index: number): number {
|
||||
return binding.dcn_array_get_accuracy(this.dc_locations, index)
|
||||
}
|
||||
|
||||
getLatitude(index: number): number {
|
||||
return binding.dcn_array_get_latitude(this.dc_locations, index)
|
||||
}
|
||||
|
||||
getLongitude(index: number): number {
|
||||
return binding.dcn_array_get_longitude(this.dc_locations, index)
|
||||
}
|
||||
|
||||
getTimestamp(index: number): number {
|
||||
return binding.dcn_array_get_timestamp(this.dc_locations, index)
|
||||
}
|
||||
|
||||
getMsgId(index: number): number {
|
||||
return binding.dcn_array_get_msg_id(this.dc_locations, index)
|
||||
}
|
||||
|
||||
getContactId(index: number): number {
|
||||
return binding.dcn_array_get_contact_id(this.dc_locations, index)
|
||||
}
|
||||
|
||||
getChatId(index: number): number {
|
||||
return binding.dcn_array_get_chat_id(this.dc_locations, index)
|
||||
}
|
||||
|
||||
isIndependent(index: number): boolean {
|
||||
return binding.dcn_array_is_independent(this.dc_locations, index)
|
||||
}
|
||||
|
||||
getMarker(index: number): string {
|
||||
return binding.dcn_array_get_marker(this.dc_locations, index)
|
||||
}
|
||||
}
|
||||
52
node/lib/lot.ts
Normal file
52
node/lib/lot.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
const binding = require('../binding')
|
||||
const debug = require('debug')('deltachat:node:lot')
|
||||
|
||||
interface NativeLot {}
|
||||
/**
|
||||
* Wrapper around dc_lot_t*
|
||||
*/
|
||||
export class Lot {
|
||||
constructor(public dc_lot: NativeLot) {
|
||||
debug('Lot constructor')
|
||||
if (dc_lot === null) {
|
||||
throw new Error('dc_lot can not be null')
|
||||
}
|
||||
}
|
||||
|
||||
toJson() {
|
||||
debug('toJson')
|
||||
return {
|
||||
state: this.getState(),
|
||||
text1: this.getText1(),
|
||||
text1Meaning: this.getText1Meaning(),
|
||||
text2: this.getText2(),
|
||||
timestamp: this.getTimestamp(),
|
||||
}
|
||||
}
|
||||
|
||||
getId(): number {
|
||||
return binding.dcn_lot_get_id(this.dc_lot)
|
||||
}
|
||||
|
||||
getState(): number {
|
||||
return binding.dcn_lot_get_state(this.dc_lot)
|
||||
}
|
||||
|
||||
getText1(): string {
|
||||
return binding.dcn_lot_get_text1(this.dc_lot)
|
||||
}
|
||||
|
||||
getText1Meaning(): string {
|
||||
return binding.dcn_lot_get_text1_meaning(this.dc_lot)
|
||||
}
|
||||
|
||||
getText2(): string {
|
||||
return binding.dcn_lot_get_text2(this.dc_lot)
|
||||
}
|
||||
|
||||
getTimestamp(): number {
|
||||
return binding.dcn_lot_get_timestamp(this.dc_lot)
|
||||
}
|
||||
}
|
||||
370
node/lib/message.ts
Normal file
370
node/lib/message.ts
Normal file
@@ -0,0 +1,370 @@
|
||||
/* eslint-disable camelcase */
|
||||
|
||||
import binding from './binding'
|
||||
import { C } from './constants'
|
||||
import { Lot } from './lot'
|
||||
import { Chat } from './chat'
|
||||
import { WebxdcInfo } from './context'
|
||||
const debug = require('debug')('deltachat:node:message')
|
||||
|
||||
export enum MessageDownloadState {
|
||||
Available = C.DC_DOWNLOAD_AVAILABLE,
|
||||
Done = C.DC_DOWNLOAD_DONE,
|
||||
Failure = C.DC_DOWNLOAD_FAILURE,
|
||||
InProgress = C.DC_DOWNLOAD_IN_PROGRESS,
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for message states so you can do e.g.
|
||||
*
|
||||
* if (msg.getState().isPending()) { .. }
|
||||
*
|
||||
*/
|
||||
export class MessageState {
|
||||
constructor(public state: number) {
|
||||
debug(`MessageState constructor ${state}`)
|
||||
}
|
||||
|
||||
isUndefined() {
|
||||
return this.state === C.DC_STATE_UNDEFINED
|
||||
}
|
||||
|
||||
isFresh() {
|
||||
return this.state === C.DC_STATE_IN_FRESH
|
||||
}
|
||||
|
||||
isNoticed() {
|
||||
return this.state === C.DC_STATE_IN_NOTICED
|
||||
}
|
||||
|
||||
isSeen() {
|
||||
return this.state === C.DC_STATE_IN_SEEN
|
||||
}
|
||||
|
||||
isPending() {
|
||||
return this.state === C.DC_STATE_OUT_PENDING
|
||||
}
|
||||
|
||||
isFailed() {
|
||||
return this.state === C.DC_STATE_OUT_FAILED
|
||||
}
|
||||
|
||||
isDelivered() {
|
||||
return this.state === C.DC_STATE_OUT_DELIVERED
|
||||
}
|
||||
|
||||
isReceived() {
|
||||
return this.state === C.DC_STATE_OUT_MDN_RCVD
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class for message types so you can do e.g.
|
||||
*
|
||||
* if (msg.getViewType().isVideo()) { .. }
|
||||
*
|
||||
*/
|
||||
export class MessageViewType {
|
||||
constructor(public viewType: number) {
|
||||
debug(`MessageViewType constructor ${viewType}`)
|
||||
}
|
||||
|
||||
isText() {
|
||||
return this.viewType === C.DC_MSG_TEXT
|
||||
}
|
||||
|
||||
isImage() {
|
||||
return this.viewType === C.DC_MSG_IMAGE || this.viewType === C.DC_MSG_GIF
|
||||
}
|
||||
|
||||
isGif() {
|
||||
return this.viewType === C.DC_MSG_GIF
|
||||
}
|
||||
|
||||
isAudio() {
|
||||
return this.viewType === C.DC_MSG_AUDIO || this.viewType === C.DC_MSG_VOICE
|
||||
}
|
||||
|
||||
isVoice() {
|
||||
return this.viewType === C.DC_MSG_VOICE
|
||||
}
|
||||
|
||||
isVideo() {
|
||||
return this.viewType === C.DC_MSG_VIDEO
|
||||
}
|
||||
|
||||
isFile() {
|
||||
return this.viewType === C.DC_MSG_FILE
|
||||
}
|
||||
|
||||
isVideochatInvitation() {
|
||||
return this.viewType === C.DC_MSG_VIDEOCHAT_INVITATION
|
||||
}
|
||||
}
|
||||
|
||||
interface NativeMessage {}
|
||||
/**
|
||||
* Wrapper around dc_msg_t*
|
||||
*/
|
||||
export class Message {
|
||||
constructor(public dc_msg: NativeMessage) {
|
||||
debug('Message constructor')
|
||||
if (dc_msg === null) {
|
||||
throw new Error('dc_msg can not be null')
|
||||
}
|
||||
}
|
||||
|
||||
toJson() {
|
||||
debug('toJson')
|
||||
const quotedMessage = this.getQuotedMessage()
|
||||
const viewType = binding.dcn_msg_get_viewtype(this.dc_msg)
|
||||
return {
|
||||
chatId: this.getChatId(),
|
||||
webxdcInfo: viewType == C.DC_MSG_WEBXDC ? this.webxdcInfo : null,
|
||||
downloadState: this.downloadState,
|
||||
duration: this.getDuration(),
|
||||
file: this.getFile(),
|
||||
fromId: this.getFromId(),
|
||||
id: this.getId(),
|
||||
quotedText: this.getQuotedText(),
|
||||
quotedMessageId: quotedMessage ? quotedMessage.getId() : null,
|
||||
receivedTimestamp: this.getReceivedTimestamp(),
|
||||
sortTimestamp: this.getSortTimestamp(),
|
||||
text: this.getText(),
|
||||
timestamp: this.getTimestamp(),
|
||||
hasLocation: this.hasLocation(),
|
||||
hasHTML: this.hasHTML,
|
||||
viewType,
|
||||
state: binding.dcn_msg_get_state(this.dc_msg),
|
||||
hasDeviatingTimestamp: this.hasDeviatingTimestamp(),
|
||||
showPadlock: this.getShowpadlock(),
|
||||
summary: this.getSummary().toJson(),
|
||||
subject: this.subject,
|
||||
isSetupmessage: this.isSetupmessage(),
|
||||
isInfo: this.isInfo(),
|
||||
isForwarded: this.isForwarded(),
|
||||
dimensions: {
|
||||
height: this.getHeight(),
|
||||
width: this.getWidth(),
|
||||
},
|
||||
videochatType: this.getVideochatType(),
|
||||
videochatUrl: this.getVideochatUrl(),
|
||||
overrideSenderName: this.overrideSenderName,
|
||||
parentId: this.parent?.getId(),
|
||||
}
|
||||
}
|
||||
|
||||
getChatId(): number {
|
||||
return binding.dcn_msg_get_chat_id(this.dc_msg)
|
||||
}
|
||||
|
||||
get webxdcInfo(): WebxdcInfo | null {
|
||||
let info = binding.dcn_msg_get_webxdc_info(this.dc_msg)
|
||||
return info
|
||||
? JSON.parse(binding.dcn_msg_get_webxdc_info(this.dc_msg))
|
||||
: null
|
||||
}
|
||||
|
||||
get downloadState(): MessageDownloadState {
|
||||
return binding.dcn_msg_get_download_state(this.dc_msg)
|
||||
}
|
||||
|
||||
get parent(): Message | null {
|
||||
let msg = binding.dcn_msg_get_parent(this.dc_msg)
|
||||
return msg ? new Message(msg) : null
|
||||
}
|
||||
|
||||
getDuration(): number {
|
||||
return binding.dcn_msg_get_duration(this.dc_msg)
|
||||
}
|
||||
|
||||
getFile(): string {
|
||||
return binding.dcn_msg_get_file(this.dc_msg)
|
||||
}
|
||||
|
||||
getFilebytes(): number {
|
||||
return binding.dcn_msg_get_filebytes(this.dc_msg)
|
||||
}
|
||||
|
||||
getFilemime(): string {
|
||||
return binding.dcn_msg_get_filemime(this.dc_msg)
|
||||
}
|
||||
|
||||
getFilename(): string {
|
||||
return binding.dcn_msg_get_filename(this.dc_msg)
|
||||
}
|
||||
|
||||
getFromId(): number {
|
||||
return binding.dcn_msg_get_from_id(this.dc_msg)
|
||||
}
|
||||
|
||||
getHeight(): number {
|
||||
return binding.dcn_msg_get_height(this.dc_msg)
|
||||
}
|
||||
|
||||
getId(): number {
|
||||
return binding.dcn_msg_get_id(this.dc_msg)
|
||||
}
|
||||
|
||||
getQuotedText(): string {
|
||||
return binding.dcn_msg_get_quoted_text(this.dc_msg)
|
||||
}
|
||||
|
||||
getQuotedMessage(): Message | null {
|
||||
const dc_msg = binding.dcn_msg_get_quoted_msg(this.dc_msg)
|
||||
return dc_msg ? new Message(dc_msg) : null
|
||||
}
|
||||
|
||||
getReceivedTimestamp(): number {
|
||||
return binding.dcn_msg_get_received_timestamp(this.dc_msg)
|
||||
}
|
||||
|
||||
getSetupcodebegin() {
|
||||
return binding.dcn_msg_get_setupcodebegin(this.dc_msg)
|
||||
}
|
||||
|
||||
getShowpadlock() {
|
||||
return Boolean(binding.dcn_msg_get_showpadlock(this.dc_msg))
|
||||
}
|
||||
|
||||
getSortTimestamp(): number {
|
||||
return binding.dcn_msg_get_sort_timestamp(this.dc_msg)
|
||||
}
|
||||
|
||||
getState() {
|
||||
return new MessageState(binding.dcn_msg_get_state(this.dc_msg))
|
||||
}
|
||||
|
||||
getSummary(chat?: Chat) {
|
||||
const dc_chat = (chat && chat.dc_chat) || null
|
||||
return new Lot(binding.dcn_msg_get_summary(this.dc_msg, dc_chat))
|
||||
}
|
||||
|
||||
get subject(): string {
|
||||
return binding.dcn_msg_get_subject(this.dc_msg)
|
||||
}
|
||||
|
||||
getSummarytext(approxCharacters: number): string {
|
||||
approxCharacters = approxCharacters || 0
|
||||
return binding.dcn_msg_get_summarytext(this.dc_msg, approxCharacters)
|
||||
}
|
||||
|
||||
getText(): string {
|
||||
return binding.dcn_msg_get_text(this.dc_msg)
|
||||
}
|
||||
|
||||
getTimestamp(): number {
|
||||
return binding.dcn_msg_get_timestamp(this.dc_msg)
|
||||
}
|
||||
|
||||
getViewType() {
|
||||
return new MessageViewType(binding.dcn_msg_get_viewtype(this.dc_msg))
|
||||
}
|
||||
|
||||
getVideochatType(): number {
|
||||
return binding.dcn_msg_get_videochat_type(this.dc_msg)
|
||||
}
|
||||
|
||||
getVideochatUrl(): string {
|
||||
return binding.dcn_msg_get_videochat_url(this.dc_msg)
|
||||
}
|
||||
|
||||
getWidth(): number {
|
||||
return binding.dcn_msg_get_width(this.dc_msg)
|
||||
}
|
||||
|
||||
get overrideSenderName(): string {
|
||||
return binding.dcn_msg_get_override_sender_name(this.dc_msg)
|
||||
}
|
||||
|
||||
hasDeviatingTimestamp() {
|
||||
return binding.dcn_msg_has_deviating_timestamp(this.dc_msg)
|
||||
}
|
||||
|
||||
hasLocation() {
|
||||
return Boolean(binding.dcn_msg_has_location(this.dc_msg))
|
||||
}
|
||||
|
||||
get hasHTML() {
|
||||
return Boolean(binding.dcn_msg_has_html(this.dc_msg))
|
||||
}
|
||||
|
||||
isDeadDrop() {
|
||||
// TODO: Fix
|
||||
//return this.getChatId() === C.DC_CHAT_ID_DEADDROP
|
||||
return false
|
||||
}
|
||||
|
||||
isForwarded() {
|
||||
return Boolean(binding.dcn_msg_is_forwarded(this.dc_msg))
|
||||
}
|
||||
|
||||
isIncreation() {
|
||||
return Boolean(binding.dcn_msg_is_increation(this.dc_msg))
|
||||
}
|
||||
|
||||
isInfo() {
|
||||
return Boolean(binding.dcn_msg_is_info(this.dc_msg))
|
||||
}
|
||||
|
||||
isSent() {
|
||||
return Boolean(binding.dcn_msg_is_sent(this.dc_msg))
|
||||
}
|
||||
|
||||
isSetupmessage() {
|
||||
return Boolean(binding.dcn_msg_is_setupmessage(this.dc_msg))
|
||||
}
|
||||
|
||||
latefilingMediasize(width: number, height: number, duration: number) {
|
||||
binding.dcn_msg_latefiling_mediasize(this.dc_msg, width, height, duration)
|
||||
}
|
||||
|
||||
setDimension(width: number, height: number) {
|
||||
binding.dcn_msg_set_dimension(this.dc_msg, width, height)
|
||||
return this
|
||||
}
|
||||
|
||||
setDuration(duration: number) {
|
||||
binding.dcn_msg_set_duration(this.dc_msg, duration)
|
||||
return this
|
||||
}
|
||||
|
||||
setFile(file: string, mime?: string) {
|
||||
if (typeof file !== 'string') throw new Error('Missing filename')
|
||||
binding.dcn_msg_set_file(this.dc_msg, file, mime || '')
|
||||
return this
|
||||
}
|
||||
|
||||
setLocation(longitude: number, latitude: number) {
|
||||
binding.dcn_msg_set_location(this.dc_msg, longitude, latitude)
|
||||
return this
|
||||
}
|
||||
|
||||
setQuote(quotedMessage: Message | null) {
|
||||
binding.dcn_msg_set_quote(this.dc_msg, quotedMessage?.dc_msg)
|
||||
return this
|
||||
}
|
||||
|
||||
setText(text: string) {
|
||||
binding.dcn_msg_set_text(this.dc_msg, text)
|
||||
return this
|
||||
}
|
||||
|
||||
setHTML(html: string) {
|
||||
binding.dcn_msg_set_html(this.dc_msg, html)
|
||||
return this
|
||||
}
|
||||
|
||||
setOverrideSenderName(senderName: string) {
|
||||
binding.dcn_msg_set_override_sender_name(this.dc_msg, senderName)
|
||||
return this
|
||||
}
|
||||
|
||||
/** Force the message to be sent in plain text.
|
||||
*
|
||||
* This API is for bots, there is no need to expose it in the UI.
|
||||
*/
|
||||
forcePlaintext() {
|
||||
binding.dcn_msg_force_plaintext(this.dc_msg)
|
||||
}
|
||||
}
|
||||
24
node/lib/types.ts
Normal file
24
node/lib/types.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { C } from './constants'
|
||||
|
||||
export type ChatTypes =
|
||||
| C.DC_CHAT_TYPE_GROUP
|
||||
| C.DC_CHAT_TYPE_MAILINGLIST
|
||||
| C.DC_CHAT_TYPE_SINGLE
|
||||
| C.DC_CHAT_TYPE_UNDEFINED
|
||||
|
||||
export interface ChatJSON {
|
||||
archived: boolean
|
||||
pinned: boolean
|
||||
color: string
|
||||
id: number
|
||||
name: string
|
||||
profileImage: string
|
||||
type: number
|
||||
isSelfTalk: boolean
|
||||
isUnpromoted: boolean
|
||||
isProtected: boolean
|
||||
canSend: boolean
|
||||
isDeviceTalk: boolean
|
||||
isContactRequest: boolean
|
||||
muted: boolean
|
||||
}
|
||||
6
node/lib/util.ts
Normal file
6
node/lib/util.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
/**
|
||||
* @param integerColor expects a 24bit rgb integer (left to right: 8bits red, 8bits green, 8bits blue)
|
||||
*/
|
||||
export function integerToHexColor(integerColor: number) {
|
||||
return '#' + (integerColor + 16777216).toString(16).substring(1)
|
||||
}
|
||||
13
node/patches/m1_build_use_x86_64.patch
Normal file
13
node/patches/m1_build_use_x86_64.patch
Normal file
@@ -0,0 +1,13 @@
|
||||
diff --git i/node/binding.gyp w/node/binding.gyp
|
||||
index b0d92eae..c5e504fa 100644
|
||||
--- i/node/binding.gyp
|
||||
+++ w/node/binding.gyp
|
||||
@@ -43,7 +43,7 @@
|
||||
"include_dirs": ["../deltachat-ffi"],
|
||||
"ldflags": ["-Wl,-Bsymbolic"], # Prevent sqlite3 from electron from overriding sqlcipher
|
||||
"libraries": [
|
||||
- "../../target/release/libdeltachat.a",
|
||||
+ "../../target/x86_64-apple-darwin/release/libdeltachat.a",
|
||||
"-ldl",
|
||||
],
|
||||
"conditions": [],
|
||||
26
node/scripts/common.js
Normal file
26
node/scripts/common.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const spawnSync = require('child_process').spawnSync
|
||||
|
||||
const verbose = isVerbose()
|
||||
|
||||
function spawn (cmd, args, opts) {
|
||||
log(`>> spawn: ${cmd} ${args.join(' ')}`)
|
||||
const result = spawnSync(cmd, args, opts)
|
||||
if (result.status === null) {
|
||||
console.error(`Could not find ${cmd}`)
|
||||
process.exit(1)
|
||||
} else if (result.status !== 0) {
|
||||
console.error(`${cmd} failed with code ${result.status}`)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
function log (...args) {
|
||||
if (verbose) console.log(...args)
|
||||
}
|
||||
|
||||
function isVerbose () {
|
||||
const loglevel = process.env.npm_config_loglevel
|
||||
return loglevel === 'verbose' || process.env.CI === 'true'
|
||||
}
|
||||
|
||||
module.exports = { spawn, log, isVerbose, verbose }
|
||||
73
node/scripts/generate-constants.js
Executable file
73
node/scripts/generate-constants.js
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
const split = require('split2')
|
||||
|
||||
const data = []
|
||||
const regex = /^#define\s+(\w+)\s+(\w+)/i
|
||||
const header = path.resolve(
|
||||
__dirname,
|
||||
'../../deltachat-ffi/deltachat.h'
|
||||
)
|
||||
|
||||
console.log('Generating constants...')
|
||||
|
||||
fs.createReadStream(header)
|
||||
.pipe(split())
|
||||
.on('data', (line) => {
|
||||
const match = regex.exec(line)
|
||||
if (match) {
|
||||
const key = match[1]
|
||||
const value = parseInt(match[2])
|
||||
if (isNaN(value)) return
|
||||
|
||||
data.push({ key, value })
|
||||
}
|
||||
})
|
||||
.on('end', () => {
|
||||
const constants = data
|
||||
.filter(
|
||||
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
|
||||
)
|
||||
.sort((lhs, rhs) => {
|
||||
if (lhs.key < rhs.key) return -1
|
||||
else if (lhs.key > rhs.key) return 1
|
||||
return 0
|
||||
})
|
||||
.map((row) => {
|
||||
return ` ${row.key}: ${row.value}`
|
||||
})
|
||||
.join(',\n')
|
||||
|
||||
const events = data
|
||||
.sort((lhs, rhs) => {
|
||||
if (lhs.value < rhs.value) return -1
|
||||
else if (lhs.value > rhs.value) return 1
|
||||
return 0
|
||||
})
|
||||
.filter((i) => {
|
||||
return i.key.startsWith('DC_EVENT_')
|
||||
})
|
||||
.map((i) => {
|
||||
return ` ${i.value}: '${i.key}'`
|
||||
})
|
||||
.join(',\n')
|
||||
|
||||
// backwards compat
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../constants.js'),
|
||||
`// Generated!\n\nmodule.exports = {\n${constants}\n}\n`
|
||||
)
|
||||
// backwards compat
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../events.js'),
|
||||
`/* eslint-disable quotes */\n// Generated!\n\nmodule.exports = {\n${events}\n}\n`
|
||||
)
|
||||
|
||||
fs.writeFileSync(
|
||||
path.resolve(__dirname, '../lib/constants.ts'),
|
||||
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, ' =')},\n}\n
|
||||
// Generated!\n\nexport const EventId2EventName: { [key: number]: string } = {\n${events},\n}\n`
|
||||
)
|
||||
})
|
||||
22
node/scripts/install.js
Normal file
22
node/scripts/install.js
Normal file
@@ -0,0 +1,22 @@
|
||||
const {execSync} = require('child_process')
|
||||
const {existsSync} = require('fs')
|
||||
const {join} = require('path')
|
||||
|
||||
const run = (cmd) => {
|
||||
console.log('[i] running `' + cmd + '`')
|
||||
execSync(cmd, {stdio: 'inherit'})
|
||||
}
|
||||
|
||||
// Build bindings
|
||||
if (process.env.USE_SYSTEM_LIBDELTACHAT === 'true') {
|
||||
console.log('[i] USE_SYSTEM_LIBDELTACHAT is true, rebuilding c bindings and using pkg-config to retrieve lib paths and cflags of libdeltachat')
|
||||
run('npm run build:bindings:c:c')
|
||||
} else {
|
||||
console.log('[i] Building rust core & c bindings, if possible use prebuilds')
|
||||
run('npm run install:prebuilds')
|
||||
}
|
||||
|
||||
if (!existsSync(join(__dirname, '..', 'dist'))) {
|
||||
console.log('[i] Didn\'t find already built typescript bindings. Trying to transpile them. If this fail, make sure typescript is installed ;)')
|
||||
run('npm run build:bindings:ts')
|
||||
}
|
||||
46
node/scripts/postLinksToDetails.js
Normal file
46
node/scripts/postLinksToDetails.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const { readFileSync } = require('fs')
|
||||
|
||||
const sha = JSON.parse(
|
||||
readFileSync(process.env['GITHUB_EVENT_PATH'], 'utf8')
|
||||
).pull_request.head.sha
|
||||
|
||||
const base_url =
|
||||
'https://download.delta.chat/node/'
|
||||
|
||||
const GITHUB_API_URL =
|
||||
'https://api.github.com/repos/deltachat/deltachat-core-rust/statuses/' + sha
|
||||
|
||||
const file_url = process.env['URL']
|
||||
const GITHUB_TOKEN = process.env['GITHUB_TOKEN']
|
||||
|
||||
const STATUS_DATA = {
|
||||
state: 'success',
|
||||
description: '⏩ Click on "Details" to download →',
|
||||
context: 'Download the node-bindings.tar.gz',
|
||||
target_url: base_url + file_url + '.tar.gz',
|
||||
}
|
||||
|
||||
const http = require('https')
|
||||
|
||||
const options = {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'User-Agent': 'github-action ci for deltachat deskop',
|
||||
authorization: 'Bearer ' + GITHUB_TOKEN,
|
||||
},
|
||||
}
|
||||
|
||||
const req = http.request(GITHUB_API_URL, options, function(res) {
|
||||
var chunks = []
|
||||
res.on('data', function(chunk) {
|
||||
chunks.push(chunk)
|
||||
})
|
||||
res.on('end', function() {
|
||||
var body = Buffer.concat(chunks)
|
||||
console.log(body.toString())
|
||||
})
|
||||
})
|
||||
|
||||
req.write(JSON.stringify(STATUS_DATA))
|
||||
req.end()
|
||||
57
node/scripts/postinstall.js
Normal file
57
node/scripts/postinstall.js
Normal file
@@ -0,0 +1,57 @@
|
||||
const fs = require('fs')
|
||||
const path = require('path')
|
||||
|
||||
if (process.platform !== 'win32') {
|
||||
console.log('postinstall: not windows, so skipping!')
|
||||
process.exit(0)
|
||||
}
|
||||
|
||||
const from = path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'..',
|
||||
'target',
|
||||
'release',
|
||||
'deltachat.dll'
|
||||
)
|
||||
|
||||
const getDestination = () => {
|
||||
const argv = process.argv
|
||||
if (argv.length === 3 && argv[2] === '--prebuild') {
|
||||
return path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'prebuilds',
|
||||
'win32-x64',
|
||||
'deltachat.dll'
|
||||
)
|
||||
} else {
|
||||
return path.resolve(
|
||||
__dirname,
|
||||
'..',
|
||||
'build',
|
||||
'Release',
|
||||
'deltachat.dll'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
const dest = getDestination()
|
||||
|
||||
copy(from, dest, (err) => {
|
||||
if (err) throw err
|
||||
console.log(`postinstall: copied ${from} to ${dest}`)
|
||||
})
|
||||
|
||||
function copy (from, to, cb) {
|
||||
fs.stat(from, (err, st) => {
|
||||
if (err) return cb(err)
|
||||
fs.readFile(from, (err, buf) => {
|
||||
if (err) return cb(err)
|
||||
fs.writeFile(to, buf, (err) => {
|
||||
if (err) return cb(err)
|
||||
fs.chmod(to, st.mode, cb)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
17
node/scripts/rebuild-core.js
Normal file
17
node/scripts/rebuild-core.js
Normal file
@@ -0,0 +1,17 @@
|
||||
const path = require('path')
|
||||
const { spawn } = require('./common')
|
||||
const opts = {
|
||||
cwd: path.resolve(__dirname, '../..'),
|
||||
stdio: 'inherit'
|
||||
}
|
||||
|
||||
const buildArgs = [
|
||||
'build',
|
||||
'--release',
|
||||
'--features',
|
||||
'vendored',
|
||||
'-p',
|
||||
'deltachat_ffi'
|
||||
]
|
||||
|
||||
spawn('cargo', buildArgs, opts)
|
||||
3505
node/src/module.c
Normal file
3505
node/src/module.c
Normal file
File diff suppressed because it is too large
Load Diff
144
node/src/napi-macros-extensions.h
Normal file
144
node/src/napi-macros-extensions.h
Normal file
@@ -0,0 +1,144 @@
|
||||
#include <napi-macros.h>
|
||||
|
||||
#undef NAPI_STATUS_THROWS
|
||||
|
||||
#define NAPI_STATUS_THROWS(call) \
|
||||
if ((call) != napi_ok) { \
|
||||
napi_throw_error(env, NULL, #call " failed!"); \
|
||||
}
|
||||
|
||||
#define NAPI_DCN_CONTEXT() \
|
||||
dcn_context_t* dcn_context; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dcn_context)); \
|
||||
if (!dcn_context) { \
|
||||
const char* msg = "Provided dnc_context is null"; \
|
||||
NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
|
||||
} \
|
||||
if (!dcn_context->dc_context) { \
|
||||
const char* msg = "Provided dc_context is null, did you close the context or not open it?"; \
|
||||
NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
|
||||
}
|
||||
|
||||
#define NAPI_DCN_ACCOUNTS() \
|
||||
dcn_accounts_t* dcn_accounts; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dcn_accounts)); \
|
||||
if (!dcn_accounts) { \
|
||||
const char* msg = "Provided dnc_acounts is null"; \
|
||||
NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
|
||||
} \
|
||||
if (!dcn_accounts->dc_accounts) { \
|
||||
const char* msg = "Provided dc_accounts is null, did you unref the accounts object?"; \
|
||||
NAPI_STATUS_THROWS(napi_throw_type_error(env, NULL, msg)); \
|
||||
}
|
||||
|
||||
|
||||
#define NAPI_DC_CHAT() \
|
||||
dc_chat_t* dc_chat; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_chat));
|
||||
|
||||
#define NAPI_DC_CHATLIST() \
|
||||
dc_chatlist_t* dc_chatlist; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_chatlist));
|
||||
|
||||
#define NAPI_DC_CONTACT() \
|
||||
dc_contact_t* dc_contact; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_contact));
|
||||
|
||||
#define NAPI_DC_LOT() \
|
||||
dc_lot_t* dc_lot; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_lot));
|
||||
|
||||
#define NAPI_DC_MSG() \
|
||||
dc_msg_t* dc_msg; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_msg));
|
||||
|
||||
#define NAPI_ARGV_DC_MSG(name, position) \
|
||||
dc_msg_t* name; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[position], (void**)&name));
|
||||
|
||||
#define NAPI_DC_PROVIDER() \
|
||||
dc_provider_t* dc_provider; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_provider));
|
||||
|
||||
#define NAPI_DC_ARRAY() \
|
||||
dc_array_t* dc_array; \
|
||||
NAPI_STATUS_THROWS(napi_get_value_external(env, argv[0], (void**)&dc_array));
|
||||
|
||||
#define NAPI_RETURN_UNDEFINED() \
|
||||
return 0;
|
||||
|
||||
#define NAPI_RETURN_UINT64(name) \
|
||||
napi_value return_int64; \
|
||||
NAPI_STATUS_THROWS(napi_create_bigint_int64(env, name, &return_int64)); \
|
||||
return return_int64;
|
||||
|
||||
#define NAPI_RETURN_INT64(name) \
|
||||
napi_value return_int64; \
|
||||
NAPI_STATUS_THROWS(napi_create_int64(env, name, &return_int64)); \
|
||||
return return_int64;
|
||||
|
||||
|
||||
#define NAPI_RETURN_AND_UNREF_STRING(name) \
|
||||
napi_value return_value; \
|
||||
if (name == NULL) { \
|
||||
NAPI_STATUS_THROWS(napi_get_null(env, &return_value)); \
|
||||
return return_value; \
|
||||
} \
|
||||
NAPI_STATUS_THROWS(napi_create_string_utf8(env, name, NAPI_AUTO_LENGTH, &return_value)); \
|
||||
dc_str_unref(name); \
|
||||
return return_value;
|
||||
|
||||
#define NAPI_ASYNC_CARRIER_BEGIN(name) \
|
||||
typedef struct name##_carrier_t { \
|
||||
napi_ref callback_ref; \
|
||||
napi_async_work async_work; \
|
||||
dcn_context_t* dcn_context;
|
||||
|
||||
#define NAPI_ASYNC_CARRIER_END(name) \
|
||||
} name##_carrier_t;
|
||||
|
||||
#define NAPI_ASYNC_EXECUTE(name) \
|
||||
static void name##_execute(napi_env env, void* data)
|
||||
|
||||
#define NAPI_ASYNC_GET_CARRIER(name) \
|
||||
name##_carrier_t* carrier = (name##_carrier_t*)data;
|
||||
|
||||
#define NAPI_ASYNC_COMPLETE(name) \
|
||||
static void name##_complete(napi_env env, napi_status status, void* data)
|
||||
|
||||
#define NAPI_ASYNC_CALL_AND_DELETE_CB() \
|
||||
napi_value global; \
|
||||
NAPI_STATUS_THROWS(napi_get_global(env, &global)); \
|
||||
napi_value callback; \
|
||||
NAPI_STATUS_THROWS(napi_get_reference_value(env, carrier->callback_ref, &callback)); \
|
||||
NAPI_STATUS_THROWS(napi_call_function(env, global, callback, argc, argv, NULL)); \
|
||||
NAPI_STATUS_THROWS(napi_delete_reference(env, carrier->callback_ref)); \
|
||||
NAPI_STATUS_THROWS(napi_delete_async_work(env, carrier->async_work));
|
||||
|
||||
#define NAPI_ASYNC_NEW_CARRIER(name) \
|
||||
name##_carrier_t* carrier = calloc(1, sizeof(name##_carrier_t)); \
|
||||
carrier->dcn_context = dcn_context;
|
||||
|
||||
#define NAPI_ASYNC_QUEUE_WORK(name, cb) \
|
||||
napi_value callback = cb; \
|
||||
napi_value async_resource_name; \
|
||||
NAPI_STATUS_THROWS(napi_create_reference(env, callback, 1, &carrier->callback_ref)); \
|
||||
NAPI_STATUS_THROWS(napi_create_string_utf8(env, #name "_callback", \
|
||||
NAPI_AUTO_LENGTH, \
|
||||
&async_resource_name)); \
|
||||
NAPI_STATUS_THROWS(napi_create_async_work(env, callback, async_resource_name, \
|
||||
name##_execute, name##_complete, \
|
||||
carrier, &carrier->async_work)); \
|
||||
NAPI_STATUS_THROWS(napi_queue_async_work(env, carrier->async_work));
|
||||
|
||||
/*** this could/should be moved to napi-macros ***/
|
||||
|
||||
#define NAPI_DOUBLE(name, val) \
|
||||
double name; \
|
||||
if (napi_get_value_double(env, val, &name) != napi_ok) { \
|
||||
napi_throw_error(env, "EINVAL", "Expected double"); \
|
||||
return NULL; \
|
||||
}
|
||||
|
||||
#define NAPI_ARGV_DOUBLE(name, i) \
|
||||
NAPI_DOUBLE(name, argv[i])
|
||||
BIN
node/test/fixtures/avatar.png
vendored
Normal file
BIN
node/test/fixtures/avatar.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.7 KiB |
BIN
node/test/fixtures/image.jpeg
vendored
Normal file
BIN
node/test/fixtures/image.jpeg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 13 KiB |
BIN
node/test/fixtures/logo.png
vendored
Normal file
BIN
node/test/fixtures/logo.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 27 KiB |
862
node/test/test.js
Normal file
862
node/test/test.js
Normal file
@@ -0,0 +1,862 @@
|
||||
// @ts-check
|
||||
import DeltaChat, { Message } from '../dist'
|
||||
import binding from '../binding'
|
||||
|
||||
import { strictEqual } from 'assert'
|
||||
import chai, { expect } from 'chai'
|
||||
import chaiAsPromised from 'chai-as-promised'
|
||||
import { EventId2EventName, C } from '../dist/constants'
|
||||
import { join } from 'path'
|
||||
import { mkdtempSync, statSync } from 'fs'
|
||||
import { tmpdir } from 'os'
|
||||
import { Context } from '../dist/context'
|
||||
chai.use(chaiAsPromised)
|
||||
|
||||
async function createTempUser(url) {
|
||||
const fetch = require('node-fetch')
|
||||
|
||||
async function postData(url = '') {
|
||||
// Default options are marked with *
|
||||
const response = await fetch(url, {
|
||||
method: 'POST', // *GET, POST, PUT, DELETE, etc.
|
||||
mode: 'cors', // no-cors, *cors, same-origin
|
||||
cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
|
||||
credentials: 'same-origin', // include, *same-origin, omit
|
||||
headers: {
|
||||
'cache-control': 'no-cache',
|
||||
},
|
||||
referrerPolicy: 'no-referrer', // no-referrer, *client
|
||||
})
|
||||
return response.json() // parses JSON response into native JavaScript objects
|
||||
}
|
||||
|
||||
return await postData(url)
|
||||
}
|
||||
|
||||
describe('static tests', function () {
|
||||
it('reverse lookup of events', function () {
|
||||
const eventKeys = Object.keys(EventId2EventName).map((k) => Number(k))
|
||||
const eventValues = Object.values(EventId2EventName)
|
||||
const reverse = eventValues.map((v) => C[v])
|
||||
expect(reverse).to.be.deep.equal(eventKeys)
|
||||
})
|
||||
|
||||
it('event constants are consistent', function () {
|
||||
const eventKeys = Object.keys(C)
|
||||
.filter((k) => k.startsWith('DC_EVENT_'))
|
||||
.sort()
|
||||
const eventValues = Object.values(EventId2EventName).sort()
|
||||
expect(eventKeys).to.be.deep.equal(eventValues)
|
||||
})
|
||||
|
||||
it('static method maybeValidAddr()', function () {
|
||||
expect(DeltaChat.maybeValidAddr(null)).to.equal(false)
|
||||
expect(DeltaChat.maybeValidAddr('')).to.equal(false)
|
||||
expect(DeltaChat.maybeValidAddr('uuu')).to.equal(false)
|
||||
expect(DeltaChat.maybeValidAddr('dd.tt')).to.equal(false)
|
||||
expect(DeltaChat.maybeValidAddr('tt.dd@yggmail')).to.equal(true)
|
||||
expect(DeltaChat.maybeValidAddr('u@d')).to.equal(true)
|
||||
//expect(DeltaChat.maybeValidAddr('u@d.')).to.equal(false)
|
||||
//expect(DeltaChat.maybeValidAddr('u@d.t')).to.equal(false)
|
||||
//expect(DeltaChat.maybeValidAddr('u@.tt')).to.equal(false)
|
||||
expect(DeltaChat.maybeValidAddr('@d.tt')).to.equal(false)
|
||||
expect(DeltaChat.maybeValidAddr('user@domain.tld')).to.equal(true)
|
||||
expect(DeltaChat.maybeValidAddr('u@d.tt')).to.equal(true)
|
||||
})
|
||||
|
||||
it('static getSystemInfo()', function () {
|
||||
const info = Context.getSystemInfo()
|
||||
expect(info).to.contain.keys([
|
||||
'arch',
|
||||
'deltachat_core_version',
|
||||
'sqlite_version',
|
||||
])
|
||||
})
|
||||
|
||||
it('static context.getProviderFromEmail("example@example.com")', function () {
|
||||
const provider = DeltaChat.getProviderFromEmail('example@example.com')
|
||||
|
||||
expect(provider).to.deep.equal({
|
||||
before_login_hint: "Hush this provider doesn't exist!",
|
||||
overview_page: 'https://providers.delta.chat/example-com',
|
||||
status: 3,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Basic offline Tests', function () {
|
||||
it('opens a context', async function () {
|
||||
const { dc, context } = DeltaChat.newTemporary()
|
||||
|
||||
strictEqual(context.isConfigured(), false)
|
||||
dc.close()
|
||||
})
|
||||
|
||||
it('set config', async function () {
|
||||
const { dc, context } = DeltaChat.newTemporary()
|
||||
|
||||
context.setConfig('bot', true)
|
||||
strictEqual(context.getConfig('bot'), '1')
|
||||
context.setConfig('bot', false)
|
||||
strictEqual(context.getConfig('bot'), '0')
|
||||
context.setConfig('bot', '1')
|
||||
strictEqual(context.getConfig('bot'), '1')
|
||||
context.setConfig('bot', '0')
|
||||
strictEqual(context.getConfig('bot'), '0')
|
||||
context.setConfig('bot', 1)
|
||||
strictEqual(context.getConfig('bot'), '1')
|
||||
context.setConfig('bot', 0)
|
||||
strictEqual(context.getConfig('bot'), '0')
|
||||
|
||||
context.setConfig('bot', null)
|
||||
strictEqual(context.getConfig('bot'), '')
|
||||
|
||||
strictEqual(context.getConfig('selfstatus'), '')
|
||||
context.setConfig('selfstatus', 'hello')
|
||||
strictEqual(context.getConfig('selfstatus'), 'hello')
|
||||
context.setConfig('selfstatus', '')
|
||||
strictEqual(context.getConfig('selfstatus'), '')
|
||||
context.setConfig('selfstatus', null)
|
||||
strictEqual(context.getConfig('selfstatus'), '')
|
||||
|
||||
dc.close()
|
||||
})
|
||||
|
||||
it('configure with either missing addr or missing mail_pw throws', async function () {
|
||||
const { dc, context } = DeltaChat.newTemporary()
|
||||
dc.startEvents()
|
||||
|
||||
await expect(
|
||||
context.configure({ addr: 'delta1@delta.localhost' })
|
||||
).to.eventually.be.rejectedWith('Please enter a password.')
|
||||
await expect(context.configure({ mailPw: 'delta1' })).to.eventually.be
|
||||
.rejected
|
||||
|
||||
context.stopOngoingProcess()
|
||||
dc.close()
|
||||
})
|
||||
|
||||
it('context.getInfo()', async function () {
|
||||
const { dc, context } = DeltaChat.newTemporary()
|
||||
|
||||
const info = await context.getInfo()
|
||||
expect(typeof info).to.be.equal('object')
|
||||
expect(info).to.contain.keys([
|
||||
'arch',
|
||||
'bcc_self',
|
||||
'blobdir',
|
||||
'bot',
|
||||
'configured_mvbox_folder',
|
||||
'configured_sentbox_folder',
|
||||
'database_dir',
|
||||
'database_encrypted',
|
||||
'database_version',
|
||||
'delete_device_after',
|
||||
'delete_server_after',
|
||||
'deltachat_core_version',
|
||||
'display_name',
|
||||
'download_limit',
|
||||
'e2ee_enabled',
|
||||
'entered_account_settings',
|
||||
'fetch_existing_msgs',
|
||||
'fingerprint',
|
||||
'folders_configured',
|
||||
'is_configured',
|
||||
'journal_mode',
|
||||
'key_gen_type',
|
||||
'last_housekeeping',
|
||||
'level',
|
||||
'mdns_enabled',
|
||||
'media_quality',
|
||||
'messages_in_contact_requests',
|
||||
'mvbox_move',
|
||||
'num_cpus',
|
||||
'number_of_chat_messages',
|
||||
'number_of_chats',
|
||||
'number_of_contacts',
|
||||
'only_fetch_mvbox',
|
||||
'private_key_count',
|
||||
'public_key_count',
|
||||
'quota_exceeding',
|
||||
'scan_all_folders_debounce_secs',
|
||||
'selfavatar',
|
||||
'send_sync_msgs',
|
||||
'sentbox_watch',
|
||||
'show_emails',
|
||||
'socks5_enabled',
|
||||
'sqlite_version',
|
||||
'uptime',
|
||||
'used_account_settings',
|
||||
'webrtc_instance',
|
||||
])
|
||||
|
||||
dc.close()
|
||||
})
|
||||
})
|
||||
|
||||
describe('Offline Tests with unconfigured account', function () {
|
||||
let [dc, context, accountId, directory] = [null, null, null, null]
|
||||
|
||||
this.beforeEach(async function () {
|
||||
let tmp = DeltaChat.newTemporary()
|
||||
dc = tmp.dc
|
||||
context = tmp.context
|
||||
accountId = tmp.accountId
|
||||
directory = tmp.directory
|
||||
dc.startEvents()
|
||||
})
|
||||
|
||||
this.afterEach(async function () {
|
||||
if (context) {
|
||||
context.stopOngoingProcess()
|
||||
}
|
||||
if (dc) {
|
||||
try {
|
||||
dc.stopIO()
|
||||
dc.close()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
dc = null
|
||||
context = null
|
||||
accountId = null
|
||||
directory = null
|
||||
})
|
||||
|
||||
it('invalid context.joinSecurejoin', async function () {
|
||||
expect(context.joinSecurejoin('test')).to.be.eq(0)
|
||||
})
|
||||
|
||||
it('Device Chat', async function () {
|
||||
const deviceChatMessageText = 'test234'
|
||||
|
||||
expect((await context.getChatList(0, '', null)).getCount()).to.equal(
|
||||
0,
|
||||
'no device chat after setup'
|
||||
)
|
||||
|
||||
await context.addDeviceMessage('test', deviceChatMessageText)
|
||||
|
||||
const chatList = await context.getChatList(0, '', null)
|
||||
expect(chatList.getCount()).to.equal(
|
||||
1,
|
||||
'device chat after adding device msg'
|
||||
)
|
||||
|
||||
const deviceChatId = await chatList.getChatId(0)
|
||||
const deviceChat = await context.getChat(deviceChatId)
|
||||
expect(deviceChat.isDeviceTalk()).to.be.true
|
||||
expect(deviceChat.toJson().isDeviceTalk).to.be.true
|
||||
|
||||
const deviceChatMessages = await context.getChatMessages(deviceChatId, 0, 0)
|
||||
expect(deviceChatMessages.length).to.be.equal(
|
||||
1,
|
||||
'device chat has added message'
|
||||
)
|
||||
|
||||
const deviceChatMessage = await context.getMessage(deviceChatMessages[0])
|
||||
expect(deviceChatMessage.getText()).to.equal(
|
||||
deviceChatMessageText,
|
||||
'device chat message has the inserted text'
|
||||
)
|
||||
})
|
||||
|
||||
it('should have e2ee enabled and right blobdir', function () {
|
||||
expect(context.getConfig('e2ee_enabled')).to.equal(
|
||||
'1',
|
||||
'e2eeEnabled correct'
|
||||
)
|
||||
expect(
|
||||
String(context.getBlobdir()).startsWith(directory),
|
||||
'blobdir should be inside temp directory'
|
||||
)
|
||||
expect(
|
||||
String(context.getBlobdir()).endsWith('db.sqlite-blobs'),
|
||||
'blobdir end with "db.sqlite-blobs"'
|
||||
)
|
||||
})
|
||||
|
||||
it('should create chat from contact and Chat methods', async function () {
|
||||
const contactId = context.createContact('aaa', 'aaa@site.org')
|
||||
|
||||
strictEqual(context.lookupContactIdByAddr('aaa@site.org'), contactId)
|
||||
strictEqual(context.lookupContactIdByAddr('nope@site.net'), 0)
|
||||
|
||||
let chatId = context.createChatByContactId(contactId)
|
||||
let chat = context.getChat(chatId)
|
||||
|
||||
strictEqual(
|
||||
chat.getVisibility(),
|
||||
C.DC_CHAT_VISIBILITY_NORMAL,
|
||||
'not archived'
|
||||
)
|
||||
strictEqual(chat.getId(), chatId, 'chat id matches')
|
||||
strictEqual(chat.getName(), 'aaa', 'chat name matches')
|
||||
strictEqual(chat.getProfileImage(), null, 'no profile image')
|
||||
strictEqual(chat.getType(), C.DC_CHAT_TYPE_SINGLE, 'single chat')
|
||||
strictEqual(chat.isSelfTalk(), false, 'no self talk')
|
||||
// TODO make sure this is really the case!
|
||||
strictEqual(chat.isUnpromoted(), false, 'not unpromoted')
|
||||
strictEqual(chat.isProtected(), false, 'not verified')
|
||||
strictEqual(typeof chat.color, 'string', 'color is a string')
|
||||
|
||||
strictEqual(context.getDraft(chatId), null, 'no draft message')
|
||||
context.setDraft(chatId, context.messageNew().setText('w00t!'))
|
||||
strictEqual(
|
||||
context.getDraft(chatId).toJson().text,
|
||||
'w00t!',
|
||||
'draft text correct'
|
||||
)
|
||||
context.setDraft(chatId, null)
|
||||
strictEqual(context.getDraft(chatId), null, 'draft removed')
|
||||
|
||||
strictEqual(context.getChatIdByContactId(contactId), chatId)
|
||||
expect(context.getChatContacts(chatId)).to.deep.equal([contactId])
|
||||
|
||||
context.setChatVisibility(chatId, C.DC_CHAT_VISIBILITY_ARCHIVED)
|
||||
strictEqual(
|
||||
context.getChat(chatId).getVisibility(),
|
||||
C.DC_CHAT_VISIBILITY_ARCHIVED,
|
||||
'chat archived'
|
||||
)
|
||||
context.setChatVisibility(chatId, C.DC_CHAT_VISIBILITY_NORMAL)
|
||||
strictEqual(
|
||||
chat.getVisibility(),
|
||||
C.DC_CHAT_VISIBILITY_NORMAL,
|
||||
'chat unarchived'
|
||||
)
|
||||
|
||||
chatId = context.createGroupChat('unverified group', false)
|
||||
chat = context.getChat(chatId)
|
||||
strictEqual(chat.isProtected(), false, 'is not verified')
|
||||
strictEqual(chat.getType(), C.DC_CHAT_TYPE_GROUP, 'group chat')
|
||||
expect(context.getChatContacts(chatId)).to.deep.equal([
|
||||
C.DC_CONTACT_ID_SELF,
|
||||
])
|
||||
|
||||
const draft2 = context.getDraft(chatId)
|
||||
expect(draft2 == null, 'unpromoted group has no draft by default')
|
||||
|
||||
context.setChatName(chatId, 'NEW NAME')
|
||||
strictEqual(context.getChat(chatId).getName(), 'NEW NAME', 'name updated')
|
||||
|
||||
chatId = context.createGroupChat('a verified group', true)
|
||||
chat = context.getChat(chatId)
|
||||
strictEqual(chat.isProtected(), true, 'is verified')
|
||||
})
|
||||
|
||||
it('test setting profile image', async function () {
|
||||
const chatId = context.createGroupChat('testing profile image group', false)
|
||||
const image = 'image.jpeg'
|
||||
const imagePath = join(__dirname, 'fixtures', image)
|
||||
const blobs = context.getBlobdir()
|
||||
|
||||
context.setChatProfileImage(chatId, imagePath)
|
||||
const blobPath = context.getChat(chatId).getProfileImage()
|
||||
expect(blobPath.startsWith(blobs)).to.be.true
|
||||
expect(blobPath.endsWith(image)).to.be.true
|
||||
|
||||
context.setChatProfileImage(chatId, null)
|
||||
expect(context.getChat(chatId).getProfileImage()).to.be.equal(
|
||||
null,
|
||||
'image is null'
|
||||
)
|
||||
})
|
||||
|
||||
it('test setting ephemeral timer', function () {
|
||||
const chatId = context.createGroupChat('testing ephemeral timer')
|
||||
|
||||
strictEqual(
|
||||
context.getChatEphemeralTimer(chatId),
|
||||
0,
|
||||
'ephemeral timer is not set by default'
|
||||
)
|
||||
|
||||
context.setChatEphemeralTimer(chatId, 60)
|
||||
strictEqual(
|
||||
context.getChatEphemeralTimer(chatId),
|
||||
60,
|
||||
'ephemeral timer is set to 1 minute'
|
||||
)
|
||||
|
||||
context.setChatEphemeralTimer(chatId, 0)
|
||||
strictEqual(
|
||||
context.getChatEphemeralTimer(chatId),
|
||||
0,
|
||||
'ephemeral timer is reset'
|
||||
)
|
||||
})
|
||||
|
||||
it('should create and delete chat', function () {
|
||||
const chatId = context.createGroupChat('GROUPCHAT')
|
||||
const chat = context.getChat(chatId)
|
||||
strictEqual(chat.getId(), chatId, 'correct chatId')
|
||||
context.deleteChat(chat.getId())
|
||||
strictEqual(context.getChat(chatId), null, 'chat removed')
|
||||
})
|
||||
|
||||
it('new message and Message methods', function () {
|
||||
const text = 'w00t!'
|
||||
const msg = context.messageNew().setText(text)
|
||||
|
||||
strictEqual(msg.getChatId(), 0, 'chat id 0 before sent')
|
||||
strictEqual(msg.getDuration(), 0, 'duration 0 before sent')
|
||||
strictEqual(msg.getFile(), '', 'no file set by default')
|
||||
strictEqual(msg.getFilebytes(), 0, 'and file bytes is 0')
|
||||
strictEqual(msg.getFilemime(), '', 'no filemime by default')
|
||||
strictEqual(msg.getFilename(), '', 'no filename set by default')
|
||||
strictEqual(msg.getFromId(), 0, 'no contact id set by default')
|
||||
strictEqual(msg.getHeight(), 0, 'plain text message have height 0')
|
||||
strictEqual(msg.getId(), 0, 'id 0 before sent')
|
||||
strictEqual(msg.getSetupcodebegin(), '', 'no setupcode begin')
|
||||
strictEqual(msg.getShowpadlock(), false, 'no padlock by default')
|
||||
|
||||
const state = msg.getState()
|
||||
strictEqual(state.isUndefined(), true, 'no state by default')
|
||||
strictEqual(state.isFresh(), false, 'no state by default')
|
||||
strictEqual(state.isNoticed(), false, 'no state by default')
|
||||
strictEqual(state.isSeen(), false, 'no state by default')
|
||||
strictEqual(state.isPending(), false, 'no state by default')
|
||||
strictEqual(state.isFailed(), false, 'no state by default')
|
||||
strictEqual(state.isDelivered(), false, 'no state by default')
|
||||
strictEqual(state.isReceived(), false, 'no state by default')
|
||||
|
||||
const summary = msg.getSummary()
|
||||
strictEqual(summary.getId(), 0, 'no summary id')
|
||||
strictEqual(summary.getState(), 0, 'no summary state')
|
||||
strictEqual(summary.getText1(), null, 'no summary text1')
|
||||
strictEqual(summary.getText1Meaning(), 0, 'no summary text1 meaning')
|
||||
strictEqual(summary.getText2(), '', 'no summary text2')
|
||||
strictEqual(summary.getTimestamp(), 0, 'no summary timestamp')
|
||||
|
||||
//strictEqual(msg.getSummarytext(50), text, 'summary text is text')
|
||||
strictEqual(msg.getText(), text, 'msg text set correctly')
|
||||
strictEqual(msg.getTimestamp(), 0, 'no timestamp')
|
||||
|
||||
const viewType = msg.getViewType()
|
||||
strictEqual(viewType.isText(), true)
|
||||
strictEqual(viewType.isImage(), false)
|
||||
strictEqual(viewType.isGif(), false)
|
||||
strictEqual(viewType.isAudio(), false)
|
||||
strictEqual(viewType.isVoice(), false)
|
||||
strictEqual(viewType.isVideo(), false)
|
||||
strictEqual(viewType.isFile(), false)
|
||||
|
||||
strictEqual(msg.getWidth(), 0, 'no message width')
|
||||
strictEqual(msg.isDeadDrop(), false, 'not deaddrop')
|
||||
strictEqual(msg.isForwarded(), false, 'not forwarded')
|
||||
strictEqual(msg.isIncreation(), false, 'not in creation')
|
||||
strictEqual(msg.isInfo(), false, 'not an info message')
|
||||
strictEqual(msg.isSent(), false, 'messge is not sent')
|
||||
strictEqual(msg.isSetupmessage(), false, 'not an autocrypt setup message')
|
||||
|
||||
msg.latefilingMediasize(10, 20, 30)
|
||||
strictEqual(msg.getWidth(), 10, 'message width set correctly')
|
||||
strictEqual(msg.getHeight(), 20, 'message height set correctly')
|
||||
strictEqual(msg.getDuration(), 30, 'message duration set correctly')
|
||||
|
||||
msg.setDimension(100, 200)
|
||||
strictEqual(msg.getWidth(), 100, 'message width set correctly')
|
||||
strictEqual(msg.getHeight(), 200, 'message height set correctly')
|
||||
|
||||
msg.setDuration(314)
|
||||
strictEqual(msg.getDuration(), 314, 'message duration set correctly')
|
||||
|
||||
expect(() => {
|
||||
msg.setFile(null)
|
||||
}).to.throw('Missing filename')
|
||||
|
||||
const logo = join(__dirname, 'fixtures', 'logo.png')
|
||||
const stat = statSync(logo)
|
||||
msg.setFile(logo)
|
||||
strictEqual(msg.getFilebytes(), stat.size, 'correct file size')
|
||||
strictEqual(msg.getFile(), logo, 'correct file name')
|
||||
strictEqual(msg.getFilemime(), 'image/png', 'mime set implicitly')
|
||||
msg.setFile(logo, 'image/gif')
|
||||
strictEqual(msg.getFilemime(), 'image/gif', 'mime set (in)correctly')
|
||||
msg.setFile(logo, 'image/png')
|
||||
strictEqual(msg.getFilemime(), 'image/png', 'mime set correctly')
|
||||
|
||||
const json = msg.toJson()
|
||||
expect(json).to.not.equal(null, 'not null')
|
||||
strictEqual(typeof json, 'object', 'json object')
|
||||
})
|
||||
|
||||
it('Contact methods', function () {
|
||||
const contactId = context.createContact('First Last', 'first.last@site.org')
|
||||
const contact = context.getContact(contactId)
|
||||
|
||||
strictEqual(contact.getAddress(), 'first.last@site.org', 'correct address')
|
||||
strictEqual(typeof contact.color, 'string', 'color is a string')
|
||||
strictEqual(contact.getDisplayName(), 'First Last', 'correct display name')
|
||||
strictEqual(contact.getId(), contactId, 'contact id matches')
|
||||
strictEqual(contact.getName(), 'First Last', 'correct name')
|
||||
strictEqual(contact.getNameAndAddress(), 'First Last (first.last@site.org)')
|
||||
strictEqual(contact.getProfileImage(), null, 'no contact image')
|
||||
strictEqual(contact.isBlocked(), false, 'not blocked')
|
||||
strictEqual(contact.isVerified(), false, 'unverified status')
|
||||
strictEqual(contact.lastSeen, 0, 'last seen unknown')
|
||||
})
|
||||
|
||||
it('create contacts from address book', function () {
|
||||
const addresses = [
|
||||
'Name One',
|
||||
'name1@site.org',
|
||||
'Name Two',
|
||||
'name2@site.org',
|
||||
'Name Three',
|
||||
'name3@site.org',
|
||||
]
|
||||
const count = context.addAddressBook(addresses.join('\n'))
|
||||
strictEqual(count, addresses.length / 2)
|
||||
context
|
||||
.getContacts(0, 'Name ')
|
||||
.map((id) => context.getContact(id))
|
||||
.forEach((contact) => {
|
||||
expect(contact.getName().startsWith('Name ')).to.be.true
|
||||
})
|
||||
})
|
||||
|
||||
it('delete contacts', function () {
|
||||
const id = context.createContact('someuser', 'someuser@site.com')
|
||||
const contact = context.getContact(id)
|
||||
strictEqual(contact.getId(), id, 'contact id matches')
|
||||
strictEqual(context.deleteContact(id), true, 'delete call succesful')
|
||||
strictEqual(context.getContact(id), null, 'contact is gone')
|
||||
})
|
||||
|
||||
it('adding and removing a contact from a chat', function () {
|
||||
const chatId = context.createGroupChat('adding_and_removing')
|
||||
const contactId = context.createContact('Add Remove', 'add.remove@site.com')
|
||||
strictEqual(
|
||||
context.addContactToChat(chatId, contactId),
|
||||
true,
|
||||
'contact added'
|
||||
)
|
||||
strictEqual(
|
||||
context.isContactInChat(chatId, contactId),
|
||||
true,
|
||||
'contact in chat'
|
||||
)
|
||||
strictEqual(
|
||||
context.removeContactFromChat(chatId, contactId),
|
||||
true,
|
||||
'contact removed'
|
||||
)
|
||||
strictEqual(
|
||||
context.isContactInChat(chatId, contactId),
|
||||
false,
|
||||
'contact not in chat'
|
||||
)
|
||||
})
|
||||
|
||||
it('blocking contacts', function () {
|
||||
const id = context.createContact('badcontact', 'bad@site.com')
|
||||
|
||||
strictEqual(context.getBlockedCount(), 0)
|
||||
strictEqual(context.getContact(id).isBlocked(), false)
|
||||
expect(context.getBlockedContacts()).to.be.empty
|
||||
|
||||
context.blockContact(id, true)
|
||||
strictEqual(context.getBlockedCount(), 1)
|
||||
strictEqual(context.getContact(id).isBlocked(), true)
|
||||
expect(context.getBlockedContacts()).to.deep.equal([id])
|
||||
|
||||
context.blockContact(id, false)
|
||||
strictEqual(context.getBlockedCount(), 0)
|
||||
strictEqual(context.getContact(id).isBlocked(), false)
|
||||
expect(context.getBlockedContacts()).to.be.empty
|
||||
})
|
||||
|
||||
it('ChatList methods', function () {
|
||||
const ids = [
|
||||
context.createGroupChat('groupchat1'),
|
||||
context.createGroupChat('groupchat11'),
|
||||
context.createGroupChat('groupchat111'),
|
||||
]
|
||||
|
||||
let chatList = context.getChatList(0, 'groupchat1', null)
|
||||
strictEqual(chatList.getCount(), 3, 'should contain above chats')
|
||||
expect(ids.indexOf(chatList.getChatId(0))).not.to.equal(-1)
|
||||
expect(ids.indexOf(chatList.getChatId(1))).not.to.equal(-1)
|
||||
expect(ids.indexOf(chatList.getChatId(2))).not.to.equal(-1)
|
||||
|
||||
const lot = chatList.getSummary(0)
|
||||
strictEqual(lot.getId(), 0, 'lot has no id')
|
||||
strictEqual(lot.getState(), C.DC_STATE_UNDEFINED, 'correct state')
|
||||
|
||||
const text = 'No messages.'
|
||||
context.createGroupChat('groupchat1111')
|
||||
chatList = context.getChatList(0, 'groupchat1111', null)
|
||||
strictEqual(
|
||||
chatList.getSummary(0).getText2(),
|
||||
text,
|
||||
'custom new group message'
|
||||
)
|
||||
|
||||
context.setChatVisibility(ids[0], C.DC_CHAT_VISIBILITY_ARCHIVED)
|
||||
chatList = context.getChatList(C.DC_GCL_ARCHIVED_ONLY, 'groupchat1', null)
|
||||
strictEqual(chatList.getCount(), 1, 'only one archived')
|
||||
})
|
||||
|
||||
it('Remove qoute from (draft) message', function () {
|
||||
context.addDeviceMessage('test_qoute', 'test')
|
||||
const msgId = context.getChatMessages(10, 0, 0)[0]
|
||||
const msg = context.messageNew()
|
||||
|
||||
msg.setQuote(context.getMessage(msgId))
|
||||
expect(msg.getQuotedMessage()).to.not.be.null
|
||||
msg.setQuote(null)
|
||||
expect(msg.getQuotedMessage()).to.be.null
|
||||
})
|
||||
})
|
||||
|
||||
describe('Integration tests', function () {
|
||||
this.timeout(60 * 3000) // increase timeout to 1min
|
||||
|
||||
let [dc, context, accountId, directory, account] = [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
]
|
||||
|
||||
let [dc2, context2, accountId2, directory2, account2] = [
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
]
|
||||
|
||||
this.beforeEach(async function () {
|
||||
let tmp = DeltaChat.newTemporary()
|
||||
dc = tmp.dc
|
||||
context = tmp.context
|
||||
accountId = tmp.accountId
|
||||
directory = tmp.directory
|
||||
dc.startEvents()
|
||||
})
|
||||
|
||||
this.afterEach(async function () {
|
||||
if (context) {
|
||||
try {
|
||||
context.stopOngoingProcess()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
if (context2) {
|
||||
try {
|
||||
context2.stopOngoingProcess()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
if (dc) {
|
||||
try {
|
||||
dc.stopIO()
|
||||
dc.close()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
dc = null
|
||||
context = null
|
||||
accountId = null
|
||||
directory = null
|
||||
|
||||
context2 = null
|
||||
accountId2 = null
|
||||
directory2 = null
|
||||
})
|
||||
|
||||
this.beforeAll(async function () {
|
||||
if (!process.env.DCC_NEW_TMP_EMAIL) {
|
||||
console.log(
|
||||
'Missing DCC_NEW_TMP_EMAIL environment variable!, skip intergration tests'
|
||||
)
|
||||
this.skip()
|
||||
}
|
||||
|
||||
account = await createTempUser(process.env.DCC_NEW_TMP_EMAIL)
|
||||
if (!account || !account.email || !account.password) {
|
||||
console.log(
|
||||
"We didn't got back an account from the api, skip intergration tests"
|
||||
)
|
||||
this.skip()
|
||||
}
|
||||
})
|
||||
|
||||
it('configure', async function () {
|
||||
strictEqual(context.isConfigured(), false, 'should not be configured')
|
||||
|
||||
// Not sure what's the best way to check the events
|
||||
// TODO: check the events
|
||||
|
||||
// dc.once('DC_EVENT_CONFIGURE_PROGRESS', (data) => {
|
||||
// t.pass('DC_EVENT_CONFIGURE_PROGRESS called at least once')
|
||||
// })
|
||||
// dc.on('DC_EVENT_ERROR', (error) => {
|
||||
// console.error('DC_EVENT_ERROR', error)
|
||||
// })
|
||||
// dc.on('DC_EVENT_ERROR_NETWORK', (first, error) => {
|
||||
// console.error('DC_EVENT_ERROR_NETWORK', error)
|
||||
// })
|
||||
|
||||
// dc.on('ALL', (event, data1, data2) => console.log('ALL', event, data1, data2))
|
||||
|
||||
await expect(
|
||||
context.configure({
|
||||
addr: account.email,
|
||||
mail_pw: account.password,
|
||||
|
||||
displayname: 'Delta One',
|
||||
selfstatus: 'From Delta One with <3',
|
||||
selfavatar: join(__dirname, 'fixtures', 'avatar.png'),
|
||||
})
|
||||
).to.be.eventually.fulfilled
|
||||
|
||||
strictEqual(context.getConfig('addr'), account.email, 'addr correct')
|
||||
strictEqual(
|
||||
context.getConfig('displayname'),
|
||||
'Delta One',
|
||||
'displayName correct'
|
||||
)
|
||||
strictEqual(
|
||||
context.getConfig('selfstatus'),
|
||||
'From Delta One with <3',
|
||||
'selfStatus correct'
|
||||
)
|
||||
expect(
|
||||
context.getConfig('selfavatar').endsWith('avatar.png'),
|
||||
'selfavatar correct'
|
||||
)
|
||||
strictEqual(context.getConfig('e2ee_enabled'), '1', 'e2ee_enabled correct')
|
||||
strictEqual(
|
||||
context.getConfig('save_mime_headers'),
|
||||
'',
|
||||
'save_mime_headers correct'
|
||||
)
|
||||
|
||||
expect(context.getBlobdir().endsWith('db.sqlite-blobs'), 'correct blobdir')
|
||||
strictEqual(context.isConfigured(), true, 'is configured')
|
||||
|
||||
// whole re-configure to only change displayname: what the heck? (copied this from the old test)
|
||||
await expect(
|
||||
context.configure({
|
||||
addr: account.email,
|
||||
mail_pw: account.password,
|
||||
displayname: 'Delta Two',
|
||||
selfstatus: 'From Delta One with <3',
|
||||
selfavatar: join(__dirname, 'fixtures', 'avatar.png'),
|
||||
})
|
||||
).to.be.eventually.fulfilled
|
||||
strictEqual(
|
||||
context.getConfig('displayname'),
|
||||
'Delta Two',
|
||||
'updated displayName correct'
|
||||
)
|
||||
})
|
||||
|
||||
it('Autocrypt setup - key transfer', async function () {
|
||||
// Spawn a second dc instance with same account
|
||||
// dc.on('ALL', (event, data1, data2) =>
|
||||
// console.log('FIRST ', event, data1, data2)
|
||||
// )
|
||||
dc.stopIO()
|
||||
await expect(
|
||||
context.configure({
|
||||
addr: account.email,
|
||||
mail_pw: account.password,
|
||||
|
||||
displayname: 'Delta One',
|
||||
selfstatus: 'From Delta One with <3',
|
||||
selfavatar: join(__dirname, 'fixtures', 'avatar.png'),
|
||||
})
|
||||
).to.be.eventually.fulfilled
|
||||
|
||||
const accountId2 = dc.addAccount()
|
||||
console.log('accountId2:', accountId2)
|
||||
context2 = dc.accountContext(accountId2)
|
||||
|
||||
let setupCode = null
|
||||
const waitForSetupCode = waitForSomething()
|
||||
const waitForEnd = waitForSomething()
|
||||
|
||||
dc.on('ALL', (event, accountId, data1, data2) => {
|
||||
console.log('[' + accountId + ']', event, data1, data2)
|
||||
})
|
||||
|
||||
dc.on('DC_EVENT_MSGS_CHANGED', async (aId, chatId, msgId) => {
|
||||
console.log('[' + accountId + '] DC_EVENT_MSGS_CHANGED', chatId, msgId)
|
||||
if (
|
||||
aId != accountId ||
|
||||
!context.getChat(chatId).isSelfTalk() ||
|
||||
!context.getMessage(msgId).isSetupmessage()
|
||||
) {
|
||||
return
|
||||
}
|
||||
console.log('Setupcode!')
|
||||
let setupCode = await waitForSetupCode.promise
|
||||
// console.log('incoming msg', { setupCode })
|
||||
const messages = context.getChatMessages(chatId, 0, 0)
|
||||
expect(messages.indexOf(msgId) !== -1, 'msgId is in chat messages').to.be
|
||||
.true
|
||||
const result = await context.continueKeyTransfer(msgId, setupCode)
|
||||
expect(result === true, 'continueKeyTransfer was successful').to.be.true
|
||||
|
||||
waitForEnd.done()
|
||||
})
|
||||
|
||||
dc.stopIO()
|
||||
await expect(
|
||||
context2.configure({
|
||||
addr: account.email,
|
||||
mail_pw: account.password,
|
||||
|
||||
displayname: 'Delta One',
|
||||
selfstatus: 'From Delta One with <3',
|
||||
selfavatar: join(__dirname, 'fixtures', 'avatar.png'),
|
||||
})
|
||||
).to.be.eventually.fulfilled
|
||||
dc.startIO()
|
||||
|
||||
console.log('Sending autocrypt setup code')
|
||||
setupCode = await context2.initiateKeyTransfer()
|
||||
console.log('Sent autocrypt setup code')
|
||||
waitForSetupCode.done(setupCode)
|
||||
console.log('setupCode is: ' + setupCode)
|
||||
expect(typeof setupCode).to.equal('string', 'setupCode is string')
|
||||
|
||||
await waitForEnd.promise
|
||||
})
|
||||
|
||||
it('configure using invalid password should fail', async function () {
|
||||
await expect(
|
||||
context.configure({
|
||||
addr: 'hpk5@testrun.org',
|
||||
mail_pw: 'asd',
|
||||
})
|
||||
).to.be.eventually.rejected
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* @returns {{done: (result?)=>void, promise:Promise<any> }}
|
||||
*/
|
||||
function waitForSomething() {
|
||||
let resolvePromise
|
||||
const promise = new Promise((res, rej) => {
|
||||
resolvePromise = res
|
||||
})
|
||||
return {
|
||||
done: resolvePromise,
|
||||
promise,
|
||||
}
|
||||
}
|
||||
22
node/tsconfig.json
Normal file
22
node/tsconfig.json
Normal file
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"outDir": "dist",
|
||||
"rootDir": "./lib",
|
||||
"sourceMap": true,
|
||||
"module": "commonjs",
|
||||
"target": "es5",
|
||||
"esModuleInterop": true,
|
||||
"declaration": true,
|
||||
"declarationMap": true,
|
||||
"strictNullChecks": true,
|
||||
"strict": true
|
||||
},
|
||||
"exclude": ["node_modules", "deltachat-core-rust", "dist", "scripts"],
|
||||
"typedocOptions": {
|
||||
"mode": "file",
|
||||
"out": "docs",
|
||||
"excludeNotExported": true,
|
||||
"defaultCategory": "index",
|
||||
"includeVersion": true
|
||||
}
|
||||
}
|
||||
37
node/windows.md
Normal file
37
node/windows.md
Normal file
@@ -0,0 +1,37 @@
|
||||
> Steps on how to get windows set up properly for the node bindings
|
||||
|
||||
## install git
|
||||
|
||||
E.g via <https://git-scm.com/download/win>
|
||||
|
||||
## install node
|
||||
|
||||
Download and install `v16` from <https://nodejs.org/en/>
|
||||
|
||||
## install rust
|
||||
|
||||
Download and run `rust-init.exe` from <https://www.rust-lang.org/tools/install>
|
||||
|
||||
## configure node for native addons
|
||||
|
||||
```
|
||||
$ npm i node-gyp -g
|
||||
$ npm i windows-build-tools -g
|
||||
```
|
||||
|
||||
`windows-build-tools` will install `Visual Studio 2017` by default and should not mess with existing installations of `Visual Studio C++`.
|
||||
|
||||
## get the code
|
||||
|
||||
```
|
||||
$ mkdir -p src/deltachat
|
||||
$ cd src/deltachat
|
||||
$ git clone https://github.com/deltachat/deltachat-node
|
||||
```
|
||||
|
||||
## build the code
|
||||
|
||||
```
|
||||
$ cd src/deltachat/deltachat-node
|
||||
$ npm install
|
||||
```
|
||||
65
package.json
Normal file
65
package.json
Normal file
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"dependencies": {
|
||||
"debug": "^4.1.1",
|
||||
"napi-macros": "^2.0.0",
|
||||
"node-gyp-build": "^4.1.0",
|
||||
"split2": "^3.1.1"
|
||||
},
|
||||
"description": "node.js bindings for deltachat-core",
|
||||
"devDependencies": {
|
||||
"@types/debug": "^4.1.7",
|
||||
"@types/node": "^16.11.26",
|
||||
"chai": "^4.2.0",
|
||||
"chai-as-promised": "^7.1.1",
|
||||
"esm": "^3.2.25",
|
||||
"hallmark": "^2.0.0",
|
||||
"mocha": "^8.2.1",
|
||||
"node-fetch": "^2.6.7",
|
||||
"node-gyp": "^9.0.0",
|
||||
"opn-cli": "^5.0.0",
|
||||
"prebuildify": "^3.0.0",
|
||||
"prebuildify-ci": "^1.0.4",
|
||||
"prettier": "^2.0.5",
|
||||
"typedoc": "^0.17.0",
|
||||
"typescript": "^3.9.10"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.0.0"
|
||||
},
|
||||
"files": [
|
||||
"node/scripts/*",
|
||||
"*"
|
||||
],
|
||||
"homepage": "https://github.com/deltachat/deltachat-core-rust/tree/master/node",
|
||||
"license": "GPL-3.0-or-later",
|
||||
"main": "node/dist/index.js",
|
||||
"name": "deltachat-node",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/deltachat/deltachat-core-rust.git"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "npm run build:core && npm run build:bindings",
|
||||
"build:bindings": "npm run build:bindings:c && npm run build:bindings:ts",
|
||||
"build:bindings:c": "npm run build:bindings:c:c && npm run build:bindings:c:postinstall",
|
||||
"build:bindings:c:c": "cd node && npx node-gyp rebuild",
|
||||
"build:bindings:c:postinstall": "node node/scripts/postinstall.js",
|
||||
"build:bindings:ts": "cd node && tsc",
|
||||
"build:core": "npm run build:core:rust && npm run build:core:constants",
|
||||
"build:core:constants": "node node/scripts/generate-constants.js",
|
||||
"build:core:rust": "node node/scripts/rebuild-core.js",
|
||||
"clean": "rm -rf node/dist node/build node/prebuilds node/node_modules ./target",
|
||||
"download-prebuilds": "prebuildify-ci download",
|
||||
"hallmark": "hallmark --fix",
|
||||
"install": "node node/scripts/install.js",
|
||||
"install:prebuilds": "cd node && node-gyp-build \"npm run build:core\" \"npm run build:bindings:c:postinstall\"",
|
||||
"lint": "prettier --check \"node/lib/**/*.{ts,tsx}\"",
|
||||
"lint-fix": "prettier --write \"node/lib/**/*.{ts,tsx}\" \"node/test/**/*.js\"",
|
||||
"prebuildify": "cd node && prebuildify -t 16.13.0 --napi --strip --postinstall \"node scripts/postinstall.js --prebuild\"",
|
||||
"test": "npm run test:lint && npm run test:mocha",
|
||||
"test:lint": "npm run lint",
|
||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec"
|
||||
},
|
||||
"types": "node/dist/index.d.ts",
|
||||
"version": "1.81.0"
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
[mypy]
|
||||
|
||||
[mypy-py.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
[mypy-deltachat.capi.*]
|
||||
ignore_missing_imports = True
|
||||
|
||||
|
||||
@@ -177,6 +177,9 @@ class IdleManager:
|
||||
def __init__(self, direct_imap):
|
||||
self.direct_imap = direct_imap
|
||||
self.log = direct_imap.account.log
|
||||
# fetch latest messages before starting idle so that it only
|
||||
# returns messages that arrive anew
|
||||
self.direct_imap.conn.fetch("1:*")
|
||||
self.direct_imap.conn.idle.start()
|
||||
|
||||
def check(self, timeout=None) -> List[bytes]:
|
||||
|
||||
@@ -13,6 +13,7 @@ from typing import List, Callable
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
import pathlib
|
||||
|
||||
from . import Account, const, account_hookimpl, get_core_info
|
||||
from .events import FFIEventLogger, FFIEventTracker
|
||||
@@ -138,6 +139,8 @@ class TestProcess:
|
||||
"""
|
||||
def __init__(self, pytestconfig):
|
||||
self.pytestconfig = pytestconfig
|
||||
self._addr2files = {}
|
||||
self._configlist = []
|
||||
|
||||
def get_liveconfig_producer(self):
|
||||
""" provide live account configs, cached on a per-test-process scope
|
||||
@@ -149,7 +152,6 @@ class TestProcess:
|
||||
if not liveconfig_opt:
|
||||
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig to provide live accounts")
|
||||
|
||||
configlist = []
|
||||
if not liveconfig_opt.startswith("http"):
|
||||
for line in open(liveconfig_opt):
|
||||
if line.strip() and not line.strip().startswith('#'):
|
||||
@@ -157,14 +159,14 @@ class TestProcess:
|
||||
for part in line.split():
|
||||
name, value = part.split("=")
|
||||
d[name] = value
|
||||
configlist.append(d)
|
||||
self._configlist.append(d)
|
||||
|
||||
yield from iter(configlist)
|
||||
yield from iter(self._configlist)
|
||||
else:
|
||||
MAX_LIVE_CREATED_ACCOUNTS = 10
|
||||
for index in range(MAX_LIVE_CREATED_ACCOUNTS):
|
||||
try:
|
||||
yield configlist[index]
|
||||
yield self._configlist[index]
|
||||
except IndexError:
|
||||
res = requests.post(liveconfig_opt)
|
||||
if res.status_code != 200:
|
||||
@@ -173,10 +175,52 @@ class TestProcess:
|
||||
d = res.json()
|
||||
config = dict(addr=d["email"], mail_pw=d["password"])
|
||||
print("newtmpuser {}: addr={}".format(index, config["addr"]))
|
||||
configlist.append(config)
|
||||
self._configlist.append(config)
|
||||
yield config
|
||||
pytest.fail("more than {} live accounts requested.".format(MAX_LIVE_CREATED_ACCOUNTS))
|
||||
|
||||
def cache_maybe_retrieve_configured_db_files(self, cache_addr, db_target_path):
|
||||
db_target_path = pathlib.Path(db_target_path)
|
||||
assert not db_target_path.exists()
|
||||
|
||||
try:
|
||||
filescache = self._addr2files[cache_addr]
|
||||
except KeyError:
|
||||
print("CACHE FAIL for", cache_addr)
|
||||
return False
|
||||
else:
|
||||
print("CACHE HIT for", cache_addr)
|
||||
targetdir = db_target_path.parent
|
||||
write_dict_to_dir(filescache, targetdir)
|
||||
return True
|
||||
|
||||
def cache_maybe_store_configured_db_files(self, acc):
|
||||
addr = acc.get_config("addr")
|
||||
assert acc.is_configured()
|
||||
# don't overwrite existing entries
|
||||
if addr not in self._addr2files:
|
||||
print("storing cache for", addr)
|
||||
basedir = pathlib.Path(acc.get_blobdir()).parent
|
||||
self._addr2files[addr] = create_dict_from_files_in_path(basedir)
|
||||
return True
|
||||
|
||||
|
||||
def create_dict_from_files_in_path(base):
|
||||
cachedict = {}
|
||||
for path in base.glob("**/*"):
|
||||
if path.is_file():
|
||||
cachedict[path.relative_to(base)] = path.read_bytes()
|
||||
return cachedict
|
||||
|
||||
|
||||
def write_dict_to_dir(dic, target_dir):
|
||||
assert dic
|
||||
for relpath, content in dic.items():
|
||||
path = target_dir.joinpath(relpath)
|
||||
if not path.parent.exists():
|
||||
os.makedirs(path.parent)
|
||||
path.write_bytes(content)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def data(request):
|
||||
@@ -217,12 +261,22 @@ class ACSetup:
|
||||
CONFIGURED = "CONFIGURED"
|
||||
IDLEREADY = "IDLEREADY"
|
||||
|
||||
def __init__(self, init_time):
|
||||
def __init__(self, testprocess, init_time):
|
||||
self._configured_events = Queue()
|
||||
self._account2state = {}
|
||||
self._imap_cleaned = set()
|
||||
self.testprocess = testprocess
|
||||
self.init_time = init_time
|
||||
|
||||
def log(self, *args):
|
||||
print("[acsetup]", "{:.3f}".format(time.time() - self.init_time), *args)
|
||||
|
||||
def add_configured(self, account):
|
||||
""" add an already configured account. """
|
||||
assert account.is_configured()
|
||||
self._account2state[account] = self.CONFIGURED
|
||||
self.log("added already configured account", account, account.get_config("addr"))
|
||||
|
||||
def start_configure(self, account, reconfigure=False):
|
||||
""" add an account and start its configure process. """
|
||||
class PendingTracker:
|
||||
@@ -233,7 +287,7 @@ class ACSetup:
|
||||
account.add_account_plugin(PendingTracker(), name="pending_tracker")
|
||||
self._account2state[account] = self.CONFIGURING
|
||||
account.configure(reconfigure=reconfigure)
|
||||
print("started configure on pending", account)
|
||||
self.log("started configure on", account)
|
||||
|
||||
def wait_one_configured(self, account):
|
||||
""" wait until this account has successfully configured. """
|
||||
@@ -242,7 +296,8 @@ class ACSetup:
|
||||
acc = self._pop_config_success()
|
||||
if acc == account:
|
||||
break
|
||||
self.init_direct_imap_and_logging(acc)
|
||||
self.init_imap(acc)
|
||||
self.init_logging(acc)
|
||||
acc._evtracker.consume_events()
|
||||
|
||||
def bring_online(self):
|
||||
@@ -272,25 +327,24 @@ class ACSetup:
|
||||
return acc
|
||||
|
||||
def _onconfigure_start_io(self, acc):
|
||||
self.init_imap(acc)
|
||||
self.init_logging(acc)
|
||||
acc.start_io()
|
||||
print(acc._logid, "waiting for inbox IDLE to become ready")
|
||||
acc._evtracker.wait_idle_inbox_ready()
|
||||
self.init_direct_imap_and_logging(acc)
|
||||
acc._evtracker.consume_events()
|
||||
acc.log("inbox IDLE ready")
|
||||
|
||||
def init_direct_imap_and_logging(self, acc):
|
||||
""" idempotent function for initializing direct_imap and logging for an account. """
|
||||
self.init_direct_imap(acc)
|
||||
self.init_logging(acc)
|
||||
|
||||
def init_logging(self, acc):
|
||||
""" idempotent function for initializing logging (will replace existing logger). """
|
||||
logger = FFIEventLogger(acc, logid=acc._logid, init_time=self.init_time)
|
||||
acc.add_account_plugin(logger, name=acc._logid)
|
||||
acc.add_account_plugin(logger, name="logger-" + acc._logid)
|
||||
|
||||
def init_direct_imap(self, acc):
|
||||
""" idempotent function for initializing direct_imap."""
|
||||
def init_imap(self, acc):
|
||||
""" initialize direct_imap and cleanup server state. """
|
||||
from deltachat.direct_imap import DirectImap
|
||||
|
||||
assert acc.is_configured()
|
||||
if not hasattr(acc, "direct_imap"):
|
||||
acc.direct_imap = DirectImap(acc)
|
||||
addr = acc.get_config("addr")
|
||||
@@ -315,16 +369,20 @@ class ACFactory:
|
||||
self.tmpdir = tmpdir
|
||||
self.pytestconfig = request.config
|
||||
self.data = data
|
||||
self.testprocess = testprocess
|
||||
self._liveconfig_producer = testprocess.get_liveconfig_producer()
|
||||
|
||||
self._finalizers = []
|
||||
self._accounts = []
|
||||
self._acsetup = ACSetup(self.init_time)
|
||||
self._acsetup = ACSetup(testprocess, self.init_time)
|
||||
self._preconfigured_keys = ["alice", "bob", "charlie",
|
||||
"dom", "elena", "fiona"]
|
||||
self.set_logging_default(False)
|
||||
request.addfinalizer(self.finalize)
|
||||
|
||||
def log(self, *args):
|
||||
print("[acfactory]", "{:.3f}".format(time.time() - self.init_time), *args)
|
||||
|
||||
def finalize(self):
|
||||
while self._finalizers:
|
||||
fin = self._finalizers.pop()
|
||||
@@ -344,7 +402,7 @@ class ACFactory:
|
||||
""" Base function to get functional online configurations
|
||||
where we can make valid SMTP and IMAP connections with.
|
||||
"""
|
||||
configdict = next(self._liveconfig_producer)
|
||||
configdict = next(self._liveconfig_producer).copy()
|
||||
if "e2ee_enabled" not in configdict:
|
||||
configdict["e2ee_enabled"] = "1"
|
||||
|
||||
@@ -356,9 +414,19 @@ class ACFactory:
|
||||
assert "addr" in configdict and "mail_pw" in configdict
|
||||
return configdict
|
||||
|
||||
def _get_cached_account(self, addr):
|
||||
if addr in self.testprocess._addr2files:
|
||||
return self._getaccount(addr)
|
||||
|
||||
def get_unconfigured_account(self):
|
||||
return self._getaccount()
|
||||
|
||||
def _getaccount(self, try_cache_addr=None):
|
||||
logid = "ac{}".format(len(self._accounts) + 1)
|
||||
path = self.tmpdir.join(logid)
|
||||
# we need to use fixed database basename for maybe_cache_* functions to work
|
||||
path = self.tmpdir.mkdir(logid).join("dc.db")
|
||||
if try_cache_addr:
|
||||
self.testprocess.cache_maybe_retrieve_configured_db_files(try_cache_addr, path)
|
||||
ac = Account(path.strpath, logging=self._logging)
|
||||
ac._logid = logid # later instantiated FFIEventLogger needs this
|
||||
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
|
||||
@@ -391,7 +459,7 @@ class ACFactory:
|
||||
def get_pseudo_configured_account(self):
|
||||
# do a pseudo-configured account
|
||||
ac = self.get_unconfigured_account()
|
||||
acname = os.path.basename(ac.db_path)
|
||||
acname = ac._logid
|
||||
addr = "{}@offline.org".format(acname)
|
||||
ac.update_config(dict(
|
||||
addr=addr, displayname=acname, mail_pw="123",
|
||||
@@ -402,7 +470,7 @@ class ACFactory:
|
||||
self._acsetup.init_logging(ac)
|
||||
return ac
|
||||
|
||||
def new_online_configuring_account(self, cloned_from=None, **kwargs):
|
||||
def new_online_configuring_account(self, cloned_from=None, cache=False, **kwargs):
|
||||
if cloned_from is None:
|
||||
configdict = self.get_next_liveconfig()
|
||||
else:
|
||||
@@ -412,6 +480,12 @@ class ACFactory:
|
||||
mail_pw=cloned_from.get_config("mail_pw"),
|
||||
)
|
||||
configdict.update(kwargs)
|
||||
ac = self._get_cached_account(addr=configdict["addr"]) if cache else None
|
||||
if ac is not None:
|
||||
# make sure we consume a preconfig key, as if we had created a fresh account
|
||||
self._preconfigured_keys.pop(0)
|
||||
self._acsetup.add_configured(ac)
|
||||
return ac
|
||||
ac = self.prepare_account_from_liveconfig(configdict)
|
||||
self._acsetup.start_configure(ac)
|
||||
return ac
|
||||
@@ -436,9 +510,11 @@ class ACFactory:
|
||||
print("all accounts online")
|
||||
|
||||
def get_online_accounts(self, num):
|
||||
# to reduce number of log events logging starts after accounts can receive
|
||||
accounts = [self.new_online_configuring_account() for i in range(num)]
|
||||
accounts = [self.new_online_configuring_account(cache=True) for i in range(num)]
|
||||
self.bring_accounts_online()
|
||||
# we cache fully configured and started accounts
|
||||
for acc in accounts:
|
||||
self.testprocess.cache_maybe_store_configured_db_files(acc)
|
||||
return accounts
|
||||
|
||||
def run_bot_process(self, module, ffi=True):
|
||||
@@ -480,10 +556,6 @@ class ACFactory:
|
||||
ac.dump_account_info(logfile=logfile)
|
||||
imap = getattr(ac, "direct_imap", None)
|
||||
if imap is not None:
|
||||
try:
|
||||
imap.idle_done()
|
||||
except Exception:
|
||||
pass
|
||||
imap.dump_imap_structures(self.tmpdir, logfile=logfile)
|
||||
|
||||
def get_accepted_chat(self, ac1: Account, ac2: Account):
|
||||
@@ -501,6 +573,7 @@ class ACFactory:
|
||||
acc2.create_chat(acc).send_text("hi back")
|
||||
to_wait.append(acc)
|
||||
for acc in to_wait:
|
||||
acc.log("waiting for incoming message")
|
||||
acc._evtracker.wait_next_incoming_message()
|
||||
|
||||
|
||||
|
||||
@@ -8,11 +8,13 @@ to see timings of test setups.
|
||||
|
||||
import pytest
|
||||
|
||||
BENCH_NUM = 3
|
||||
|
||||
|
||||
class TestEmpty:
|
||||
def test_prepare_setup_measurings(self, acfactory):
|
||||
acfactory.get_online_accounts(5)
|
||||
acfactory.get_online_accounts(BENCH_NUM)
|
||||
|
||||
@pytest.mark.parametrize("num", range(0, 5))
|
||||
@pytest.mark.parametrize("num", range(0, BENCH_NUM + 1))
|
||||
def test_setup_online_accounts(self, acfactory, num):
|
||||
acfactory.get_online_accounts(num)
|
||||
|
||||
@@ -419,8 +419,8 @@ def test_send_and_receive_message_markseen(acfactory, lp):
|
||||
assert msg2.chat.id == msg4.chat.id
|
||||
assert ev.data1 == msg2.chat.id
|
||||
assert ev.data2 == 0
|
||||
idle2.wait_for_seen()
|
||||
|
||||
idle2.wait_for_new_message()
|
||||
lp.step("1")
|
||||
for i in range(2):
|
||||
ev = ac1._evtracker.get_matching("DC_EVENT_MSG_READ")
|
||||
@@ -689,7 +689,7 @@ def test_gossip_encryption_preference(acfactory, lp):
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
assert msg.text == "first message"
|
||||
assert not msg.is_encrypted()
|
||||
res = "{} End-to-end encryption preferred.".format(ac2.get_config('addr'))
|
||||
res = "End-to-end encryption preferred:\n{}\n".format(ac2.get_config('addr'))
|
||||
assert msg.chat.get_encryption_info() == res
|
||||
lp.sec("ac2 learns that ac3 prefers encryption")
|
||||
ac2.create_chat(ac3)
|
||||
@@ -701,7 +701,7 @@ def test_gossip_encryption_preference(acfactory, lp):
|
||||
lp.sec("ac3 does not know that ac1 prefers encryption")
|
||||
ac1.create_chat(ac3)
|
||||
chat = ac3.create_chat(ac1)
|
||||
res = "{} No encryption.".format(ac1.get_config('addr'))
|
||||
res = "No encryption:\n{}\n".format(ac1.get_config('addr'))
|
||||
assert chat.get_encryption_info() == res
|
||||
msg = chat.send_text("not encrypted")
|
||||
msg = ac1._evtracker.wait_next_incoming_message()
|
||||
@@ -712,7 +712,7 @@ def test_gossip_encryption_preference(acfactory, lp):
|
||||
group_chat = ac1.create_group_chat("hello")
|
||||
group_chat.add_contact(ac2)
|
||||
encryption_info = group_chat.get_encryption_info()
|
||||
res = "{} End-to-end encryption preferred.".format(ac2.get_config("addr"))
|
||||
res = "End-to-end encryption preferred:\n{}\n".format(ac2.get_config("addr"))
|
||||
assert encryption_info == res
|
||||
msg = group_chat.send_text("hi")
|
||||
|
||||
@@ -727,10 +727,9 @@ def test_gossip_encryption_preference(acfactory, lp):
|
||||
lp.sec("ac3 learns that ac1 prefers encryption")
|
||||
msg = ac3._evtracker.wait_next_incoming_message()
|
||||
encryption_info = msg.chat.get_encryption_info().splitlines()
|
||||
res = "{} End-to-end encryption preferred.".format(ac1.get_config("addr"))
|
||||
assert res in encryption_info
|
||||
res = "{} End-to-end encryption preferred.".format(ac2.get_config("addr"))
|
||||
assert res in encryption_info
|
||||
assert encryption_info[0] == "End-to-end encryption preferred:"
|
||||
assert ac1.get_config("addr") in encryption_info[1:]
|
||||
assert ac2.get_config("addr") in encryption_info[1:]
|
||||
msg = chat.send_text("encrypted")
|
||||
assert msg.is_encrypted()
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import os
|
||||
|
||||
from queue import Queue
|
||||
from deltachat import capi, cutil, const
|
||||
@@ -6,28 +7,44 @@ from deltachat import register_global_plugin
|
||||
from deltachat.hookspec import global_hookimpl
|
||||
from deltachat.capi import ffi
|
||||
from deltachat.capi import lib
|
||||
from deltachat.testplugin import ACSetup
|
||||
from deltachat.testplugin import ACSetup, create_dict_from_files_in_path, write_dict_to_dir
|
||||
# from deltachat.account import EventLogger
|
||||
|
||||
|
||||
class TestACSetup:
|
||||
def test_basic_states(self, acfactory, monkeypatch):
|
||||
pc = ACSetup(init_time=0.0)
|
||||
|
||||
def test_cache_writing(self, tmp_path):
|
||||
base = tmp_path.joinpath("hello")
|
||||
base.mkdir()
|
||||
d1 = base.joinpath("dir1")
|
||||
d1.mkdir()
|
||||
d1.joinpath("file1").write_bytes(b'content1')
|
||||
d2 = d1.joinpath("dir2")
|
||||
d2.mkdir()
|
||||
d2.joinpath("file2").write_bytes(b"123")
|
||||
d = create_dict_from_files_in_path(base)
|
||||
newbase = tmp_path.joinpath("other")
|
||||
write_dict_to_dir(d, newbase)
|
||||
assert newbase.joinpath("dir1", "dir2", "file2").exists()
|
||||
assert newbase.joinpath("dir1", "file1").exists()
|
||||
|
||||
def test_basic_states(self, acfactory, monkeypatch, testprocess):
|
||||
pc = ACSetup(init_time=0.0, testprocess=testprocess)
|
||||
acc = acfactory.get_unconfigured_account()
|
||||
monkeypatch.setattr(acc, "configure", lambda **kwargs: None)
|
||||
pc.start_configure(acc)
|
||||
assert pc._account2state[acc] == pc.CONFIGURING
|
||||
pc._configured_events.put((acc, True))
|
||||
monkeypatch.setattr(pc, "init_direct_imap", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(pc, "init_imap", lambda *args, **kwargs: None)
|
||||
pc.wait_one_configured(acc)
|
||||
assert pc._account2state[acc] == pc.CONFIGURED
|
||||
monkeypatch.setattr(pc, "_onconfigure_start_io", lambda *args, **kwargs: None)
|
||||
pc.bring_online()
|
||||
assert pc._account2state[acc] == pc.IDLEREADY
|
||||
|
||||
def test_two_accounts_one_waited_all_started(self, monkeypatch, acfactory):
|
||||
pc = ACSetup(init_time=0.0)
|
||||
monkeypatch.setattr(pc, "init_direct_imap", lambda *args, **kwargs: None)
|
||||
def test_two_accounts_one_waited_all_started(self, monkeypatch, acfactory, testprocess):
|
||||
pc = ACSetup(init_time=0.0, testprocess=testprocess)
|
||||
monkeypatch.setattr(pc, "init_imap", lambda *args, **kwargs: None)
|
||||
monkeypatch.setattr(pc, "_onconfigure_start_io", lambda *args, **kwargs: None)
|
||||
ac1 = acfactory.get_unconfigured_account()
|
||||
monkeypatch.setattr(ac1, "configure", lambda **kwargs: None)
|
||||
@@ -46,6 +63,29 @@ class TestACSetup:
|
||||
assert pc._account2state[ac1] == pc.IDLEREADY
|
||||
assert pc._account2state[ac2] == pc.IDLEREADY
|
||||
|
||||
def test_store_and_retrieve_configured_account_cache(self, acfactory, tmpdir):
|
||||
ac1 = acfactory.get_pseudo_configured_account()
|
||||
holder = acfactory._acsetup.testprocess
|
||||
assert holder.cache_maybe_store_configured_db_files(ac1)
|
||||
assert not holder.cache_maybe_store_configured_db_files(ac1)
|
||||
acdir = tmpdir.mkdir("newaccount")
|
||||
addr = ac1.get_config("addr")
|
||||
target_db_path = acdir.join("db").strpath
|
||||
assert holder.cache_maybe_retrieve_configured_db_files(addr, target_db_path)
|
||||
assert len(os.listdir(acdir)) >= 2
|
||||
|
||||
|
||||
def test_liveconfig_caching(acfactory, monkeypatch):
|
||||
prod = [
|
||||
{"addr": "1@example.org", "mail_pw": "123"},
|
||||
]
|
||||
acfactory._liveconfig_producer = iter(prod)
|
||||
d1 = acfactory.get_next_liveconfig()
|
||||
d1["hello"] = "world"
|
||||
acfactory._liveconfig_producer = iter(prod)
|
||||
d2 = acfactory.get_next_liveconfig()
|
||||
assert "hello" not in d2
|
||||
|
||||
|
||||
def test_empty_context():
|
||||
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL, capi.ffi.NULL)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import os
|
||||
import sys
|
||||
import json
|
||||
import re
|
||||
import pathlib
|
||||
import subprocess
|
||||
@@ -41,6 +41,24 @@ def replace_toml_version(relpath, newversion):
|
||||
os.rename(tmp_path, str(p))
|
||||
|
||||
|
||||
def read_json_version(relpath):
|
||||
p = pathlib.Path("package.json")
|
||||
assert p.exists()
|
||||
with open(p, "r") as f:
|
||||
json_data = json.loads(f.read())
|
||||
return json_data["version"]
|
||||
|
||||
|
||||
def update_package_json(newversion):
|
||||
p = pathlib.Path("package.json")
|
||||
assert p.exists()
|
||||
with open(p, "r") as f:
|
||||
json_data = json.loads(f.read())
|
||||
json_data["version"] = newversion
|
||||
with open(p, "w") as f:
|
||||
f.write(json.dumps(json_data, sort_keys=True, indent=2))
|
||||
|
||||
|
||||
def main():
|
||||
parser = ArgumentParser(prog="set_core_version")
|
||||
parser.add_argument("newversion")
|
||||
@@ -52,6 +70,7 @@ def main():
|
||||
print()
|
||||
for x in toml_list:
|
||||
print("{}: {}".format(x, read_toml_version(x)))
|
||||
print("package.json:", str(read_json_version("package.json")))
|
||||
print()
|
||||
raise SystemExit("need argument: new version, example: 1.25.0")
|
||||
|
||||
@@ -74,6 +93,7 @@ def main():
|
||||
|
||||
replace_toml_version("Cargo.toml", newversion)
|
||||
replace_toml_version("deltachat-ffi/Cargo.toml", newversion)
|
||||
update_package_json(newversion)
|
||||
|
||||
print("running cargo check")
|
||||
subprocess.call(["cargo", "check"])
|
||||
|
||||
149
src/chat.rs
149
src/chat.rs
@@ -47,10 +47,6 @@ pub enum ChatItem {
|
||||
msg_id: MsgId,
|
||||
},
|
||||
|
||||
/// A marker without inherent meaning. It is inserted before user
|
||||
/// supplied MsgId.
|
||||
Marker1,
|
||||
|
||||
/// Day marker, separating messages that correspond to different
|
||||
/// days according to local time.
|
||||
DayMarker {
|
||||
@@ -846,7 +842,9 @@ impl ChatId {
|
||||
///
|
||||
/// To get more verbose summary for a contact, including its key fingerprint, use [`Contact::get_encrinfo`].
|
||||
pub async fn get_encryption_info(self, context: &Context) -> Result<String> {
|
||||
let mut ret = String::new();
|
||||
let mut ret_mutual = String::new();
|
||||
let mut ret_nopreference = String::new();
|
||||
let mut ret_reset = String::new();
|
||||
|
||||
for contact_id in get_chat_contacts(context, self)
|
||||
.await?
|
||||
@@ -857,7 +855,7 @@ impl ChatId {
|
||||
let addr = contact.get_addr();
|
||||
let peerstate = Peerstate::from_addr(context, addr).await?;
|
||||
|
||||
let stock_message = match peerstate
|
||||
match peerstate
|
||||
.filter(|peerstate| {
|
||||
peerstate
|
||||
.peek_key(PeerstateVerifiedStatus::Unverified)
|
||||
@@ -865,15 +863,36 @@ impl ChatId {
|
||||
})
|
||||
.map(|peerstate| peerstate.prefer_encrypt)
|
||||
{
|
||||
Some(EncryptPreference::Mutual) => stock_str::e2e_preferred(context).await,
|
||||
Some(EncryptPreference::NoPreference) => stock_str::e2e_available(context).await,
|
||||
Some(EncryptPreference::Reset) => stock_str::encr_none(context).await,
|
||||
None => stock_str::encr_none(context).await,
|
||||
Some(EncryptPreference::Mutual) => ret_mutual += &format!("{}\n", addr),
|
||||
Some(EncryptPreference::NoPreference) => ret_nopreference += &format!("{}\n", addr),
|
||||
Some(EncryptPreference::Reset) | None => ret_reset += &format!("{}\n", addr),
|
||||
};
|
||||
}
|
||||
|
||||
let mut ret = String::new();
|
||||
if !ret_reset.is_empty() {
|
||||
ret += &stock_str::encr_none(context).await;
|
||||
ret.push(':');
|
||||
ret.push('\n');
|
||||
ret += &ret_reset;
|
||||
}
|
||||
if !ret_nopreference.is_empty() {
|
||||
if !ret.is_empty() {
|
||||
ret.push('\n')
|
||||
ret.push('\n');
|
||||
}
|
||||
ret += &format!("{} {}", addr, stock_message);
|
||||
ret += &stock_str::e2e_available(context).await;
|
||||
ret.push(':');
|
||||
ret.push('\n');
|
||||
ret += &ret_nopreference;
|
||||
}
|
||||
if !ret_mutual.is_empty() {
|
||||
if !ret.is_empty() {
|
||||
ret.push('\n');
|
||||
}
|
||||
ret += &stock_str::e2e_preferred(context).await;
|
||||
ret.push(':');
|
||||
ret.push('\n');
|
||||
ret += &ret_mutual;
|
||||
}
|
||||
|
||||
Ok(ret)
|
||||
@@ -2176,7 +2195,6 @@ pub async fn get_chat_msgs(
|
||||
context: &Context,
|
||||
chat_id: ChatId,
|
||||
flags: u32,
|
||||
marker1before: Option<MsgId>,
|
||||
) -> Result<Vec<ChatItem>> {
|
||||
let process_row = if (flags & DC_GCM_INFO_ONLY) != 0 {
|
||||
|row: &rusqlite::Row| {
|
||||
@@ -2226,12 +2244,8 @@ pub async fn get_chat_msgs(
|
||||
let mut ret = Vec::new();
|
||||
let mut last_day = 0;
|
||||
let cnv_to_local = dc_gm2local_offset();
|
||||
let marker1 = marker1before.unwrap_or_else(MsgId::new_unset);
|
||||
|
||||
for (ts, curr_id) in sorted_rows {
|
||||
if curr_id == marker1 {
|
||||
ret.push(ChatItem::Marker1);
|
||||
}
|
||||
if (flags & DC_GCM_ADDDAYMARKER) != 0 {
|
||||
let curr_local_timestamp = ts + cnv_to_local;
|
||||
let curr_day = curr_local_timestamp / 86400;
|
||||
@@ -4547,7 +4561,7 @@ mod tests {
|
||||
assert!(chat.is_protected());
|
||||
assert!(chat.is_unpromoted());
|
||||
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0, None).await.unwrap();
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0).await.unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
let msg = t.get_last_msg_in(chat_id).await;
|
||||
@@ -4577,7 +4591,7 @@ mod tests {
|
||||
assert!(!chat.is_protected());
|
||||
assert!(!chat.is_unpromoted());
|
||||
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0, None).await.unwrap();
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0).await.unwrap();
|
||||
assert_eq!(msgs.len(), 3);
|
||||
|
||||
// enable protection on promoted chat, the info-message is sent via send_msg() this time
|
||||
@@ -4674,10 +4688,7 @@ mod tests {
|
||||
add_contact_to_chat(&alice, alice_chat_id, contact_id).await?;
|
||||
assert_eq!(get_chat_contacts(&alice, alice_chat_id).await?.len(), 2);
|
||||
send_text_msg(&alice, alice_chat_id, "hi!".to_string()).await?;
|
||||
assert_eq!(
|
||||
get_chat_msgs(&alice, alice_chat_id, 0, None).await?.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(get_chat_msgs(&alice, alice_chat_id, 0).await?.len(), 1);
|
||||
|
||||
// Alice has an SMTP-server replacing the `Message-ID:`-header (as done eg. by outlook.com).
|
||||
let sent_msg = alice.pop_sent_msg().await;
|
||||
@@ -4710,10 +4721,7 @@ mod tests {
|
||||
let msg = alice.get_last_msg().await;
|
||||
assert_eq!(msg.chat_id, alice_chat_id);
|
||||
assert_eq!(msg.text, Some("ho!".to_string()));
|
||||
assert_eq!(
|
||||
get_chat_msgs(&alice, alice_chat_id, 0, None).await?.len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(get_chat_msgs(&alice, alice_chat_id, 0).await?.len(), 2);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -4741,7 +4749,7 @@ mod tests {
|
||||
assert_eq!(chat.id.get_fresh_msg_cnt(&t).await?, 1);
|
||||
assert_eq!(t.get_fresh_msgs().await?.len(), 1);
|
||||
|
||||
let msgs = get_chat_msgs(&t, chat.id, 0, None).await?;
|
||||
let msgs = get_chat_msgs(&t, chat.id, 0).await?;
|
||||
assert_eq!(msgs.len(), 1);
|
||||
let msg_id = match msgs.first().unwrap() {
|
||||
ChatItem::Message { msg_id } => *msg_id,
|
||||
@@ -4791,7 +4799,7 @@ mod tests {
|
||||
.is_contact_request());
|
||||
assert_eq!(chat_id.get_msg_cnt(&t).await?, 1);
|
||||
assert_eq!(chat_id.get_fresh_msg_cnt(&t).await?, 1);
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0, None).await?;
|
||||
let msgs = get_chat_msgs(&t, chat_id, 0).await?;
|
||||
assert_eq!(msgs.len(), 1);
|
||||
let msg_id = match msgs.first().unwrap() {
|
||||
ChatItem::Message { msg_id } => *msg_id,
|
||||
@@ -4879,7 +4887,7 @@ mod tests {
|
||||
let chat_id = msg.chat_id;
|
||||
assert_eq!(chat_id.get_fresh_msg_cnt(&alice).await?, 1);
|
||||
|
||||
let msgs = get_chat_msgs(&alice, chat_id, 0, None).await?;
|
||||
let msgs = get_chat_msgs(&alice, chat_id, 0).await?;
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
// Alice disables receiving classic emails.
|
||||
@@ -4891,12 +4899,28 @@ mod tests {
|
||||
// Already received classic email should still be in the chat.
|
||||
assert_eq!(chat_id.get_fresh_msg_cnt(&alice).await?, 1);
|
||||
|
||||
let msgs = get_chat_msgs(&alice, chat_id, 0, None).await?;
|
||||
let msgs = get_chat_msgs(&alice, chat_id, 0).await?;
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_chat_get_color() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "a chat").await?;
|
||||
let color1 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?;
|
||||
assert_eq!(color1, 0x008772);
|
||||
|
||||
// upper-/lowercase makes a difference for the colors, these are different groups
|
||||
// (in contrast to email addresses, where upper-/lowercase is ignored in practise)
|
||||
let t = TestContext::new().await;
|
||||
let chat_id = create_group_chat(&t, ProtectionStatus::Unprotected, "A CHAT").await?;
|
||||
let color2 = Chat::load_from_db(&t, chat_id).await?.get_color(&t).await?;
|
||||
assert_ne!(color2, color1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn test_sticker(filename: &str, bytes: &[u8], w: i32, h: i32) -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
@@ -5180,13 +5204,13 @@ mod tests {
|
||||
let msg = bob.get_last_msg().await;
|
||||
assert_eq!(msg.get_text().unwrap(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 2);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0, None).await?.len(), 1);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0).await?.len(), 1);
|
||||
bob.recv_msg(&sent2).await;
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0, None).await?.len(), 2);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0).await?.len(), 2);
|
||||
bob.recv_msg(&sent3).await;
|
||||
assert_eq!(get_chat_contacts(&bob, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0, None).await?.len(), 2);
|
||||
assert_eq!(get_chat_msgs(&bob, msg.chat_id, 0).await?.len(), 2);
|
||||
|
||||
// Claire does not receive the first message, however, due to resending, she has a similar view as Alice and Bob
|
||||
let claire = TestContext::new().await;
|
||||
@@ -5196,7 +5220,7 @@ mod tests {
|
||||
let msg = claire.get_last_msg().await;
|
||||
assert_eq!(msg.get_text().unwrap(), "alice->bob");
|
||||
assert_eq!(get_chat_contacts(&claire, msg.chat_id).await?.len(), 3);
|
||||
assert_eq!(get_chat_msgs(&claire, msg.chat_id, 0, None).await?.len(), 2);
|
||||
assert_eq!(get_chat_msgs(&claire, msg.chat_id, 0).await?.len(), 2);
|
||||
let msg_from = Contact::get_by_id(&claire, msg.get_from_id()).await?;
|
||||
assert_eq!(msg_from.get_addr(), "alice@example.org");
|
||||
|
||||
@@ -5389,4 +5413,59 @@ mod tests {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_chat_get_encryption_info() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
let contact_bob = Contact::create(&alice, "Bob", "bob@example.net").await?;
|
||||
let contact_fiona = Contact::create(&alice, "", "fiona@example.net").await?;
|
||||
|
||||
let chat_id = create_group_chat(&alice, ProtectionStatus::Unprotected, "Group").await?;
|
||||
assert_eq!(chat_id.get_encryption_info(&alice).await?, "");
|
||||
|
||||
add_contact_to_chat(&alice, chat_id, contact_bob).await?;
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(&alice).await?,
|
||||
"No encryption:\n\
|
||||
bob@example.net\n"
|
||||
);
|
||||
|
||||
add_contact_to_chat(&alice, chat_id, contact_fiona).await?;
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(&alice).await?,
|
||||
"No encryption:\n\
|
||||
bob@example.net\n\
|
||||
fiona@example.net\n"
|
||||
);
|
||||
|
||||
let direct_chat = bob.create_chat(&alice).await;
|
||||
send_text_msg(&bob, direct_chat.id, "Hello!".to_string()).await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(&alice).await?,
|
||||
"No encryption:\n\
|
||||
fiona@example.net\n\
|
||||
\n\
|
||||
End-to-end encryption preferred:\n\
|
||||
bob@example.net\n"
|
||||
);
|
||||
|
||||
bob.set_config(Config::E2eeEnabled, Some("0")).await?;
|
||||
send_text_msg(&bob, direct_chat.id, "Hello!".to_string()).await?;
|
||||
alice.recv_msg(&bob.pop_sent_msg().await).await;
|
||||
|
||||
assert_eq!(
|
||||
chat_id.get_encryption_info(&alice).await?,
|
||||
"No encryption:\n\
|
||||
fiona@example.net\n\
|
||||
\n\
|
||||
End-to-end encryption available:\n\
|
||||
bob@example.net\n"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,7 +60,7 @@ impl Context {
|
||||
use futures::future::FutureExt;
|
||||
|
||||
ensure!(
|
||||
!self.scheduler.read().await.is_running(),
|
||||
self.scheduler.read().await.is_none(),
|
||||
"cannot configure, already running"
|
||||
);
|
||||
ensure!(
|
||||
|
||||
@@ -162,7 +162,6 @@ impl Default for Chattype {
|
||||
}
|
||||
}
|
||||
|
||||
pub const DC_MSG_ID_MARKER1: u32 = 1;
|
||||
pub const DC_MSG_ID_DAYMARKER: u32 = 9;
|
||||
pub const DC_MSG_ID_LAST_SPECIAL: u32 = 9;
|
||||
|
||||
|
||||
@@ -686,7 +686,7 @@ impl Contact {
|
||||
pub async fn get_all(
|
||||
context: &Context,
|
||||
listflags: u32,
|
||||
query: Option<impl AsRef<str>>,
|
||||
query: Option<&str>,
|
||||
) -> Result<Vec<ContactId>> {
|
||||
let self_addrs = context.get_all_self_addrs().await?;
|
||||
let mut add_self = false;
|
||||
@@ -695,7 +695,7 @@ impl Contact {
|
||||
let flag_add_self = (listflags & DC_GCL_ADD_SELF) != 0;
|
||||
|
||||
if flag_verified_only || query.is_some() {
|
||||
let s3str_like_cmd = format!("%{}%", query.as_ref().map(|s| s.as_ref()).unwrap_or(""));
|
||||
let s3str_like_cmd = format!("%{}%", query.unwrap_or(""));
|
||||
context
|
||||
.sql
|
||||
.query_map(
|
||||
@@ -739,9 +739,9 @@ impl Contact {
|
||||
.unwrap_or_default();
|
||||
let self_name2 = stock_str::self_msg(context);
|
||||
|
||||
if self_addr.contains(query.as_ref())
|
||||
|| self_name.contains(query.as_ref())
|
||||
|| self_name2.await.contains(query.as_ref())
|
||||
if self_addr.contains(query)
|
||||
|| self_name.contains(query)
|
||||
|| self_name2.await.contains(query)
|
||||
{
|
||||
add_self = true;
|
||||
}
|
||||
@@ -889,7 +889,7 @@ impl Contact {
|
||||
};
|
||||
|
||||
ret += &format!(
|
||||
"{}\n{}:",
|
||||
"{}.\n{}:",
|
||||
stock_message,
|
||||
stock_str::finger_prints(context).await
|
||||
);
|
||||
@@ -1082,7 +1082,7 @@ impl Contact {
|
||||
/// and can be used for an fallback avatar with white initials
|
||||
/// as well as for headlines in bubbles of group chats.
|
||||
pub fn get_color(&self) -> u32 {
|
||||
str_to_color(&self.addr)
|
||||
str_to_color(&self.addr.to_lowercase())
|
||||
}
|
||||
|
||||
/// Gets the contact's status.
|
||||
@@ -1396,27 +1396,23 @@ pub fn normalize_name(full_name: &str) -> String {
|
||||
fn cat_fingerprint(
|
||||
ret: &mut String,
|
||||
addr: &str,
|
||||
fingerprint_verified: impl AsRef<str>,
|
||||
fingerprint_unverified: impl AsRef<str>,
|
||||
fingerprint_verified: &str,
|
||||
fingerprint_unverified: &str,
|
||||
) {
|
||||
*ret += &format!(
|
||||
"\n\n{}:\n{}",
|
||||
addr,
|
||||
if !fingerprint_verified.as_ref().is_empty() {
|
||||
fingerprint_verified.as_ref()
|
||||
if !fingerprint_verified.is_empty() {
|
||||
fingerprint_verified
|
||||
} else {
|
||||
fingerprint_unverified.as_ref()
|
||||
fingerprint_unverified
|
||||
},
|
||||
);
|
||||
if !fingerprint_verified.as_ref().is_empty()
|
||||
&& !fingerprint_unverified.as_ref().is_empty()
|
||||
&& fingerprint_verified.as_ref() != fingerprint_unverified.as_ref()
|
||||
if !fingerprint_verified.is_empty()
|
||||
&& !fingerprint_unverified.is_empty()
|
||||
&& fingerprint_verified != fingerprint_unverified
|
||||
{
|
||||
*ret += &format!(
|
||||
"\n\n{} (alternative):\n{}",
|
||||
addr,
|
||||
fingerprint_unverified.as_ref()
|
||||
);
|
||||
*ret += &format!("\n\n{} (alternative):\n{}", addr, fingerprint_unverified);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1950,6 +1946,25 @@ mod tests {
|
||||
assert_eq!(id, Some(ContactId::SELF));
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_contact_get_color() -> Result<()> {
|
||||
let t = TestContext::new().await;
|
||||
let contact_id = Contact::create(&t, "name", "name@example.net").await?;
|
||||
let color1 = Contact::get_by_id(&t, contact_id).await?.get_color();
|
||||
assert_eq!(color1, 0xA739FF);
|
||||
|
||||
let t = TestContext::new().await;
|
||||
let contact_id = Contact::create(&t, "prename name", "name@example.net").await?;
|
||||
let color2 = Contact::get_by_id(&t, contact_id).await?.get_color();
|
||||
assert_eq!(color2, color1);
|
||||
|
||||
let t = TestContext::new().await;
|
||||
let contact_id = Contact::create(&t, "Name", "nAme@exAmple.NET").await?;
|
||||
let color3 = Contact::get_by_id(&t, contact_id).await?.get_color();
|
||||
assert_eq!(color3, color1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_contact_get_encrinfo() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
@@ -1965,7 +1980,7 @@ mod tests {
|
||||
.await?;
|
||||
|
||||
let encrinfo = Contact::get_encrinfo(&alice, contact_bob_id).await?;
|
||||
assert_eq!(encrinfo, "No encryption.");
|
||||
assert_eq!(encrinfo, "No encryption");
|
||||
|
||||
let bob = TestContext::new_bob().await;
|
||||
let chat_alice = bob
|
||||
|
||||
@@ -54,7 +54,7 @@ pub struct InnerContext {
|
||||
pub(crate) translated_stockstrings: RwLock<HashMap<usize, String>>,
|
||||
pub(crate) events: Events,
|
||||
|
||||
pub(crate) scheduler: RwLock<Scheduler>,
|
||||
pub(crate) scheduler: RwLock<Option<Scheduler>>,
|
||||
|
||||
/// Recently loaded quota information, if any.
|
||||
/// Set to `None` if quota was never tried to load.
|
||||
@@ -173,7 +173,7 @@ impl Context {
|
||||
wrong_pw_warning_mutex: Mutex::new(()),
|
||||
translated_stockstrings: RwLock::new(HashMap::new()),
|
||||
events: Events::default(),
|
||||
scheduler: RwLock::new(Scheduler::Stopped),
|
||||
scheduler: RwLock::new(None),
|
||||
quota: RwLock::new(None),
|
||||
creation_time: std::time::SystemTime::now(),
|
||||
last_full_folder_scan: Mutex::new(None),
|
||||
@@ -195,8 +195,12 @@ impl Context {
|
||||
}
|
||||
|
||||
info!(self, "starting IO");
|
||||
if let Err(err) = self.inner.scheduler.write().await.start(self.clone()).await {
|
||||
error!(self, "Failed to start IO: {}", err)
|
||||
let mut lock = self.inner.scheduler.write().await;
|
||||
if lock.is_none() {
|
||||
match Scheduler::start(self.clone()).await {
|
||||
Err(err) => error!(self, "Failed to start IO: {}", err),
|
||||
Ok(scheduler) => *lock = Some(scheduler),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,8 +213,8 @@ impl Context {
|
||||
// which will emit the below event(s)
|
||||
info!(self, "stopping IO");
|
||||
|
||||
if let Err(err) = self.inner.stop_io().await {
|
||||
warn!(self, "failed to stop IO: {}", err);
|
||||
if let Some(scheduler) = self.inner.scheduler.write().await.take() {
|
||||
scheduler.stop(self).await;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,13 +278,11 @@ impl Context {
|
||||
// Ongoing process allocation/free/check
|
||||
|
||||
pub(crate) async fn alloc_ongoing(&self) -> Result<Receiver<()>> {
|
||||
if self.has_ongoing().await {
|
||||
let mut s = self.running_state.write().await;
|
||||
if s.ongoing_running || !s.shall_stop_ongoing {
|
||||
bail!("There is already another ongoing process running.");
|
||||
}
|
||||
|
||||
let s_a = &self.running_state;
|
||||
let mut s = s_a.write().await;
|
||||
|
||||
s.ongoing_running = true;
|
||||
s.shall_stop_ongoing = false;
|
||||
let (sender, receiver) = channel::bounded(1);
|
||||
@@ -290,25 +292,16 @@ impl Context {
|
||||
}
|
||||
|
||||
pub(crate) async fn free_ongoing(&self) {
|
||||
let s_a = &self.running_state;
|
||||
let mut s = s_a.write().await;
|
||||
let mut s = self.running_state.write().await;
|
||||
|
||||
s.ongoing_running = false;
|
||||
s.shall_stop_ongoing = true;
|
||||
s.cancel_sender.take();
|
||||
}
|
||||
|
||||
pub(crate) async fn has_ongoing(&self) -> bool {
|
||||
let s_a = &self.running_state;
|
||||
let s = s_a.read().await;
|
||||
|
||||
s.ongoing_running || !s.shall_stop_ongoing
|
||||
}
|
||||
|
||||
/// Signal an ongoing process to stop.
|
||||
pub async fn stop_ongoing(&self) {
|
||||
let s_a = &self.running_state;
|
||||
let mut s = s_a.write().await;
|
||||
let mut s = self.running_state.write().await;
|
||||
if let Some(cancel) = s.cancel_sender.take() {
|
||||
if let Err(err) = cancel.send(()).await {
|
||||
warn!(self, "could not cancel ongoing: {:?}", err);
|
||||
@@ -320,7 +313,7 @@ impl Context {
|
||||
s.shall_stop_ongoing = true;
|
||||
} else {
|
||||
info!(self, "No ongoing process to stop.",);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn shall_stop_ongoing(&self) -> bool {
|
||||
@@ -642,13 +635,6 @@ impl Context {
|
||||
}
|
||||
}
|
||||
|
||||
impl InnerContext {
|
||||
async fn stop_io(&self) -> Result<()> {
|
||||
self.scheduler.write().await.stop().await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for RunningState {
|
||||
fn default() -> Self {
|
||||
RunningState {
|
||||
@@ -729,23 +715,20 @@ mod tests {
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 0);
|
||||
|
||||
receive_msg(&t, &bob).await;
|
||||
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.unwrap().len(), 1);
|
||||
assert_eq!(get_chat_msgs(&t, bob.id, 0).await.unwrap().len(), 1);
|
||||
assert_eq!(bob.id.get_fresh_msg_cnt(&t).await.unwrap(), 1);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 1);
|
||||
|
||||
receive_msg(&t, &claire).await;
|
||||
receive_msg(&t, &claire).await;
|
||||
assert_eq!(
|
||||
get_chat_msgs(&t, claire.id, 0, None).await.unwrap().len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(get_chat_msgs(&t, claire.id, 0).await.unwrap().len(), 2);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 2);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 3);
|
||||
|
||||
receive_msg(&t, &dave).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
assert_eq!(get_chat_msgs(&t, dave.id, 0, None).await.unwrap().len(), 3);
|
||||
assert_eq!(get_chat_msgs(&t, dave.id, 0).await.unwrap().len(), 3);
|
||||
assert_eq!(dave.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6);
|
||||
|
||||
@@ -760,10 +743,7 @@ mod tests {
|
||||
receive_msg(&t, &bob).await;
|
||||
receive_msg(&t, &claire).await;
|
||||
receive_msg(&t, &dave).await;
|
||||
assert_eq!(
|
||||
get_chat_msgs(&t, claire.id, 0, None).await.unwrap().len(),
|
||||
3
|
||||
);
|
||||
assert_eq!(get_chat_msgs(&t, claire.id, 0).await.unwrap().len(), 3);
|
||||
assert_eq!(claire.id.get_fresh_msg_cnt(&t).await.unwrap(), 3);
|
||||
assert_eq!(t.get_fresh_msgs().await.unwrap().len(), 6); // muted claire is not counted
|
||||
|
||||
@@ -780,7 +760,7 @@ mod tests {
|
||||
let t = TestContext::new_alice().await;
|
||||
let bob = t.create_chat_with_contact("", "bob@g.it").await;
|
||||
receive_msg(&t, &bob).await;
|
||||
assert_eq!(get_chat_msgs(&t, bob.id, 0, None).await.unwrap().len(), 1);
|
||||
assert_eq!(get_chat_msgs(&t, bob.id, 0).await.unwrap().len(), 1);
|
||||
|
||||
// chat is unmuted by default, here and in the following assert(),
|
||||
// we check mainly that the SQL-statements in is_muted() and get_fresh_msgs()
|
||||
|
||||
@@ -2350,26 +2350,14 @@ mod tests {
|
||||
assert_eq!(chat.typ, Chattype::Single);
|
||||
assert_eq!(chat.name, "Bob");
|
||||
assert_eq!(chat::get_chat_contacts(&t, chat_id).await.unwrap().len(), 1);
|
||||
assert_eq!(
|
||||
chat::get_chat_msgs(&t, chat_id, 0, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(chat::get_chat_msgs(&t, chat_id, 0).await.unwrap().len(), 1);
|
||||
|
||||
// receive a non-delta-message from Bob, shows up because of the show_emails setting
|
||||
dc_receive_imf(&t, ONETOONE_NOREPLY_MAIL, false)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
chat::get_chat_msgs(&t, chat_id, 0, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(chat::get_chat_msgs(&t, chat_id, 0).await.unwrap().len(), 2);
|
||||
|
||||
// let Bob create an adhoc-group by a non-delta-message, shows up because of the show_emails setting
|
||||
dc_receive_imf(&t, GRP_MAIL, false).await.unwrap();
|
||||
@@ -2418,13 +2406,7 @@ mod tests {
|
||||
// create a group with bob, archive group
|
||||
let group_id = chat::create_group_chat(&t, ProtectionStatus::Unprotected, "foo").await?;
|
||||
chat::add_contact_to_chat(&t, group_id, bob_id).await?;
|
||||
assert_eq!(
|
||||
chat::get_chat_msgs(&t, group_id, 0, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
0
|
||||
);
|
||||
assert_eq!(chat::get_chat_msgs(&t, group_id, 0).await.unwrap().len(), 0);
|
||||
group_id
|
||||
.set_visibility(&t, ChatVisibility::Archived)
|
||||
.await?;
|
||||
@@ -2505,7 +2487,7 @@ mod tests {
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
assert_eq!(chat::get_chat_msgs(&t, group_id, 0, None).await?.len(), 1);
|
||||
assert_eq!(chat::get_chat_msgs(&t, group_id, 0).await?.len(), 1);
|
||||
let msg = message::Message::load_from_db(&t, msg.id).await?;
|
||||
assert_eq!(msg.state, MessageState::OutMdnRcvd);
|
||||
|
||||
@@ -2834,7 +2816,7 @@ mod tests {
|
||||
|
||||
assert_eq!(msg.state, MessageState::OutFailed);
|
||||
|
||||
let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0, None).await?;
|
||||
let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0).await?;
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
|
||||
msg_id
|
||||
} else {
|
||||
@@ -2920,7 +2902,7 @@ mod tests {
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let contacts = Contact::get_all(&t.ctx, 0, None as Option<String>).await?;
|
||||
let contacts = Contact::get_all(&t.ctx, 0, None).await?;
|
||||
assert_eq!(contacts.len(), 0); // mailing list recipients and senders do not count as "known contacts"
|
||||
|
||||
let msg1 = get_chat_msg(&t, chat_id, 0, 2).await;
|
||||
@@ -3087,7 +3069,7 @@ Hello mailinglist!\r\n"
|
||||
assert_eq!(chats.len(), 0); // Test that the message is not shown
|
||||
|
||||
// Both messages are in the same blocked chat.
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap();
|
||||
assert_eq!(msgs.len(), 2);
|
||||
}
|
||||
|
||||
@@ -3120,7 +3102,7 @@ Hello mailinglist!\r\n"
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = t.get_last_msg().await;
|
||||
let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0, None).await.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0).await.unwrap();
|
||||
assert_eq!(msgs.len(), 2);
|
||||
}
|
||||
|
||||
@@ -3142,7 +3124,7 @@ Hello mailinglist!\r\n"
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1); // Test that chat is still in the chatlist
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap();
|
||||
assert_eq!(msgs.len(), 1); // ...and contains 1 message
|
||||
|
||||
dc_receive_imf(&t.ctx, DC_MAILINGLIST2, false)
|
||||
@@ -3151,7 +3133,7 @@ Hello mailinglist!\r\n"
|
||||
|
||||
let chats = Chatlist::try_load(&t.ctx, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1); // Test that the new mailing list message got into the same chat
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap();
|
||||
assert_eq!(msgs.len(), 2);
|
||||
let chat = Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
|
||||
assert!(chat.is_contact_request());
|
||||
@@ -3179,7 +3161,7 @@ Hello mailinglist!\r\n"
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap();
|
||||
assert_eq!(msgs.len(), 2);
|
||||
let chat = chat::Chat::load_from_db(&t.ctx, chat_id).await.unwrap();
|
||||
assert!(chat.can_send(&t.ctx).await.unwrap());
|
||||
@@ -3212,13 +3194,7 @@ Hello mailinglist!\r\n"
|
||||
assert_eq!(chat.typ, Chattype::Mailinglist);
|
||||
assert_eq!(chat.grpid, "mylist@bar.org");
|
||||
assert_eq!(chat.name, "ola");
|
||||
assert_eq!(
|
||||
chat::get_chat_msgs(&t, chat.id, 0, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(chat::get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 1);
|
||||
|
||||
// receive another message with no sender name but the same address,
|
||||
// make sure this lands in the same chat
|
||||
@@ -3237,13 +3213,7 @@ Hello mailinglist!\r\n"
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
chat::get_chat_msgs(&t, chat.id, 0, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
2
|
||||
);
|
||||
assert_eq!(chat::get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 2);
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
@@ -3422,10 +3392,7 @@ Hello mailinglist!\r\n"
|
||||
);
|
||||
assert!(msg.has_html());
|
||||
let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap();
|
||||
assert_eq!(
|
||||
get_chat_msgs(&t, msg.chat_id, 0, None).await.unwrap().len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(get_chat_msgs(&t, msg.chat_id, 0).await.unwrap().len(), 1);
|
||||
assert_eq!(chat.typ, Chattype::Mailinglist);
|
||||
assert_eq!(chat.blocked, Blocked::Request);
|
||||
assert_eq!(chat.grpid, "intern.lists.abc.de");
|
||||
@@ -3445,10 +3412,7 @@ Hello mailinglist!\r\n"
|
||||
.await
|
||||
.unwrap();
|
||||
let msg = t.get_last_msg().await;
|
||||
assert_eq!(
|
||||
get_chat_msgs(&t, msg.chat_id, 0, None).await.unwrap().len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(get_chat_msgs(&t, msg.chat_id, 0).await.unwrap().len(), 1);
|
||||
let text = msg.text.clone().unwrap();
|
||||
assert!(text.contains("content text"));
|
||||
assert!(!text.contains("footer text"));
|
||||
@@ -3604,7 +3568,7 @@ YEAAAAAA!.
|
||||
assert_eq!(msg.viewtype, Viewtype::Image);
|
||||
assert!(msg.has_html());
|
||||
let chat = Chat::load_from_db(&t, msg.chat_id).await.unwrap();
|
||||
assert_eq!(get_chat_msgs(&t, chat.id, 0, None).await.unwrap().len(), 1);
|
||||
assert_eq!(get_chat_msgs(&t, chat.id, 0).await.unwrap().len(), 1);
|
||||
}
|
||||
|
||||
/// Test that classical MUA messages are assigned to group chats based on the `In-Reply-To`
|
||||
@@ -3654,7 +3618,7 @@ YEAAAAAA!.
|
||||
assert_eq!(msg.get_text().unwrap(), "reply foo");
|
||||
|
||||
// Load the first message from the same chat.
|
||||
let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0, None).await.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t, msg.chat_id, 0).await.unwrap();
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs.first().unwrap() {
|
||||
msg_id
|
||||
} else {
|
||||
@@ -3887,10 +3851,7 @@ YEAAAAAA!.
|
||||
assert!(msg.get_text().unwrap().contains("hi support!"));
|
||||
let chat = Chat::load_from_db(&alice, msg.chat_id).await.unwrap();
|
||||
assert_eq!(chat.typ, Chattype::Group);
|
||||
assert_eq!(
|
||||
get_chat_msgs(&alice, chat.id, 0, None).await.unwrap().len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(get_chat_msgs(&alice, chat.id, 0).await.unwrap().len(), 1);
|
||||
if group_request {
|
||||
assert_eq!(get_chat_contacts(&alice, chat.id).await.unwrap().len(), 4);
|
||||
} else {
|
||||
@@ -3923,13 +3884,7 @@ YEAAAAAA!.
|
||||
} else {
|
||||
assert_eq!(chat.typ, Chattype::Single);
|
||||
}
|
||||
assert_eq!(
|
||||
get_chat_msgs(&claire, chat.id, 0, None)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
1
|
||||
);
|
||||
assert_eq!(get_chat_msgs(&claire, chat.id, 0).await.unwrap().len(), 1);
|
||||
assert_eq!(msg.get_override_sender_name(), None);
|
||||
|
||||
(claire, alice)
|
||||
@@ -4044,7 +3999,7 @@ YEAAAAAA!.
|
||||
println!("\n========= Delete the message ==========");
|
||||
msg.id.trash(&t).await.unwrap();
|
||||
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap();
|
||||
assert_eq!(msgs.len(), 0);
|
||||
|
||||
println!("\n========= Receive a message that is a reply to the deleted message ==========");
|
||||
|
||||
@@ -1042,9 +1042,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let device_chat_id = chats.get_chat_id(0).unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0).await.unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
// the message should be added only once a day - test that an hour later and nearly a day later
|
||||
@@ -1054,9 +1052,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
|
||||
get_provider_update_timestamp(),
|
||||
)
|
||||
.await;
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0).await.unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
maybe_warn_on_bad_time(
|
||||
@@ -1065,9 +1061,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
|
||||
get_provider_update_timestamp(),
|
||||
)
|
||||
.await;
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0).await.unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
// next day, there should be another device message
|
||||
@@ -1080,9 +1074,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
assert_eq!(device_chat_id, chats.get_chat_id(0).unwrap());
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0).await.unwrap();
|
||||
assert_eq!(msgs.len(), 2);
|
||||
}
|
||||
|
||||
@@ -1112,9 +1104,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let device_chat_id = chats.get_chat_id(0).unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0).await.unwrap();
|
||||
assert_eq!(msgs.len(), 1);
|
||||
|
||||
// do not repeat the warning every day ...
|
||||
@@ -1134,9 +1124,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let device_chat_id = chats.get_chat_id(0).unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0).await.unwrap();
|
||||
let test_len = msgs.len();
|
||||
assert!(test_len == 1 || test_len == 2);
|
||||
|
||||
@@ -1151,9 +1139,7 @@ Hop: From: hq5.example.org; By: hq5.example.org; Date: Mon, 27 Dec 2021 11:21:22
|
||||
let chats = Chatlist::try_load(&t, 0, None, None).await.unwrap();
|
||||
assert_eq!(chats.len(), 1);
|
||||
let device_chat_id = chats.get_chat_id(0).unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t, device_chat_id, 0).await.unwrap();
|
||||
assert_eq!(msgs.len(), test_len + 1);
|
||||
}
|
||||
}
|
||||
|
||||
49
src/e2ee.rs
49
src/e2ee.rs
@@ -266,13 +266,40 @@ fn get_mixed_up_mime<'a, 'b>(mail: &'a ParsedMail<'b>) -> Option<&'a ParsedMail<
|
||||
}
|
||||
}
|
||||
|
||||
/// 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
|
||||
}
|
||||
}
|
||||
|
||||
async fn decrypt_if_autocrypt_message(
|
||||
context: &Context,
|
||||
mail: &ParsedMail<'_>,
|
||||
private_keyring: Keyring<SignedSecretKey>,
|
||||
public_keyring_for_validate: Keyring<SignedPublicKey>,
|
||||
) -> Result<Option<(Vec<u8>, HashSet<Fingerprint>)>> {
|
||||
let encrypted_data_part = match get_autocrypt_mime(mail).or_else(|| get_mixed_up_mime(mail)) {
|
||||
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);
|
||||
@@ -389,6 +416,7 @@ pub async fn ensure_secret_key_exists(context: &Context) -> Result<String> {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::chat;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::message::{Message, Viewtype};
|
||||
use crate::param::Param;
|
||||
use crate::peerstate::ToSave;
|
||||
@@ -592,8 +620,8 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
assert!(!encrypt_helper.should_encrypt(&t, false, &ps).unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_mixed_up_mime() -> Result<()> {
|
||||
#[async_std::test]
|
||||
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.
|
||||
@@ -601,6 +629,7 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
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.
|
||||
//
|
||||
@@ -611,6 +640,20 @@ Sent with my Delta Chat Messenger: https://delta.chat";
|
||||
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;
|
||||
dc_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(())
|
||||
}
|
||||
|
||||
@@ -966,7 +966,7 @@ mod tests {
|
||||
}
|
||||
|
||||
async fn check_msg_is_deleted(t: &TestContext, chat: &Chat, msg_id: MsgId) {
|
||||
let chat_items = chat::get_chat_msgs(t, chat.id, 0, None).await.unwrap();
|
||||
let chat_items = chat::get_chat_msgs(t, chat.id, 0).await.unwrap();
|
||||
// Check that the chat is empty except for possibly info messages:
|
||||
for item in &chat_items {
|
||||
if let ChatItem::Message { msg_id } = item {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use super::Imap;
|
||||
|
||||
use crate::context::Context;
|
||||
use anyhow::Context as _;
|
||||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
@@ -15,8 +16,8 @@ pub enum Error {
|
||||
#[error("IMAP Folder name invalid: {0}")]
|
||||
BadFolderName(String),
|
||||
|
||||
#[error("IMAP folder does not exist: {0}")]
|
||||
NoFolder(String),
|
||||
#[error("Got a NO response when trying to select {0}, usually this means that it doesn't exist: {1}")]
|
||||
NoFolder(String, String),
|
||||
|
||||
#[error("IMAP close/expunge failed")]
|
||||
CloseExpungeFailed(#[from] async_imap::error::Error),
|
||||
@@ -117,8 +118,8 @@ impl Imap {
|
||||
Err(async_imap::error::Error::Validate(_)) => {
|
||||
Err(Error::BadFolderName(folder.to_string()))
|
||||
}
|
||||
Err(async_imap::error::Error::No(_)) => {
|
||||
Err(Error::NoFolder(folder.to_string()))
|
||||
Err(async_imap::error::Error::No(response)) => {
|
||||
Err(Error::NoFolder(folder.to_string(), response))
|
||||
}
|
||||
Err(err) => {
|
||||
self.config.selected_folder = None;
|
||||
@@ -139,19 +140,19 @@ impl Imap {
|
||||
&mut self,
|
||||
context: &Context,
|
||||
folder: &str,
|
||||
) -> Result<NewlySelected> {
|
||||
) -> anyhow::Result<NewlySelected> {
|
||||
match self.select_folder(context, Some(folder)).await {
|
||||
Ok(newly_selected) => Ok(newly_selected),
|
||||
Err(err) => match err {
|
||||
Error::NoFolder(_) => {
|
||||
if let Some(ref mut session) = self.session {
|
||||
session.create(folder).await?;
|
||||
} else {
|
||||
return Err(Error::NoSession);
|
||||
}
|
||||
self.select_folder(context, Some(folder)).await
|
||||
Error::NoFolder(..) => {
|
||||
let session = self.session.as_mut().context("no IMAP session")?;
|
||||
session.create(folder).await.with_context(|| {
|
||||
format!("Couldn't select folder ('{}'), then create() failed", err)
|
||||
})?;
|
||||
|
||||
Ok(self.select_folder(context, Some(folder)).await?)
|
||||
}
|
||||
_ => Err(err),
|
||||
_ => Err(err.into()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -445,8 +445,8 @@ async fn import_backup(
|
||||
"Cannot import backups to accounts in use."
|
||||
);
|
||||
ensure!(
|
||||
!context.scheduler.read().await.is_running(),
|
||||
"cannot import backup, IO already running"
|
||||
context.scheduler.read().await.is_none(),
|
||||
"cannot import backup, IO is running"
|
||||
);
|
||||
|
||||
let backup_file = File::open(backup_to_import).await?;
|
||||
@@ -563,8 +563,8 @@ async fn export_backup(context: &Context, dir: &Path, passphrase: String) -> Res
|
||||
.ok();
|
||||
|
||||
ensure!(
|
||||
!context.scheduler.read().await.is_running(),
|
||||
"cannot export backup, IO already running"
|
||||
context.scheduler.read().await.is_none(),
|
||||
"cannot export backup, IO is running"
|
||||
);
|
||||
|
||||
info!(
|
||||
|
||||
283
src/job.rs
283
src/job.rs
@@ -4,36 +4,22 @@
|
||||
//! and job types.
|
||||
use std::fmt;
|
||||
|
||||
use anyhow::{format_err, Context as _, Result};
|
||||
use anyhow::{Context as _, Result};
|
||||
use deltachat_derive::{FromSql, ToSql};
|
||||
use rand::{thread_rng, Rng};
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::contact::{normalize_name, Contact, ContactId, Modifier, Origin};
|
||||
use crate::contact::{normalize_name, Contact, Modifier, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::dc_tools::time;
|
||||
use crate::events::EventType;
|
||||
use crate::imap::Imap;
|
||||
use crate::message::{Message, MsgId};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::param::{Param, Params};
|
||||
use crate::param::Params;
|
||||
use crate::scheduler::InterruptInfo;
|
||||
use crate::smtp::{smtp_send, SendResult, Smtp};
|
||||
use crate::sql;
|
||||
|
||||
// results in ~3 weeks for the last backoff timespan
|
||||
const JOB_RETRIES: u32 = 17;
|
||||
|
||||
/// Thread IDs
|
||||
#[derive(
|
||||
Debug, Display, Copy, Clone, PartialEq, Eq, FromPrimitive, ToPrimitive, FromSql, ToSql,
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub(crate) enum Thread {
|
||||
Imap = 100,
|
||||
Smtp = 5000,
|
||||
}
|
||||
|
||||
/// Job try result.
|
||||
#[derive(Debug, Display)]
|
||||
pub enum Status {
|
||||
@@ -72,7 +58,6 @@ macro_rules! job_try {
|
||||
)]
|
||||
#[repr(u32)]
|
||||
pub enum Action {
|
||||
// Jobs in the INBOX-thread, range from DC_IMAP_THREAD..DC_IMAP_THREAD+999
|
||||
FetchExistingMsgs = 110,
|
||||
|
||||
// this is user initiated so it should have a fairly high priority
|
||||
@@ -87,24 +72,6 @@ pub enum Action {
|
||||
// UID synchronization is high-priority to make sure correct UIDs
|
||||
// are used by message moving/deletion.
|
||||
ResyncFolders = 300,
|
||||
|
||||
// Jobs in the SMTP-thread, range from DC_SMTP_THREAD..DC_SMTP_THREAD+999
|
||||
SendMdn = 5010,
|
||||
}
|
||||
|
||||
impl From<Action> for Thread {
|
||||
fn from(action: Action) -> Thread {
|
||||
use Action::*;
|
||||
|
||||
match action {
|
||||
FetchExistingMsgs => Thread::Imap,
|
||||
ResyncFolders => Thread::Imap,
|
||||
UpdateRecentQuota => Thread::Imap,
|
||||
DownloadMsg => Thread::Imap,
|
||||
|
||||
SendMdn => Thread::Smtp,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -159,9 +126,7 @@ impl Job {
|
||||
///
|
||||
/// The Job is consumed by this method.
|
||||
pub(crate) async fn save(self, context: &Context) -> Result<()> {
|
||||
let thread: Thread = self.action.into();
|
||||
|
||||
info!(context, "saving job for {}-thread: {:?}", thread, self);
|
||||
info!(context, "saving job {:?}", self);
|
||||
|
||||
if self.job_id != 0 {
|
||||
context
|
||||
@@ -178,10 +143,9 @@ impl Job {
|
||||
.await?;
|
||||
} else {
|
||||
context.sql.execute(
|
||||
"INSERT INTO jobs (added_timestamp, thread, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?,?);",
|
||||
"INSERT INTO jobs (added_timestamp, action, foreign_id, param, desired_timestamp) VALUES (?,?,?,?,?);",
|
||||
paramsv![
|
||||
self.added_timestamp,
|
||||
thread,
|
||||
self.action,
|
||||
self.foreign_id,
|
||||
self.param.to_string(),
|
||||
@@ -193,118 +157,6 @@ impl Job {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get `SendMdn` jobs with foreign_id equal to `contact_id` excluding the `job_id` job.
|
||||
async fn get_additional_mdn_jobs(
|
||||
&self,
|
||||
context: &Context,
|
||||
contact_id: ContactId,
|
||||
) -> Result<(Vec<u32>, Vec<String>)> {
|
||||
// Extract message IDs from job parameters
|
||||
let res: Vec<(u32, MsgId)> = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT id, param FROM jobs WHERE foreign_id=? AND id!=?",
|
||||
paramsv![contact_id, self.job_id],
|
||||
|row| {
|
||||
let job_id: u32 = row.get(0)?;
|
||||
let params_str: String = row.get(1)?;
|
||||
let params: Params = params_str.parse().unwrap_or_default();
|
||||
Ok((job_id, params))
|
||||
},
|
||||
|jobs| {
|
||||
let res = jobs
|
||||
.filter_map(|row| {
|
||||
let (job_id, params) = row.ok()?;
|
||||
let msg_id = params.get_msg_id()?;
|
||||
Some((job_id, msg_id))
|
||||
})
|
||||
.collect();
|
||||
Ok(res)
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Load corresponding RFC724 message IDs
|
||||
let mut job_ids = Vec::new();
|
||||
let mut rfc724_mids = Vec::new();
|
||||
for (job_id, msg_id) in res {
|
||||
if let Ok(Message { rfc724_mid, .. }) = Message::load_from_db(context, msg_id).await {
|
||||
job_ids.push(job_id);
|
||||
rfc724_mids.push(rfc724_mid);
|
||||
}
|
||||
}
|
||||
Ok((job_ids, rfc724_mids))
|
||||
}
|
||||
|
||||
async fn send_mdn(&mut self, context: &Context, smtp: &mut Smtp) -> Status {
|
||||
let mdns_enabled = job_try!(context.get_config_bool(Config::MdnsEnabled).await);
|
||||
if !mdns_enabled {
|
||||
// User has disabled MDNs after job scheduling but before
|
||||
// execution.
|
||||
return Status::Finished(Err(format_err!("MDNs are disabled")));
|
||||
}
|
||||
|
||||
let contact_id = ContactId::new(self.foreign_id);
|
||||
let contact = job_try!(Contact::load_from_db(context, contact_id).await);
|
||||
if contact.is_blocked() {
|
||||
return Status::Finished(Err(format_err!("Contact is blocked")));
|
||||
}
|
||||
|
||||
let msg_id = if let Some(msg_id) = self.param.get_msg_id() {
|
||||
msg_id
|
||||
} else {
|
||||
return Status::Finished(Err(format_err!(
|
||||
"SendMdn job has invalid parameters: {}",
|
||||
self.param
|
||||
)));
|
||||
};
|
||||
|
||||
// Try to aggregate other SendMdn jobs and send a combined MDN.
|
||||
let (additional_job_ids, additional_rfc724_mids) = self
|
||||
.get_additional_mdn_jobs(context, contact_id)
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
|
||||
if !additional_rfc724_mids.is_empty() {
|
||||
info!(
|
||||
context,
|
||||
"SendMdn job: aggregating {} additional MDNs",
|
||||
additional_rfc724_mids.len()
|
||||
)
|
||||
}
|
||||
|
||||
let msg = job_try!(Message::load_from_db(context, msg_id).await);
|
||||
let mimefactory =
|
||||
job_try!(MimeFactory::from_mdn(context, &msg, additional_rfc724_mids).await);
|
||||
let rendered_msg = job_try!(mimefactory.render(context).await);
|
||||
let body = rendered_msg.message;
|
||||
|
||||
let addr = contact.get_addr();
|
||||
let recipient = job_try!(async_smtp::EmailAddress::new(addr.to_string())
|
||||
.map_err(|err| format_err!("invalid recipient: {} {:?}", addr, err)));
|
||||
let recipients = vec![recipient];
|
||||
|
||||
// connect to SMTP server, if not yet done
|
||||
if let Err(err) = smtp.connect_configured(context).await {
|
||||
warn!(context, "SMTP connection failure: {:?}", err);
|
||||
smtp.last_send_error = Some(err.to_string());
|
||||
return Status::RetryLater;
|
||||
}
|
||||
|
||||
match smtp_send(context, &recipients, &body, smtp, msg_id, 0).await {
|
||||
SendResult::Success => {
|
||||
// Remove additional SendMdn jobs we have aggregated into this one.
|
||||
job_try!(kill_ids(context, &additional_job_ids).await);
|
||||
Status::Finished(Ok(()))
|
||||
}
|
||||
SendResult::Retry => {
|
||||
info!(context, "Temporary SMTP failure while sending an MDN");
|
||||
Status::RetryLater
|
||||
}
|
||||
SendResult::Failure(err) => Status::Finished(Err(err)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the recipients from old emails sent by the user and add them as contacts.
|
||||
/// This way, we can already offer them some email addresses they can write to.
|
||||
///
|
||||
@@ -387,22 +239,6 @@ pub async fn kill_action(context: &Context, action: Action) -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove jobs with specified IDs.
|
||||
async fn kill_ids(context: &Context, job_ids: &[u32]) -> Result<()> {
|
||||
if job_ids.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
let q = format!(
|
||||
"DELETE FROM jobs WHERE id IN({})",
|
||||
sql::repeat_vars(job_ids.len())
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.execute(q, rusqlite::params_from_iter(job_ids))
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn action_exists(context: &Context, action: Action) -> Result<bool> {
|
||||
let exists = context
|
||||
.sql
|
||||
@@ -461,36 +297,18 @@ async fn add_all_recipients_as_contacts(context: &Context, imap: &mut Imap, fold
|
||||
|
||||
pub(crate) enum Connection<'a> {
|
||||
Inbox(&'a mut Imap),
|
||||
Smtp(&'a mut Smtp),
|
||||
}
|
||||
|
||||
impl<'a> fmt::Display for Connection<'a> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Connection::Inbox(_) => write!(f, "Inbox"),
|
||||
Connection::Smtp(_) => write!(f, "Smtp"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Connection<'a> {
|
||||
fn inbox(&mut self) -> &mut Imap {
|
||||
match self {
|
||||
Connection::Inbox(imap) => imap,
|
||||
_ => panic!("Not an inbox"),
|
||||
}
|
||||
}
|
||||
|
||||
fn smtp(&mut self) -> &mut Smtp {
|
||||
match self {
|
||||
Connection::Smtp(smtp) => smtp,
|
||||
_ => panic!("Not a smtp"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_>, mut job: Job) {
|
||||
info!(context, "{}-job {} started...", &connection, &job);
|
||||
info!(context, "job {} started...", &job);
|
||||
|
||||
let try_res = match perform_job_action(context, &mut job, &mut connection, 0).await {
|
||||
Status::RetryNow => perform_job_action(context, &mut job, &mut connection, 1).await,
|
||||
@@ -502,17 +320,13 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
|
||||
let tries = job.tries + 1;
|
||||
|
||||
if tries < JOB_RETRIES {
|
||||
info!(
|
||||
context,
|
||||
"{} thread increases job {} tries to {}", &connection, job, tries
|
||||
);
|
||||
info!(context, "increase job {} tries to {}", job, tries);
|
||||
job.tries = tries;
|
||||
let time_offset = get_backoff_time_offset(tries, job.action);
|
||||
job.desired_timestamp = time() + time_offset;
|
||||
info!(
|
||||
context,
|
||||
"{}-job #{} not succeeded on try #{}, retry in {} seconds.",
|
||||
&connection,
|
||||
"job #{} not succeeded on try #{}, retry in {} seconds.",
|
||||
job.job_id as u32,
|
||||
tries,
|
||||
time_offset
|
||||
@@ -523,10 +337,7 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"{} thread removes job {} as it exhausted {} retries",
|
||||
&connection,
|
||||
job,
|
||||
JOB_RETRIES
|
||||
"remove job {} as it exhausted {} retries", job, JOB_RETRIES
|
||||
);
|
||||
job.delete(context).await.unwrap_or_else(|err| {
|
||||
error!(context, "failed to delete job: {}", err);
|
||||
@@ -537,13 +348,10 @@ pub(crate) async fn perform_job(context: &Context, mut connection: Connection<'_
|
||||
if let Err(err) = res {
|
||||
warn!(
|
||||
context,
|
||||
"{} removes job {} as it failed with error {:#}", &connection, job, err
|
||||
"remove job {} as it failed with error {:#}", job, err
|
||||
);
|
||||
} else {
|
||||
info!(
|
||||
context,
|
||||
"{} removes job {} as it succeeded", &connection, job
|
||||
);
|
||||
info!(context, "remove job {} as it succeeded", job);
|
||||
}
|
||||
|
||||
job.delete(context).await.unwrap_or_else(|err| {
|
||||
@@ -559,13 +367,9 @@ async fn perform_job_action(
|
||||
connection: &mut Connection<'_>,
|
||||
tries: u32,
|
||||
) -> Status {
|
||||
info!(
|
||||
context,
|
||||
"{} begin immediate try {} of job {}", &connection, tries, job
|
||||
);
|
||||
info!(context, "begin immediate try {} of job {}", tries, job);
|
||||
|
||||
let try_res = match job.action {
|
||||
Action::SendMdn => job.send_mdn(context, connection.smtp()).await,
|
||||
Action::ResyncFolders => job.resync_folders(context, connection.inbox()).await,
|
||||
Action::FetchExistingMsgs => job.fetch_existing_msgs(context, connection.inbox()).await,
|
||||
Action::UpdateRecentQuota => match context.update_recent_quota(connection.inbox()).await {
|
||||
@@ -600,19 +404,6 @@ fn get_backoff_time_offset(tries: u32, action: Action) -> i64 {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn send_mdn(context: &Context, msg_id: MsgId, from_id: ContactId) -> Result<()> {
|
||||
let mut param = Params::new();
|
||||
param.set(Param::MsgId, msg_id.to_u32().to_string());
|
||||
|
||||
add(
|
||||
context,
|
||||
Job::new(Action::SendMdn, from_id.to_u32(), param, 0),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn schedule_resync(context: &Context) -> Result<()> {
|
||||
kill_action(context, Action::ResyncFolders).await?;
|
||||
add(
|
||||
@@ -638,10 +429,6 @@ pub async fn add(context: &Context, job: Job) -> Result<()> {
|
||||
info!(context, "interrupt: imap");
|
||||
context.interrupt_inbox(InterruptInfo::new(false)).await;
|
||||
}
|
||||
Action::SendMdn => {
|
||||
info!(context, "interrupt: smtp");
|
||||
context.interrupt_smtp(InterruptInfo::new(false)).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
@@ -649,21 +436,15 @@ pub async fn add(context: &Context, job: Job) -> Result<()> {
|
||||
|
||||
/// Load jobs from the database.
|
||||
///
|
||||
/// Load jobs for this "[Thread]", i.e. either load SMTP jobs or load
|
||||
/// IMAP jobs. The `probe_network` parameter decides how to query
|
||||
/// The `probe_network` parameter decides how to query
|
||||
/// jobs, this is tricky and probably wrong currently. Look at the
|
||||
/// SQL queries for details.
|
||||
pub(crate) async fn load_next(
|
||||
context: &Context,
|
||||
thread: Thread,
|
||||
info: &InterruptInfo,
|
||||
) -> Result<Option<Job>> {
|
||||
info!(context, "loading job for {}-thread", thread);
|
||||
pub(crate) async fn load_next(context: &Context, info: &InterruptInfo) -> Result<Option<Job>> {
|
||||
info!(context, "loading job");
|
||||
|
||||
let query;
|
||||
let params;
|
||||
let t = time();
|
||||
let thread_i = thread as i64;
|
||||
|
||||
if !info.probe_network {
|
||||
// processing for first-try and after backoff-timeouts:
|
||||
@@ -671,11 +452,11 @@ pub(crate) async fn load_next(
|
||||
query = r#"
|
||||
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
|
||||
FROM jobs
|
||||
WHERE thread=? AND desired_timestamp<=?
|
||||
WHERE desired_timestamp<=?
|
||||
ORDER BY action DESC, added_timestamp
|
||||
LIMIT 1;
|
||||
"#;
|
||||
params = paramsv![thread_i, t];
|
||||
params = paramsv![t];
|
||||
} else {
|
||||
// processing after call to dc_maybe_network():
|
||||
// process _all_ pending jobs that failed before
|
||||
@@ -683,11 +464,11 @@ LIMIT 1;
|
||||
query = r#"
|
||||
SELECT id, action, foreign_id, param, added_timestamp, desired_timestamp, tries
|
||||
FROM jobs
|
||||
WHERE thread=? AND tries>0
|
||||
WHERE tries>0
|
||||
ORDER BY desired_timestamp, action DESC
|
||||
LIMIT 1;
|
||||
"#;
|
||||
params = paramsv![thread_i];
|
||||
params = paramsv![];
|
||||
};
|
||||
|
||||
loop {
|
||||
@@ -742,11 +523,10 @@ mod tests {
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO jobs
|
||||
(added_timestamp, thread, action, foreign_id, param, desired_timestamp)
|
||||
VALUES (?, ?, ?, ?, ?, ?);",
|
||||
(added_timestamp, action, foreign_id, param, desired_timestamp)
|
||||
VALUES (?, ?, ?, ?, ?);",
|
||||
paramsv![
|
||||
now,
|
||||
Thread::from(Action::DownloadMsg),
|
||||
if valid {
|
||||
Action::DownloadMsg as i32
|
||||
} else {
|
||||
@@ -768,21 +548,11 @@ mod tests {
|
||||
// all jobs.
|
||||
let t = TestContext::new().await;
|
||||
insert_job(&t, 1, false).await; // This can not be loaded into Job struct.
|
||||
let jobs = load_next(
|
||||
&t,
|
||||
Thread::from(Action::DownloadMsg),
|
||||
&InterruptInfo::new(false),
|
||||
)
|
||||
.await?;
|
||||
let jobs = load_next(&t, &InterruptInfo::new(false)).await?;
|
||||
assert!(jobs.is_none());
|
||||
|
||||
insert_job(&t, 1, true).await;
|
||||
let jobs = load_next(
|
||||
&t,
|
||||
Thread::from(Action::DownloadMsg),
|
||||
&InterruptInfo::new(false),
|
||||
)
|
||||
.await?;
|
||||
let jobs = load_next(&t, &InterruptInfo::new(false)).await?;
|
||||
assert!(jobs.is_some());
|
||||
Ok(())
|
||||
}
|
||||
@@ -793,12 +563,7 @@ mod tests {
|
||||
|
||||
insert_job(&t, 1, true).await;
|
||||
|
||||
let jobs = load_next(
|
||||
&t,
|
||||
Thread::from(Action::DownloadMsg),
|
||||
&InterruptInfo::new(false),
|
||||
)
|
||||
.await?;
|
||||
let jobs = load_next(&t, &InterruptInfo::new(false)).await?;
|
||||
assert!(jobs.is_some());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -150,8 +150,7 @@ impl LoginParam {
|
||||
}
|
||||
|
||||
/// Read the login parameters from the database.
|
||||
async fn from_database(context: &Context, prefix: impl AsRef<str>) -> Result<Self> {
|
||||
let prefix = prefix.as_ref();
|
||||
async fn from_database(context: &Context, prefix: &str) -> Result<Self> {
|
||||
let sql = &context.sql;
|
||||
|
||||
let key = format!("{}addr", prefix);
|
||||
|
||||
@@ -23,7 +23,6 @@ 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::job;
|
||||
use crate::log::LogExt;
|
||||
use crate::mimeparser::{parse_message_id, FailureReport, SystemMessage};
|
||||
use crate::param::{Param, Params};
|
||||
@@ -1364,9 +1363,15 @@ pub async fn markseen_msgs(context: &Context, msg_ids: Vec<MsgId>) -> Result<()>
|
||||
{
|
||||
let mdns_enabled = context.get_config_bool(Config::MdnsEnabled).await?;
|
||||
if mdns_enabled {
|
||||
if let Err(err) = job::send_mdn(context, id, curr_from_id).await {
|
||||
warn!(context, "could not send out mdn for {}: {}", id, err);
|
||||
}
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"INSERT INTO smtp_mdns (msg_id, from_id, rfc724_mid) VALUES(?, ?, ?)",
|
||||
paramsv![id, curr_from_id, curr_rfc724_mid],
|
||||
)
|
||||
.await
|
||||
.context("failed to insert into smtp_mdns")?;
|
||||
context.interrupt_smtp(InterruptInfo::new(false)).await;
|
||||
}
|
||||
}
|
||||
updated_chat_ids.insert(curr_chat_id);
|
||||
@@ -1975,9 +1980,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
let mut has_image = false;
|
||||
let chatitems = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let chatitems = chat::get_chat_msgs(&t, device_chat_id, 0).await.unwrap();
|
||||
for chatitem in chatitems {
|
||||
if let ChatItem::Message { msg_id } = chatitem {
|
||||
if let Ok(msg) = Message::load_from_db(&t, msg_id).await {
|
||||
@@ -2123,7 +2126,7 @@ mod tests {
|
||||
assert_eq!(msg1.chat_id, msg2.chat_id);
|
||||
let chats = Chatlist::try_load(&bob, 0, None, None).await?;
|
||||
assert_eq!(chats.len(), 1);
|
||||
let msgs = chat::get_chat_msgs(&bob, bob_chat_id, 0, None).await?;
|
||||
let msgs = chat::get_chat_msgs(&bob, bob_chat_id, 0).await?;
|
||||
assert_eq!(msgs.len(), 2);
|
||||
assert_eq!(bob.get_fresh_msgs().await?.len(), 0);
|
||||
|
||||
@@ -2134,7 +2137,7 @@ mod tests {
|
||||
let bob_chat = Chat::load_from_db(&bob, bob_chat_id).await?;
|
||||
assert_eq!(bob_chat.blocked, Blocked::Request);
|
||||
|
||||
let msgs = chat::get_chat_msgs(&bob, bob_chat_id, 0, None).await?;
|
||||
let msgs = chat::get_chat_msgs(&bob, bob_chat_id, 0).await?;
|
||||
assert_eq!(msgs.len(), 2);
|
||||
bob_chat_id.accept(&bob).await.unwrap();
|
||||
|
||||
|
||||
@@ -805,7 +805,7 @@ impl MimeMessage {
|
||||
The second body part contains the control information necessary to
|
||||
verify the digital signature." We simply take the first body part and
|
||||
skip the rest. (see
|
||||
<https://k9mail.github.io/2016/11/24/OpenPGP-Considerations-Part-I.html>
|
||||
<https://k9mail.app/2016/11/24/OpenPGP-Considerations-Part-I.html>
|
||||
for background information why we use encrypted+signed) */
|
||||
if let Some(first) = mail.subparts.get(0) {
|
||||
any_part_added = self
|
||||
|
||||
@@ -169,6 +169,12 @@ pub enum Param {
|
||||
/// For Chats: timestamp of protection settings update.
|
||||
ProtectionSettingsTimestamp = b'L',
|
||||
|
||||
/// For Webxdc Message Instances: Current document name
|
||||
WebxdcDocument = b'R',
|
||||
|
||||
/// For Webxdc Message Instances: timestamp of document name update.
|
||||
WebxdcDocumentTimestamp = b'W',
|
||||
|
||||
/// For Webxdc Message Instances: Current summary
|
||||
WebxdcSummary = b'N',
|
||||
|
||||
|
||||
@@ -190,7 +190,9 @@ pub(crate) fn create_keypair(
|
||||
let key = key_params
|
||||
.generate()
|
||||
.map_err(|err| PgpKeygenError::new("invalid params", err))?;
|
||||
let private_key = key.sign(|| "".into()).expect("failed to sign secret key");
|
||||
let private_key = key
|
||||
.sign(|| "".into())
|
||||
.map_err(|err| PgpKeygenError::new("failed to sign secret key", err))?;
|
||||
|
||||
let public_key = private_key.public_key();
|
||||
let public_key = public_key
|
||||
|
||||
308
src/scheduler.rs
308
src/scheduler.rs
@@ -2,7 +2,7 @@ use anyhow::{bail, Context as _, Result};
|
||||
use async_std::prelude::*;
|
||||
use async_std::{
|
||||
channel::{self, Receiver, Sender},
|
||||
task,
|
||||
future, task,
|
||||
};
|
||||
|
||||
use crate::config::Config;
|
||||
@@ -11,7 +11,7 @@ use crate::dc_tools::maybe_add_time_based_warnings;
|
||||
use crate::dc_tools::time;
|
||||
use crate::ephemeral::{self, delete_expired_imap_messages};
|
||||
use crate::imap::Imap;
|
||||
use crate::job::{self, Thread};
|
||||
use crate::job;
|
||||
use crate::location;
|
||||
use crate::log::LogExt;
|
||||
use crate::smtp::{send_smtp_messages, Smtp};
|
||||
@@ -23,54 +23,62 @@ pub(crate) mod connectivity;
|
||||
|
||||
/// Job and connection scheduler.
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub(crate) enum Scheduler {
|
||||
Stopped,
|
||||
Running {
|
||||
inbox: ImapConnectionState,
|
||||
inbox_handle: Option<task::JoinHandle<()>>,
|
||||
mvbox: ImapConnectionState,
|
||||
mvbox_handle: Option<task::JoinHandle<()>>,
|
||||
sentbox: ImapConnectionState,
|
||||
sentbox_handle: Option<task::JoinHandle<()>>,
|
||||
smtp: SmtpConnectionState,
|
||||
smtp_handle: Option<task::JoinHandle<()>>,
|
||||
ephemeral_handle: Option<task::JoinHandle<()>>,
|
||||
ephemeral_interrupt_send: Sender<()>,
|
||||
location_handle: Option<task::JoinHandle<()>>,
|
||||
location_interrupt_send: Sender<()>,
|
||||
},
|
||||
pub(crate) struct Scheduler {
|
||||
inbox: ImapConnectionState,
|
||||
inbox_handle: task::JoinHandle<()>,
|
||||
mvbox: ImapConnectionState,
|
||||
mvbox_handle: Option<task::JoinHandle<()>>,
|
||||
sentbox: ImapConnectionState,
|
||||
sentbox_handle: Option<task::JoinHandle<()>>,
|
||||
smtp: SmtpConnectionState,
|
||||
smtp_handle: task::JoinHandle<()>,
|
||||
ephemeral_handle: task::JoinHandle<()>,
|
||||
ephemeral_interrupt_send: Sender<()>,
|
||||
location_handle: task::JoinHandle<()>,
|
||||
location_interrupt_send: Sender<()>,
|
||||
}
|
||||
|
||||
impl Context {
|
||||
/// Indicate that the network likely has come back.
|
||||
pub async fn maybe_network(&self) {
|
||||
let lock = self.scheduler.read().await;
|
||||
lock.maybe_network().await;
|
||||
if let Some(scheduler) = &*lock {
|
||||
scheduler.maybe_network().await;
|
||||
}
|
||||
connectivity::idle_interrupted(lock).await;
|
||||
}
|
||||
|
||||
/// Indicate that the network likely is lost.
|
||||
pub async fn maybe_network_lost(&self) {
|
||||
let lock = self.scheduler.read().await;
|
||||
lock.maybe_network_lost().await;
|
||||
if let Some(scheduler) = &*lock {
|
||||
scheduler.maybe_network_lost().await;
|
||||
}
|
||||
connectivity::maybe_network_lost(self, lock).await;
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_inbox(&self, info: InterruptInfo) {
|
||||
self.scheduler.read().await.interrupt_inbox(info).await;
|
||||
if let Some(scheduler) = &*self.scheduler.read().await {
|
||||
scheduler.interrupt_inbox(info).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_smtp(&self, info: InterruptInfo) {
|
||||
self.scheduler.read().await.interrupt_smtp(info).await;
|
||||
if let Some(scheduler) = &*self.scheduler.read().await {
|
||||
scheduler.interrupt_smtp(info).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_ephemeral_task(&self) {
|
||||
self.scheduler.read().await.interrupt_ephemeral_task().await;
|
||||
if let Some(scheduler) = &*self.scheduler.read().await {
|
||||
scheduler.interrupt_ephemeral_task().await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn interrupt_location(&self) {
|
||||
self.scheduler.read().await.interrupt_location().await;
|
||||
if let Some(scheduler) = &*self.scheduler.read().await {
|
||||
scheduler.interrupt_location().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,7 +101,7 @@ async fn inbox_loop(ctx: Context, started: Sender<()>, inbox_handlers: ImapConne
|
||||
|
||||
let mut info = InterruptInfo::default();
|
||||
loop {
|
||||
let job = match job::load_next(&ctx, Thread::Imap, &info).await {
|
||||
let job = match job::load_next(&ctx, &info).await {
|
||||
Err(err) => {
|
||||
error!(ctx, "Failed loading job from the database: {:#}.", err);
|
||||
None
|
||||
@@ -303,65 +311,46 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
|
||||
}
|
||||
|
||||
let mut timeout = None;
|
||||
let mut interrupt_info = Default::default();
|
||||
loop {
|
||||
let job = match job::load_next(&ctx, Thread::Smtp, &interrupt_info).await {
|
||||
Err(err) => {
|
||||
error!(ctx, "Failed loading job from the database: {:#}.", err);
|
||||
None
|
||||
}
|
||||
Ok(job) => job,
|
||||
let res = send_smtp_messages(&ctx, &mut connection).await;
|
||||
if let Err(err) = &res {
|
||||
warn!(ctx, "send_smtp_messages failed: {:#}", err);
|
||||
}
|
||||
let success = res.unwrap_or(false);
|
||||
timeout = if success {
|
||||
None
|
||||
} else {
|
||||
Some(timeout.map_or(30, |timeout: u64| timeout.saturating_mul(3)))
|
||||
};
|
||||
|
||||
match job {
|
||||
Some(job) => {
|
||||
info!(ctx, "executing smtp job");
|
||||
job::perform_job(&ctx, job::Connection::Smtp(&mut connection), job).await;
|
||||
interrupt_info = Default::default();
|
||||
}
|
||||
None => {
|
||||
let res = send_smtp_messages(&ctx, &mut connection).await;
|
||||
if let Err(err) = &res {
|
||||
warn!(ctx, "send_smtp_messages failed: {:#}", err);
|
||||
}
|
||||
let success = res.unwrap_or(false);
|
||||
timeout = if success {
|
||||
None
|
||||
} else {
|
||||
Some(timeout.map_or(30, |timeout: u64| timeout.saturating_mul(3)))
|
||||
};
|
||||
|
||||
// Fake Idle
|
||||
info!(ctx, "smtp fake idle - started");
|
||||
match &connection.last_send_error {
|
||||
None => connection.connectivity.set_connected(&ctx).await,
|
||||
Some(err) => connection.connectivity.set_err(&ctx, err).await,
|
||||
}
|
||||
|
||||
// If send_smtp_messages() failed, we set a timeout for the fake-idle so that
|
||||
// sending is retried (at the latest) after the timeout. If sending fails
|
||||
// again, we increase the timeout exponentially, in order not to do lots of
|
||||
// unnecessary retries.
|
||||
if let Some(timeout) = timeout {
|
||||
info!(
|
||||
ctx,
|
||||
"smtp has messages to retry, planning to retry {} seconds later",
|
||||
timeout
|
||||
);
|
||||
let duration = std::time::Duration::from_secs(timeout);
|
||||
interrupt_info = async_std::future::timeout(duration, async {
|
||||
idle_interrupt_receiver.recv().await.unwrap_or_default()
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
} else {
|
||||
info!(ctx, "smtp has no messages to retry, waiting for interrupt");
|
||||
interrupt_info = idle_interrupt_receiver.recv().await.unwrap_or_default();
|
||||
};
|
||||
|
||||
info!(ctx, "smtp fake idle - interrupted")
|
||||
}
|
||||
// Fake Idle
|
||||
info!(ctx, "smtp fake idle - started");
|
||||
match &connection.last_send_error {
|
||||
None => connection.connectivity.set_connected(&ctx).await,
|
||||
Some(err) => connection.connectivity.set_err(&ctx, err).await,
|
||||
}
|
||||
|
||||
// If send_smtp_messages() failed, we set a timeout for the fake-idle so that
|
||||
// sending is retried (at the latest) after the timeout. If sending fails
|
||||
// again, we increase the timeout exponentially, in order not to do lots of
|
||||
// unnecessary retries.
|
||||
if let Some(timeout) = timeout {
|
||||
info!(
|
||||
ctx,
|
||||
"smtp has messages to retry, planning to retry {} seconds later", timeout
|
||||
);
|
||||
let duration = std::time::Duration::from_secs(timeout);
|
||||
async_std::future::timeout(duration, async {
|
||||
idle_interrupt_receiver.recv().await.unwrap_or_default()
|
||||
})
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
} else {
|
||||
info!(ctx, "smtp has no messages to retry, waiting for interrupt");
|
||||
idle_interrupt_receiver.recv().await.unwrap_or_default();
|
||||
};
|
||||
|
||||
info!(ctx, "smtp fake idle - interrupted")
|
||||
}
|
||||
};
|
||||
|
||||
@@ -375,12 +364,8 @@ async fn smtp_loop(ctx: Context, started: Sender<()>, smtp_handlers: SmtpConnect
|
||||
}
|
||||
|
||||
impl Scheduler {
|
||||
/// Start the scheduler, returns error if it is already running.
|
||||
pub async fn start(&mut self, ctx: Context) -> Result<()> {
|
||||
if self.is_running() {
|
||||
bail!("scheduler is already started");
|
||||
}
|
||||
|
||||
/// Start the scheduler.
|
||||
pub async fn start(ctx: Context) -> Result<Self> {
|
||||
let (mvbox, mvbox_handlers) = ImapConnectionState::new(&ctx).await?;
|
||||
let (sentbox, sentbox_handlers) = ImapConnectionState::new(&ctx).await?;
|
||||
let (smtp, smtp_handlers) = SmtpConnectionState::new();
|
||||
@@ -397,9 +382,7 @@ impl Scheduler {
|
||||
|
||||
let inbox_handle = {
|
||||
let ctx = ctx.clone();
|
||||
Some(task::spawn(async move {
|
||||
inbox_loop(ctx, inbox_start_send, inbox_handlers).await
|
||||
}))
|
||||
task::spawn(async move { inbox_loop(ctx, inbox_start_send, inbox_handlers).await })
|
||||
};
|
||||
|
||||
if ctx.should_watch_mvbox().await? {
|
||||
@@ -450,26 +433,24 @@ impl Scheduler {
|
||||
|
||||
let smtp_handle = {
|
||||
let ctx = ctx.clone();
|
||||
Some(task::spawn(async move {
|
||||
smtp_loop(ctx, smtp_start_send, smtp_handlers).await
|
||||
}))
|
||||
task::spawn(async move { smtp_loop(ctx, smtp_start_send, smtp_handlers).await })
|
||||
};
|
||||
|
||||
let ephemeral_handle = {
|
||||
let ctx = ctx.clone();
|
||||
Some(task::spawn(async move {
|
||||
task::spawn(async move {
|
||||
ephemeral::ephemeral_loop(&ctx, ephemeral_interrupt_recv).await;
|
||||
}))
|
||||
})
|
||||
};
|
||||
|
||||
let location_handle = {
|
||||
let ctx = ctx.clone();
|
||||
Some(task::spawn(async move {
|
||||
task::spawn(async move {
|
||||
location::location_loop(&ctx, location_interrupt_recv).await;
|
||||
}))
|
||||
})
|
||||
};
|
||||
|
||||
*self = Scheduler::Running {
|
||||
let res = Self {
|
||||
inbox,
|
||||
mvbox,
|
||||
sentbox,
|
||||
@@ -496,14 +477,10 @@ impl Scheduler {
|
||||
}
|
||||
|
||||
info!(ctx, "scheduler is running");
|
||||
Ok(())
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
async fn maybe_network(&self) {
|
||||
if !self.is_running() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.interrupt_inbox(InterruptInfo::new(true))
|
||||
.join(self.interrupt_mvbox(InterruptInfo::new(true)))
|
||||
.join(self.interrupt_sentbox(InterruptInfo::new(true)))
|
||||
@@ -512,10 +489,6 @@ impl Scheduler {
|
||||
}
|
||||
|
||||
async fn maybe_network_lost(&self) {
|
||||
if !self.is_running() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.interrupt_inbox(InterruptInfo::new(false))
|
||||
.join(self.interrupt_mvbox(InterruptInfo::new(false)))
|
||||
.join(self.interrupt_sentbox(InterruptInfo::new(false)))
|
||||
@@ -524,109 +497,64 @@ impl Scheduler {
|
||||
}
|
||||
|
||||
async fn interrupt_inbox(&self, info: InterruptInfo) {
|
||||
if let Scheduler::Running { ref inbox, .. } = self {
|
||||
inbox.interrupt(info).await;
|
||||
}
|
||||
self.inbox.interrupt(info).await;
|
||||
}
|
||||
|
||||
async fn interrupt_mvbox(&self, info: InterruptInfo) {
|
||||
if let Scheduler::Running { ref mvbox, .. } = self {
|
||||
mvbox.interrupt(info).await;
|
||||
}
|
||||
self.mvbox.interrupt(info).await;
|
||||
}
|
||||
|
||||
async fn interrupt_sentbox(&self, info: InterruptInfo) {
|
||||
if let Scheduler::Running { ref sentbox, .. } = self {
|
||||
sentbox.interrupt(info).await;
|
||||
}
|
||||
self.sentbox.interrupt(info).await;
|
||||
}
|
||||
|
||||
async fn interrupt_smtp(&self, info: InterruptInfo) {
|
||||
if let Scheduler::Running { ref smtp, .. } = self {
|
||||
smtp.interrupt(info).await;
|
||||
}
|
||||
self.smtp.interrupt(info).await;
|
||||
}
|
||||
|
||||
async fn interrupt_ephemeral_task(&self) {
|
||||
if let Scheduler::Running {
|
||||
ref ephemeral_interrupt_send,
|
||||
..
|
||||
} = self
|
||||
{
|
||||
ephemeral_interrupt_send.try_send(()).ok();
|
||||
}
|
||||
self.ephemeral_interrupt_send.try_send(()).ok();
|
||||
}
|
||||
|
||||
async fn interrupt_location(&self) {
|
||||
if let Scheduler::Running {
|
||||
ref location_interrupt_send,
|
||||
..
|
||||
} = self
|
||||
{
|
||||
location_interrupt_send.try_send(()).ok();
|
||||
}
|
||||
self.location_interrupt_send.try_send(()).ok();
|
||||
}
|
||||
|
||||
/// Halt the scheduler.
|
||||
pub(crate) async fn stop(&mut self) -> Result<()> {
|
||||
match self {
|
||||
Scheduler::Stopped => {
|
||||
bail!("scheduler is already stopped");
|
||||
}
|
||||
Scheduler::Running {
|
||||
inbox,
|
||||
inbox_handle,
|
||||
mvbox,
|
||||
mvbox_handle,
|
||||
sentbox,
|
||||
sentbox_handle,
|
||||
smtp,
|
||||
smtp_handle,
|
||||
ephemeral_handle,
|
||||
location_handle,
|
||||
..
|
||||
} => {
|
||||
if inbox_handle.is_some() {
|
||||
inbox.stop().await?;
|
||||
}
|
||||
if mvbox_handle.is_some() {
|
||||
mvbox.stop().await?;
|
||||
}
|
||||
if sentbox_handle.is_some() {
|
||||
sentbox.stop().await?;
|
||||
}
|
||||
if smtp_handle.is_some() {
|
||||
smtp.stop().await?;
|
||||
}
|
||||
|
||||
if let Some(handle) = inbox_handle.take() {
|
||||
handle.await;
|
||||
}
|
||||
if let Some(handle) = mvbox_handle.take() {
|
||||
handle.await;
|
||||
}
|
||||
if let Some(handle) = sentbox_handle.take() {
|
||||
handle.await;
|
||||
}
|
||||
if let Some(handle) = smtp_handle.take() {
|
||||
handle.await;
|
||||
}
|
||||
if let Some(handle) = ephemeral_handle.take() {
|
||||
handle.cancel().await;
|
||||
}
|
||||
if let Some(handle) = location_handle.take() {
|
||||
handle.cancel().await;
|
||||
}
|
||||
|
||||
*self = Scheduler::Stopped;
|
||||
Ok(())
|
||||
}
|
||||
///
|
||||
/// It consumes the scheduler and never fails to stop it. In the worst case, long-running tasks
|
||||
/// are forcefully terminated if they cannot shutdown within the timeout.
|
||||
pub(crate) async fn stop(mut self, context: &Context) {
|
||||
// Send stop signals to tasks so they can shutdown cleanly.
|
||||
self.inbox.stop().await.ok_or_log(context);
|
||||
if self.mvbox_handle.is_some() {
|
||||
self.mvbox.stop().await.ok_or_log(context);
|
||||
}
|
||||
}
|
||||
if self.sentbox_handle.is_some() {
|
||||
self.sentbox.stop().await.ok_or_log(context);
|
||||
}
|
||||
self.smtp.stop().await.ok_or_log(context);
|
||||
|
||||
/// Check if the scheduler is running.
|
||||
pub fn is_running(&self) -> bool {
|
||||
matches!(self, Scheduler::Running { .. })
|
||||
// Actually shutdown tasks.
|
||||
let timeout_duration = std::time::Duration::from_secs(30);
|
||||
future::timeout(timeout_duration, self.inbox_handle)
|
||||
.await
|
||||
.ok_or_log(context);
|
||||
if let Some(mvbox_handle) = self.mvbox_handle.take() {
|
||||
future::timeout(timeout_duration, mvbox_handle)
|
||||
.await
|
||||
.ok_or_log(context);
|
||||
}
|
||||
if let Some(sentbox_handle) = self.sentbox_handle.take() {
|
||||
future::timeout(timeout_duration, sentbox_handle)
|
||||
.await
|
||||
.ok_or_log(context);
|
||||
}
|
||||
future::timeout(timeout_duration, self.smtp_handle)
|
||||
.await
|
||||
.ok_or_log(context);
|
||||
self.ephemeral_handle.cancel().await;
|
||||
self.location_handle.cancel().await;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -161,19 +161,19 @@ impl ConnectivityStore {
|
||||
/// Set all folder states to InterruptingIdle in case they were `Connected` before.
|
||||
/// Called during `dc_maybe_network()` to make sure that `dc_accounts_all_work_done()`
|
||||
/// returns false immediately after `dc_maybe_network()`.
|
||||
pub(crate) async fn idle_interrupted(scheduler: RwLockReadGuard<'_, Scheduler>) {
|
||||
pub(crate) async fn idle_interrupted(scheduler: RwLockReadGuard<'_, Option<Scheduler>>) {
|
||||
let [inbox, mvbox, sentbox] = match &*scheduler {
|
||||
Scheduler::Running {
|
||||
Some(Scheduler {
|
||||
inbox,
|
||||
mvbox,
|
||||
sentbox,
|
||||
..
|
||||
} => [
|
||||
}) => [
|
||||
inbox.state.connectivity.clone(),
|
||||
mvbox.state.connectivity.clone(),
|
||||
sentbox.state.connectivity.clone(),
|
||||
],
|
||||
Scheduler::Stopped => return,
|
||||
None => return,
|
||||
};
|
||||
drop(scheduler);
|
||||
|
||||
@@ -205,20 +205,20 @@ pub(crate) async fn idle_interrupted(scheduler: RwLockReadGuard<'_, Scheduler>)
|
||||
/// after `maybe_network_lost()` was called.
|
||||
pub(crate) async fn maybe_network_lost(
|
||||
context: &Context,
|
||||
scheduler: RwLockReadGuard<'_, Scheduler>,
|
||||
scheduler: RwLockReadGuard<'_, Option<Scheduler>>,
|
||||
) {
|
||||
let stores = match &*scheduler {
|
||||
Scheduler::Running {
|
||||
Some(Scheduler {
|
||||
inbox,
|
||||
mvbox,
|
||||
sentbox,
|
||||
..
|
||||
} => [
|
||||
}) => [
|
||||
inbox.state.connectivity.clone(),
|
||||
mvbox.state.connectivity.clone(),
|
||||
sentbox.state.connectivity.clone(),
|
||||
],
|
||||
Scheduler::Stopped => return,
|
||||
None => return,
|
||||
};
|
||||
drop(scheduler);
|
||||
|
||||
@@ -265,16 +265,16 @@ impl Context {
|
||||
pub async fn get_connectivity(&self) -> Connectivity {
|
||||
let lock = self.scheduler.read().await;
|
||||
let stores: Vec<_> = match &*lock {
|
||||
Scheduler::Running {
|
||||
Some(Scheduler {
|
||||
inbox,
|
||||
mvbox,
|
||||
sentbox,
|
||||
..
|
||||
} => [&inbox.state, &mvbox.state, &sentbox.state]
|
||||
}) => [&inbox.state, &mvbox.state, &sentbox.state]
|
||||
.iter()
|
||||
.map(|state| state.connectivity.clone())
|
||||
.collect(),
|
||||
Scheduler::Stopped => return Connectivity::NotConnected,
|
||||
None => return Connectivity::NotConnected,
|
||||
};
|
||||
drop(lock);
|
||||
|
||||
@@ -353,13 +353,13 @@ impl Context {
|
||||
|
||||
let lock = self.scheduler.read().await;
|
||||
let (folders_states, smtp) = match &*lock {
|
||||
Scheduler::Running {
|
||||
Some(Scheduler {
|
||||
inbox,
|
||||
mvbox,
|
||||
sentbox,
|
||||
smtp,
|
||||
..
|
||||
} => (
|
||||
}) => (
|
||||
[
|
||||
(
|
||||
Config::ConfiguredInboxFolder,
|
||||
@@ -376,7 +376,7 @@ impl Context {
|
||||
],
|
||||
smtp.state.connectivity.clone(),
|
||||
),
|
||||
Scheduler::Stopped => {
|
||||
None => {
|
||||
return Err(anyhow!("Not started"));
|
||||
}
|
||||
};
|
||||
@@ -552,17 +552,17 @@ impl Context {
|
||||
pub async fn all_work_done(&self) -> bool {
|
||||
let lock = self.scheduler.read().await;
|
||||
let stores: Vec<_> = match &*lock {
|
||||
Scheduler::Running {
|
||||
Some(Scheduler {
|
||||
inbox,
|
||||
mvbox,
|
||||
sentbox,
|
||||
smtp,
|
||||
..
|
||||
} => [&inbox.state, &mvbox.state, &sentbox.state, &smtp.state]
|
||||
}) => [&inbox.state, &mvbox.state, &sentbox.state, &smtp.state]
|
||||
.iter()
|
||||
.map(|state| state.connectivity.clone())
|
||||
.collect(),
|
||||
Scheduler::Stopped => return false,
|
||||
None => return false,
|
||||
};
|
||||
drop(lock);
|
||||
|
||||
|
||||
@@ -139,9 +139,6 @@ async fn get_self_fingerprint(context: &Context) -> Option<Fingerprint> {
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum JoinError {
|
||||
#[error("An \"ongoing\" process is already running")]
|
||||
OngoingRunning,
|
||||
|
||||
#[error("Failed to send handshake message: {0}")]
|
||||
SendMessage(#[from] SendMsgError),
|
||||
|
||||
@@ -150,10 +147,6 @@ pub enum JoinError {
|
||||
#[error("Unknown contact (this is a bug): {0}")]
|
||||
UnknownContact(#[source] anyhow::Error),
|
||||
|
||||
// Note that this can only occur if we failed to create the chat correctly.
|
||||
#[error("Ongoing sender dropped (this is a bug)")]
|
||||
OngoingSenderDropped,
|
||||
|
||||
#[error("Other")]
|
||||
Other(#[from] anyhow::Error),
|
||||
}
|
||||
@@ -715,7 +708,7 @@ mod tests {
|
||||
use crate::chat;
|
||||
use crate::chat::ProtectionStatus;
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::constants::Chattype;
|
||||
use crate::constants::{Chattype, DC_GCM_ADDDAYMARKER};
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
use crate::peerstate::Peerstate;
|
||||
use crate::test_utils::{TestContext, TestContextManager};
|
||||
@@ -748,7 +741,6 @@ mod tests {
|
||||
);
|
||||
|
||||
let sent = bob.pop_sent_msg().await;
|
||||
assert!(!bob.ctx.has_ongoing().await);
|
||||
assert_eq!(sent.recipient(), "alice@example.org".parse().unwrap());
|
||||
let msg = alice.parse_msg(&sent).await;
|
||||
assert!(!msg.was_encrypted());
|
||||
@@ -853,7 +845,7 @@ mod tests {
|
||||
// Check Alice got the verified message in her 1:1 chat.
|
||||
{
|
||||
let chat = alice.create_chat(&bob).await;
|
||||
let msg_id = chat::get_chat_msgs(&alice.ctx, chat.get_id(), 0x1, None)
|
||||
let msg_id = chat::get_chat_msgs(&alice.ctx, chat.get_id(), DC_GCM_ADDDAYMARKER)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
@@ -902,7 +894,7 @@ mod tests {
|
||||
// Check Bob got the verified message in his 1:1 chat.
|
||||
{
|
||||
let chat = bob.create_chat(&alice).await;
|
||||
let msg_id = chat::get_chat_msgs(&bob.ctx, chat.get_id(), 0x1, None)
|
||||
let msg_id = chat::get_chat_msgs(&bob.ctx, chat.get_id(), DC_GCM_ADDDAYMARKER)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
@@ -1209,7 +1201,7 @@ mod tests {
|
||||
Blocked::Yes,
|
||||
"Alice's 1:1 chat with Bob is not hidden"
|
||||
);
|
||||
let msg_id = chat::get_chat_msgs(&alice.ctx, alice_chatid, 0x1, None)
|
||||
let msg_id = chat::get_chat_msgs(&alice.ctx, alice_chatid, DC_GCM_ADDDAYMARKER)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
@@ -1254,7 +1246,7 @@ mod tests {
|
||||
Blocked::Yes,
|
||||
"Bob's 1:1 chat with Alice is not hidden"
|
||||
);
|
||||
for item in chat::get_chat_msgs(&bob.ctx, bob_chatid, 0x1, None)
|
||||
for item in chat::get_chat_msgs(&bob.ctx, bob_chatid, DC_GCM_ADDDAYMARKER)
|
||||
.await
|
||||
.unwrap()
|
||||
{
|
||||
@@ -1264,7 +1256,7 @@ mod tests {
|
||||
println!("msg {} text: {}", msg_id, text);
|
||||
}
|
||||
}
|
||||
let mut msg_iter = chat::get_chat_msgs(&bob.ctx, bob_chatid, 0x1, None)
|
||||
let mut msg_iter = chat::get_chat_msgs(&bob.ctx, bob_chatid, DC_GCM_ADDDAYMARKER)
|
||||
.await
|
||||
.unwrap()
|
||||
.into_iter();
|
||||
@@ -1298,7 +1290,6 @@ mod tests {
|
||||
let bob_chat = Chat::load_from_db(&bob.ctx, bob_chatid).await?;
|
||||
assert!(bob_chat.is_protected());
|
||||
assert!(bob_chat.typ == Chattype::Group);
|
||||
assert!(!bob.ctx.has_ongoing().await);
|
||||
|
||||
// On this "happy path", Alice and Bob get only a group-chat where all information are added to.
|
||||
// The one-to-one chats are used internally for the hidden handshake messages,
|
||||
|
||||
176
src/smtp.rs
176
src/smtp.rs
@@ -10,14 +10,19 @@ use async_smtp::smtp::response::{Category, Code, Detail};
|
||||
use async_smtp::{smtp, EmailAddress, ServerAddress};
|
||||
use async_std::task;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::constants::DC_LP_AUTH_OAUTH2;
|
||||
use crate::contact::{Contact, ContactId};
|
||||
use crate::events::EventType;
|
||||
use crate::login_param::{
|
||||
dc_build_tls, CertificateChecks, LoginParam, ServerLoginParam, Socks5Config,
|
||||
};
|
||||
use crate::message::Message;
|
||||
use crate::message::{self, MsgId};
|
||||
use crate::mimefactory::MimeFactory;
|
||||
use crate::oauth2::dc_get_oauth2_access_token;
|
||||
use crate::provider::Socket;
|
||||
use crate::sql;
|
||||
use crate::{context::Context, scheduler::connectivity::ConnectivityStore};
|
||||
|
||||
/// SMTP write and read timeout in seconds.
|
||||
@@ -82,6 +87,11 @@ impl Smtp {
|
||||
|
||||
/// Connect using configured parameters.
|
||||
pub async fn connect_configured(&mut self, context: &Context) -> Result<()> {
|
||||
if self.has_maybe_stale_connection().await {
|
||||
info!(context, "Closing stale connection");
|
||||
self.disconnect().await;
|
||||
}
|
||||
|
||||
if self.is_connected().await {
|
||||
return Ok(());
|
||||
}
|
||||
@@ -226,18 +236,13 @@ pub(crate) async fn smtp_send(
|
||||
|
||||
smtp.connectivity.set_working(context).await;
|
||||
|
||||
if smtp.has_maybe_stale_connection().await {
|
||||
info!(context, "Closing stale connection");
|
||||
smtp.disconnect().await;
|
||||
|
||||
if let Err(err) = smtp
|
||||
.connect_configured(context)
|
||||
.await
|
||||
.context("failed to reopen stale SMTP connection")
|
||||
{
|
||||
smtp.last_send_error = Some(format!("{:#}", err));
|
||||
return SendResult::Retry;
|
||||
}
|
||||
if let Err(err) = smtp
|
||||
.connect_configured(context)
|
||||
.await
|
||||
.context("Failed to open SMTP connection")
|
||||
{
|
||||
smtp.last_send_error = Some(format!("{:#}", err));
|
||||
return SendResult::Retry;
|
||||
}
|
||||
|
||||
let send_result = smtp
|
||||
@@ -479,7 +484,7 @@ pub(crate) async fn send_msg_to_smtp(
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to send all messages currently in `smtp` table.
|
||||
/// Tries to send all messages currently in `smtp` and `smtp_mdns` tables.
|
||||
///
|
||||
/// Logs and ignores SMTP errors to ensure that a single SMTP message constantly failing to be sent
|
||||
/// does not block other messages in the queue from being sent.
|
||||
@@ -510,5 +515,150 @@ pub(crate) async fn send_smtp_messages(context: &Context, connection: &mut Smtp)
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
match send_mdn(context, connection).await {
|
||||
Err(err) => {
|
||||
info!(context, "Failed to send MDNs over SMTP: {:#}.", err);
|
||||
success = false;
|
||||
break;
|
||||
}
|
||||
Ok(false) => {
|
||||
break;
|
||||
}
|
||||
Ok(true) => {}
|
||||
}
|
||||
}
|
||||
Ok(success)
|
||||
}
|
||||
|
||||
/// Tries to send MDN for message `msg_id` to `contact_id`.
|
||||
///
|
||||
/// Attempts to aggregate additional MDNs for `contact_id` into sent MDN.
|
||||
///
|
||||
/// On failure returns an error without removing any `smtp_mdns` entries, the caller is responsible
|
||||
/// for removing the corresponding entry to prevent endless loop in case the entry is invalid, e.g.
|
||||
/// points to non-existent message or contact.
|
||||
async fn send_mdn_msg_id(
|
||||
context: &Context,
|
||||
msg_id: MsgId,
|
||||
contact_id: ContactId,
|
||||
smtp: &mut Smtp,
|
||||
) -> Result<()> {
|
||||
let contact = Contact::load_from_db(context, contact_id).await?;
|
||||
if contact.is_blocked() {
|
||||
return Err(format_err!("Contact is blocked"));
|
||||
}
|
||||
|
||||
// Try to aggregate additional MDNs into this MDN.
|
||||
let (additional_msg_ids, additional_rfc724_mids): (Vec<MsgId>, Vec<String>) = context
|
||||
.sql
|
||||
.query_map(
|
||||
"SELECT msg_id, rfc724_mid
|
||||
FROM smtp_mdns
|
||||
WHERE from_id=? AND msg_id!=?",
|
||||
paramsv![contact_id, msg_id],
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let rfc724_mid: String = row.get(1)?;
|
||||
Ok((msg_id, rfc724_mid))
|
||||
},
|
||||
|rows| rows.collect::<Result<Vec<_>, _>>().map_err(Into::into),
|
||||
)
|
||||
.await?
|
||||
.into_iter()
|
||||
.unzip();
|
||||
|
||||
let msg = Message::load_from_db(context, msg_id).await?;
|
||||
let mimefactory = MimeFactory::from_mdn(context, &msg, additional_rfc724_mids).await?;
|
||||
let rendered_msg = mimefactory.render(context).await?;
|
||||
let body = rendered_msg.message;
|
||||
|
||||
let addr = contact.get_addr();
|
||||
let recipient = async_smtp::EmailAddress::new(addr.to_string())
|
||||
.map_err(|err| format_err!("invalid recipient: {} {:?}", addr, err))?;
|
||||
let recipients = vec![recipient];
|
||||
|
||||
match smtp_send(context, &recipients, &body, smtp, msg_id, 0).await {
|
||||
SendResult::Success => {
|
||||
info!(context, "Successfully sent MDN for {}", msg_id);
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp_mdns WHERE msg_id = ?", paramsv![msg_id])
|
||||
.await?;
|
||||
if !additional_msg_ids.is_empty() {
|
||||
let q = format!(
|
||||
"DELETE FROM smtp_mdns WHERE msg_id IN({})",
|
||||
sql::repeat_vars(additional_msg_ids.len())
|
||||
);
|
||||
context
|
||||
.sql
|
||||
.execute(q, rusqlite::params_from_iter(additional_msg_ids))
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
SendResult::Retry => {
|
||||
info!(
|
||||
context,
|
||||
"Temporary SMTP failure while sending an MDN for {}", msg_id
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
SendResult::Failure(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
/// Tries to send a single MDN. Returns false if there are no MDNs to send.
|
||||
async fn send_mdn(context: &Context, smtp: &mut Smtp) -> Result<bool> {
|
||||
let mdns_enabled = context.get_config_bool(Config::MdnsEnabled).await?;
|
||||
if !mdns_enabled {
|
||||
// User has disabled MDNs.
|
||||
context.sql.execute("DELETE FROM smtp_mdns", []).await?;
|
||||
return Ok(false);
|
||||
}
|
||||
info!(context, "Sending MDNs");
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp_mdns WHERE retries > 6", [])
|
||||
.await?;
|
||||
let msg_row = match context
|
||||
.sql
|
||||
.query_row_optional(
|
||||
"SELECT msg_id, from_id FROM smtp_mdns ORDER BY retries LIMIT 1",
|
||||
[],
|
||||
|row| {
|
||||
let msg_id: MsgId = row.get(0)?;
|
||||
let from_id: ContactId = row.get(1)?;
|
||||
Ok((msg_id, from_id))
|
||||
},
|
||||
)
|
||||
.await?
|
||||
{
|
||||
Some(msg_row) => msg_row,
|
||||
None => return Ok(false),
|
||||
};
|
||||
let (msg_id, contact_id) = msg_row;
|
||||
|
||||
context
|
||||
.sql
|
||||
.execute(
|
||||
"UPDATE smtp_mdns SET retries=retries+1 WHERE msg_id=?",
|
||||
paramsv![msg_id],
|
||||
)
|
||||
.await
|
||||
.context("failed to update MDN retries count")?;
|
||||
|
||||
if let Err(err) = send_mdn_msg_id(context, msg_id, contact_id, smtp).await {
|
||||
// If there is an error, for example there is no message corresponding to the msg_id in the
|
||||
// database, do not try to send this MDN again.
|
||||
context
|
||||
.sql
|
||||
.execute("DELETE FROM smtp_mdns WHERE msg_id = ?", paramsv![msg_id])
|
||||
.await?;
|
||||
Err(err)
|
||||
} else {
|
||||
Ok(true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -578,7 +578,7 @@ rfc724_mid TEXT NOT NULL, -- Message-ID
|
||||
mime TEXT NOT NULL, -- SMTP payload
|
||||
msg_id INTEGER NOT NULL, -- ID of the message in `msgs` table
|
||||
recipients TEXT NOT NULL, -- List of recipients separated by space
|
||||
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the messsage
|
||||
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send the message
|
||||
);
|
||||
CREATE INDEX smtp_messageid ON imap(rfc724_mid);
|
||||
"#,
|
||||
@@ -624,6 +624,19 @@ CREATE INDEX smtp_messageid ON imap(rfc724_mid);
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
if dbversion < 90 {
|
||||
info!(context, "[migration] v90");
|
||||
sql.execute_migration(
|
||||
r#"CREATE TABLE smtp_mdns (
|
||||
msg_id INTEGER NOT NULL, -- id of the message in msgs table which requested MDN
|
||||
from_id INTEGER NOT NULL, -- id of the contact that sent the message, MDN destination
|
||||
rfc724_mid TEXT NOT NULL, -- Message-ID header
|
||||
retries INTEGER NOT NULL DEFAULT 0 -- Number of failed attempts to send MDN
|
||||
);"#,
|
||||
90,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
|
||||
Ok((
|
||||
recalc_fingerprints,
|
||||
|
||||
@@ -73,10 +73,10 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Encrypted message"))]
|
||||
EncryptedMsg = 24,
|
||||
|
||||
#[strum(props(fallback = "End-to-end encryption available."))]
|
||||
#[strum(props(fallback = "End-to-end encryption available"))]
|
||||
E2eAvailable = 25,
|
||||
|
||||
#[strum(props(fallback = "No encryption."))]
|
||||
#[strum(props(fallback = "No encryption"))]
|
||||
EncrNone = 28,
|
||||
|
||||
#[strum(props(fallback = "This message was encrypted for another setup."))]
|
||||
@@ -94,7 +94,7 @@ pub enum StockMessage {
|
||||
#[strum(props(fallback = "Group image deleted."))]
|
||||
MsgGrpImgDeleted = 33,
|
||||
|
||||
#[strum(props(fallback = "End-to-end encryption preferred."))]
|
||||
#[strum(props(fallback = "End-to-end encryption preferred"))]
|
||||
E2ePreferred = 34,
|
||||
|
||||
#[strum(props(fallback = "%1$s verified."))]
|
||||
@@ -1300,13 +1300,13 @@ mod tests {
|
||||
};
|
||||
|
||||
// delete self-talk first; this adds a message to device-chat about how self-talk can be restored
|
||||
let device_chat_msgs_before = chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
let device_chat_msgs_before = chat::get_chat_msgs(&t, device_chat_id, 0)
|
||||
.await
|
||||
.unwrap()
|
||||
.len();
|
||||
self_talk_id.delete(&t).await.ok();
|
||||
assert_eq!(
|
||||
chat::get_chat_msgs(&t, device_chat_id, 0, None)
|
||||
chat::get_chat_msgs(&t, device_chat_id, 0)
|
||||
.await
|
||||
.unwrap()
|
||||
.len(),
|
||||
|
||||
@@ -488,7 +488,7 @@ mod tests {
|
||||
let chat = Chat::load_from_db(&alice, chat_id).await?;
|
||||
assert!(chat.is_self_talk());
|
||||
assert_eq!(Chatlist::try_load(&alice, 0, None, None).await?.len(), 1);
|
||||
let msgs = chat::get_chat_msgs(&alice, chat_id, 0, None).await?;
|
||||
let msgs = chat::get_chat_msgs(&alice, chat_id, 0).await?;
|
||||
assert_eq!(msgs.len(), 0);
|
||||
|
||||
// let alice's other device receive and execute the sync message,
|
||||
|
||||
@@ -22,7 +22,7 @@ use crate::chat::{self, Chat, ChatId};
|
||||
use crate::chatlist::Chatlist;
|
||||
use crate::config::Config;
|
||||
use crate::constants::Chattype;
|
||||
use crate::constants::{DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1};
|
||||
use crate::constants::{DC_GCM_ADDDAYMARKER, DC_MSG_ID_DAYMARKER};
|
||||
use crate::contact::{Contact, ContactId, Modifier, Origin};
|
||||
use crate::context::Context;
|
||||
use crate::dc_receive_imf::dc_receive_imf;
|
||||
@@ -387,9 +387,7 @@ impl TestContext {
|
||||
///
|
||||
/// Panics on errors or if the most recent message is a marker.
|
||||
pub async fn get_last_msg_in(&self, chat_id: ChatId) -> Message {
|
||||
let msgs = chat::get_chat_msgs(&self.ctx, chat_id, 0, None)
|
||||
.await
|
||||
.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&self.ctx, chat_id, 0).await.unwrap();
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs.last().unwrap() {
|
||||
msg_id
|
||||
} else {
|
||||
@@ -511,12 +509,13 @@ impl TestContext {
|
||||
#[allow(dead_code)]
|
||||
#[allow(clippy::indexing_slicing)]
|
||||
pub async fn print_chat(&self, chat_id: ChatId) {
|
||||
let msglist = chat::get_chat_msgs(self, chat_id, 0x1, None).await.unwrap();
|
||||
let msglist = chat::get_chat_msgs(self, chat_id, DC_GCM_ADDDAYMARKER)
|
||||
.await
|
||||
.unwrap();
|
||||
let msglist: Vec<MsgId> = msglist
|
||||
.into_iter()
|
||||
.map(|x| match x {
|
||||
ChatItem::Message { msg_id } => msg_id,
|
||||
ChatItem::Marker1 => MsgId::new(DC_MSG_ID_MARKER1),
|
||||
ChatItem::DayMarker { .. } => MsgId::new(DC_MSG_ID_DAYMARKER),
|
||||
})
|
||||
.collect();
|
||||
@@ -773,7 +772,7 @@ pub(crate) async fn get_chat_msg(
|
||||
index: usize,
|
||||
asserted_msgs_count: usize,
|
||||
) -> Message {
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0, None).await.unwrap();
|
||||
let msgs = chat::get_chat_msgs(&t.ctx, chat_id, 0).await.unwrap();
|
||||
assert_eq!(msgs.len(), asserted_msgs_count);
|
||||
let msg_id = if let ChatItem::Message { msg_id } = msgs[index] {
|
||||
msg_id
|
||||
|
||||
100
src/webxdc.rs
100
src/webxdc.rs
@@ -51,6 +51,7 @@ const WEBXDC_RECEIVING_LIMIT: u64 = 4194304;
|
||||
struct WebxdcManifest {
|
||||
name: Option<String>,
|
||||
min_api: Option<u32>,
|
||||
source_code_url: Option<String>,
|
||||
}
|
||||
|
||||
/// Parsed information from WebxdcManifest and fallbacks.
|
||||
@@ -58,7 +59,9 @@ struct WebxdcManifest {
|
||||
pub struct WebxdcInfo {
|
||||
pub name: String,
|
||||
pub icon: String,
|
||||
pub document: String,
|
||||
pub summary: String,
|
||||
pub source_code_url: String,
|
||||
}
|
||||
|
||||
/// Status Update ID.
|
||||
@@ -114,6 +117,9 @@ pub(crate) struct StatusUpdateItem {
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
info: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
document: Option<String>,
|
||||
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
summary: Option<String>,
|
||||
}
|
||||
@@ -189,8 +195,8 @@ impl Context {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Takes an update-json as `{payload: PAYLOAD}` (or legacy `PAYLOAD`)
|
||||
/// writes it to the database and handles events, info-messages and summary.
|
||||
/// Takes an update-json as `{payload: PAYLOAD}`
|
||||
/// writes it to the database and handles events, info-messages, document name and summary.
|
||||
async fn create_status_update_record(
|
||||
&self,
|
||||
instance: &mut Message,
|
||||
@@ -210,6 +216,7 @@ impl Context {
|
||||
| MessageState::OutDraft => StatusUpdateItem {
|
||||
payload: item.payload,
|
||||
info: None, // no info-messages in draft mode
|
||||
document: item.document,
|
||||
summary: item.summary,
|
||||
},
|
||||
_ => item,
|
||||
@@ -232,17 +239,33 @@ impl Context {
|
||||
.await?;
|
||||
}
|
||||
|
||||
let mut param_changed = false;
|
||||
|
||||
if let Some(ref document) = status_update_item.document {
|
||||
if instance
|
||||
.param
|
||||
.update_timestamp(Param::WebxdcDocumentTimestamp, timestamp)?
|
||||
{
|
||||
instance.param.set(Param::WebxdcDocument, document);
|
||||
param_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref summary) = status_update_item.summary {
|
||||
if instance
|
||||
.param
|
||||
.update_timestamp(Param::WebxdcSummaryTimestamp, timestamp)?
|
||||
{
|
||||
instance.param.set(Param::WebxdcSummary, summary);
|
||||
instance.update_param(self).await;
|
||||
self.emit_msgs_changed(instance.chat_id, instance.id);
|
||||
param_changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if param_changed {
|
||||
instance.update_param(self).await;
|
||||
self.emit_msgs_changed(instance.chat_id, instance.id);
|
||||
}
|
||||
|
||||
let rowid = self
|
||||
.sql
|
||||
.insert(
|
||||
@@ -538,12 +561,14 @@ impl Message {
|
||||
WebxdcManifest {
|
||||
name: None,
|
||||
min_api: None,
|
||||
source_code_url: None,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
WebxdcManifest {
|
||||
name: None,
|
||||
min_api: None,
|
||||
source_code_url: None,
|
||||
}
|
||||
};
|
||||
|
||||
@@ -568,11 +593,21 @@ impl Message {
|
||||
} else {
|
||||
WEBXDC_DEFAULT_ICON.to_string()
|
||||
},
|
||||
document: self
|
||||
.param
|
||||
.get(Param::WebxdcDocument)
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
summary: self
|
||||
.param
|
||||
.get(Param::WebxdcSummary)
|
||||
.unwrap_or_default()
|
||||
.to_string(),
|
||||
source_code_url: if let Some(url) = manifest.source_code_url {
|
||||
url
|
||||
} else {
|
||||
"".to_string()
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1343,6 +1378,21 @@ sth_for_the = "future""#
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_parse_webxdc_manifest_source_code_url() -> Result<()> {
|
||||
let result = parse_webxdc_manifest(r#"source_code_url = 3"#.as_bytes()).await;
|
||||
assert!(result.is_err());
|
||||
|
||||
let manifest =
|
||||
parse_webxdc_manifest(r#"source_code_url = "https://foo.bar""#.as_bytes()).await?;
|
||||
assert_eq!(
|
||||
manifest.source_code_url,
|
||||
Some("https://foo.bar".to_string())
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_webxdc_min_api_too_large() -> Result<()> {
|
||||
let t = TestContext::new_alice().await;
|
||||
@@ -1512,6 +1562,48 @@ sth_for_the = "future""#
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_webxdc_document_name() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
let bob = TestContext::new_bob().await;
|
||||
|
||||
// Alice creates an webxdc instance and updates document name
|
||||
let alice_chat = alice.create_chat(&bob).await;
|
||||
let alice_instance = send_webxdc_instance(&alice, alice_chat.id).await?;
|
||||
let sent_instance = &alice.pop_sent_msg().await;
|
||||
let info = alice_instance.get_webxdc_info(&alice).await?;
|
||||
assert_eq!(info.document, "".to_string());
|
||||
assert_eq!(info.summary, "".to_string());
|
||||
|
||||
alice
|
||||
.send_webxdc_status_update(
|
||||
alice_instance.id,
|
||||
r#"{"document":"my file", "payload":1337}"#,
|
||||
"descr",
|
||||
)
|
||||
.await?;
|
||||
let sent_update1 = &alice.pop_sent_msg().await;
|
||||
let info = Message::load_from_db(&alice, alice_instance.id)
|
||||
.await?
|
||||
.get_webxdc_info(&alice)
|
||||
.await?;
|
||||
assert_eq!(info.document, "my file".to_string());
|
||||
assert_eq!(info.summary, "".to_string());
|
||||
|
||||
// Bob receives the updates
|
||||
bob.recv_msg(sent_instance).await;
|
||||
let bob_instance = bob.get_last_msg().await;
|
||||
bob.recv_msg(sent_update1).await;
|
||||
let info = Message::load_from_db(&bob, bob_instance.id)
|
||||
.await?
|
||||
.get_webxdc_info(&bob)
|
||||
.await?;
|
||||
assert_eq!(info.document, "my file".to_string());
|
||||
assert_eq!(info.summary, "".to_string());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[async_std::test]
|
||||
async fn test_webxdc_info_msg() -> Result<()> {
|
||||
let alice = TestContext::new_alice().await;
|
||||
|
||||
78
test-data/message/google-workspace-mixed-up.eml
Normal file
78
test-data/message/google-workspace-mixed-up.eml
Normal file
@@ -0,0 +1,78 @@
|
||||
Subject: ...
|
||||
MIME-Version: 1.0
|
||||
Date: Tue, 10 May 2022 08:24:11 +0000
|
||||
Chat-Version: 1.0
|
||||
Message-ID: <foobar@example.net>
|
||||
To: Bob <bob@example.net>
|
||||
From: <alice@example.org>
|
||||
Content-Type: multipart/mixed; boundary="000000000000b4907a05dea40c88"
|
||||
|
||||
--000000000000b4907a05dea40c88
|
||||
Content-Type: text/plain; charset="UTF-8"
|
||||
Content-Disposition: inline
|
||||
|
||||
|
||||
--
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
--000000000000b4907a05dea40c88
|
||||
Content-Type: multipart/encrypted; protocol="application/pgp-encrypted";
|
||||
boundary="bi1vjiRd9u4EaEuno1ejliJactdnFl"
|
||||
|
||||
|
||||
--bi1vjiRd9u4EaEuno1ejliJactdnFl
|
||||
Content-Type: application/pgp-encrypted
|
||||
Content-Description: PGP/MIME version identification
|
||||
|
||||
Version: 1
|
||||
|
||||
|
||||
--bi1vjiRd9u4EaEuno1ejliJactdnFl
|
||||
Content-Type: application/octet-stream; name="encrypted.asc"
|
||||
Content-Description: OpenPGP encrypted message
|
||||
Content-Disposition: inline; filename="encrypted.asc";
|
||||
|
||||
-----BEGIN PGP MESSAGE-----
|
||||
|
||||
wV4D5tq63hTeebASAQdAt2c3rVUh+l0Ps7/Je83NaA7M6HsobtfMueqLUBaeancw0rRAo7PbLDLL
|
||||
cVX3SiPw6qqZyD99JZEgxZJFWM2GVILGqdvJFl11OKqXUDbzRgq6wcBMA+PY3JvEjuMiAQf6An2O
|
||||
xxjJsLgY3Ys6Ndqm8Tqp0XxK3gQuj5Vqpgd7Qv+57psL5jLHc46RxUR/txlY3Kay3yITG82iDvi4
|
||||
fbpkes7/t8eWOrtGdyPVokhfekuCLBoF24F4tEYBsumcurkNDqY1l+dxMzGB9goQWiVOUK3n+IV8
|
||||
fWPTazXTxO5o0VbCFU6RklpW07JEQUrmTzc+cwlIMhttU+h9rkfu8lm+9+KpI8GOHGV3RSCfZ1ns
|
||||
PiZL2xgJsTXAb7dF4vaAWozS7BFfxGZ1DknrySGMUBV3nmDjy/na5YiOqe/PWaZE19LcYEUdR6K5
|
||||
AFyifXDAwi0EoMe9w+aFWqnvuOkPWnhTVNLEPAFlODnAMgqeFMfHCiIrRI/UcA/NUNuY/MCFUC17
|
||||
aAw4Gl4v/pGRnVU3H+4KhW7AqNuqXQC0SpqZDuLEfr5DqUtd7at9TJh+n3kACs7sMzj3pLmZwBcg
|
||||
HddQoI35SuiLQwa79Ws/BwwSPKjRNYcKjwrjuG+k0gk+x5vd9PfUIX1ypatyJC5ZeIpFUiqPZYlg
|
||||
RCzYaWkGvvSFKIOrEWHMcUaP1p51L3n4Bc8UjVcvoeXjD2w5/SzbQ9/gp8Pno+lk1F1StDOQcRGw
|
||||
wzlKzw9KyznRCXtBtnGqgjr1gW2c1nt3BDBqq4KKTaf64eorkWOe29Qwk7jWkh+4HOe9uYd4raU3
|
||||
sLSY/LRSbYpJnNVsympMqPYopr7pO5W7sgqU1VFtfdCVZfzgvXi1USgnqQ++2BA253nrN203ZERL
|
||||
sHwWPIjeo5kULPqV7tUfU0goc7uerEFeFjJOg+Z1ZNU9/fhfJYoJTbo+2Kd6v93PPPgGzxeAU+zL
|
||||
in4yDAAJB9yJzkbVL83G7yfJ+3J5h+19aTc6XSlkXzNyLmQvTKFqDdq2SHooAlG7UJoE6vRK+mDz
|
||||
vbND9KbAAtQ4aQp10OYNyb+ZSXiwsKrgxMP3FE3j6Ui7Q9Fp3GgJC5SR0gTcGwqRWODgQau8E26r
|
||||
ukYKlB6XJ9tPAf2BwXeqwiQ3QU1704BzbO5G3tby9TpWqnAdtEfT2LdhllrwQmPWo+lNNWf1oLWu
|
||||
ylhJ1yEWETzeClDFxeyAoehJLZImlISQQsEoEPxCqHZ60o9x6ANto6xv3CIbu0WziA2A6R7tweBi
|
||||
mCAsyZdVCL2gg2nw+UWUyv6baTDpkxtKJOvYZeyzR0TH6KExRgeKjBrWPuHxJ7b+e70/DLvfNg+x
|
||||
Q6pulf+LWDKgZ9bGCZWbutp2uFyvdW+RdJXXXmhSZ3nrhusw/PVdGeQz+3N6LK3yiVOcvLeyNqGW
|
||||
/yYST6Rmqen0/JQPDDdKh4JjmLnJ/SmPTDOCD29uB03tCDDU2mzOUUncJWURE3jmJlKGGoOq4Ar9
|
||||
W03ud3E1ks/ZXk+aqz3jQ354cqSampZcxqX90esibuV/guUI3u0N3ah+FW1IfRhP2xJ36SIzc1lu
|
||||
Bs/jehRDJ9/BSFH+lHRftcYoGjNNFzl7Hx4me8EDdfhzX0HXNUZhVYJlFktdr1cjhPNzlxlnCL8b
|
||||
MgERav2VKFBvW0LR4Mm+trtbFU1ajybVihk7R56yJ/itnTHd3BxR7s8sRsG/6a8d2QiKjfNHBU05
|
||||
KEATHBFwTz3WWBbtBMN8fmIg8g2MrOfjcaHoTAgRJVr0rf+ww+KyaI8ZsraB+KTzXk+iVegNaUe/
|
||||
CiLI+Yl9ePNkFFbi4MyrY0ujXM6zRp7nbUlDewzGpI4LTyyAQ9IUqkCnAi0k7AkM1BIp8z1wxWlW
|
||||
JRAnxGSzxgibYLZ9f/fd9vBAiYA1ZVsuZTN2iUtt2/VJr2K7zPHwgO4j2OLtR4DKazCd7IlrArRH
|
||||
BfawosWYQ7cQJyo/+wxjXccvHVrZRn8vBvmFWdKz9mi1wC1HYyLeMJwYpaPsK79TRedA34pQSuAa
|
||||
QkAO79MxOVnknYS8pEGxrwD9l9vxrlZEllnFtG+QJeXsZgMIjwCaByJs7I3skUAHcuimN1X8htU2
|
||||
ofVNpLp9SUsrtXbFp89Dxiuflj10VvcLGU2AjSsUtjEpPl0nobeJmA3RzFxJZ61RG+E=
|
||||
=dcQr
|
||||
-----END PGP MESSAGE-----
|
||||
|
||||
|
||||
--bi1vjiRd9u4EaEuno1ejliJactdnFl--
|
||||
|
||||
|
||||
--000000000000b4907a05dea40c88--
|
||||
Reference in New Issue
Block a user