mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 15:42:10 +03:00
Compare commits
5 Commits
v2.31.0
...
link2xt/sq
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5820c4ce95 | ||
|
|
12ba33d9d4 | ||
|
|
60a7bbc9b5 | ||
|
|
e9f434b562 | ||
|
|
2423cb8175 |
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
35
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,35 +0,0 @@
|
|||||||
---
|
|
||||||
name: Bug report
|
|
||||||
about: Report something that isn't working.
|
|
||||||
title: ''
|
|
||||||
assignees: ''
|
|
||||||
labels: bug
|
|
||||||
---
|
|
||||||
|
|
||||||
<!--
|
|
||||||
This is the chatmail core's bug report tracker.
|
|
||||||
For Delta Chat feature requests and support, please go to the forum: https://support.delta.chat
|
|
||||||
Please fill out as much of this form as you can (leaving out stuff that is not applicable is ok).
|
|
||||||
-->
|
|
||||||
|
|
||||||
- Operating System (Linux/Mac/Windows/iOS/Android):
|
|
||||||
- Core Version:
|
|
||||||
- Client Version:
|
|
||||||
|
|
||||||
## Expected behavior
|
|
||||||
|
|
||||||
*What did you try to achieve?*
|
|
||||||
|
|
||||||
## Actual behavior
|
|
||||||
|
|
||||||
*What happened instead?*
|
|
||||||
|
|
||||||
### Steps to reproduce the problem
|
|
||||||
|
|
||||||
1.
|
|
||||||
2.
|
|
||||||
|
|
||||||
### Screenshots
|
|
||||||
|
|
||||||
### Logs
|
|
||||||
|
|
||||||
11
.github/dependabot.yml
vendored
11
.github/dependabot.yml
vendored
@@ -7,14 +7,3 @@ updates:
|
|||||||
commit-message:
|
commit-message:
|
||||||
prefix: "chore(cargo)"
|
prefix: "chore(cargo)"
|
||||||
open-pull-requests-limit: 50
|
open-pull-requests-limit: 50
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
|
|
||||||
# Keep GitHub Actions up to date.
|
|
||||||
# <https://docs.github.com/en/code-security/dependabot/working-with-dependabot/keeping-your-actions-up-to-date-with-dependabot>
|
|
||||||
- package-ecosystem: "github-actions"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "weekly"
|
|
||||||
cooldown:
|
|
||||||
default-days: 7
|
|
||||||
|
|||||||
167
.github/workflows/ci.yml
vendored
167
.github/workflows/ci.yml
vendored
@@ -16,50 +16,38 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: -Dwarnings
|
RUSTFLAGS: -Dwarnings
|
||||||
RUST_VERSION: 1.91.0
|
|
||||||
|
|
||||||
# Minimum Supported Rust Version
|
|
||||||
MSRV: 1.88.0
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
lint_rust:
|
lint_rust:
|
||||||
name: Lint Rust
|
name: Lint Rust
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
env:
|
||||||
|
RUSTUP_TOOLCHAIN: 1.77.1
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
|
||||||
- name: Install rustfmt and clippy
|
- name: Install rustfmt and clippy
|
||||||
run: rustup toolchain install $RUST_VERSION --profile minimal --component rustfmt --component clippy
|
run: rustup toolchain install $RUSTUP_TOOLCHAIN --profile minimal --component rustfmt --component clippy
|
||||||
- run: rustup override set $RUST_VERSION
|
|
||||||
shell: bash
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
- name: Run rustfmt
|
- name: Run rustfmt
|
||||||
run: cargo fmt --all -- --check
|
run: cargo fmt --all -- --check
|
||||||
- name: Run clippy
|
- name: Run clippy
|
||||||
run: scripts/clippy.sh
|
run: scripts/clippy.sh
|
||||||
- name: Check with all features
|
- name: Check
|
||||||
run: cargo check --workspace --all-targets --all-features
|
run: cargo check --workspace --all-targets --all-features
|
||||||
- name: Check with only default features
|
|
||||||
run: cargo check --all-targets
|
|
||||||
|
|
||||||
cargo_deny:
|
cargo_deny:
|
||||||
name: cargo deny
|
name: cargo deny
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
- uses: EmbarkStudios/cargo-deny-action@v1
|
||||||
- uses: EmbarkStudios/cargo-deny-action@v2
|
|
||||||
with:
|
with:
|
||||||
arguments: --all-features --workspace
|
arguments: --all-features --workspace
|
||||||
command: check
|
command: check
|
||||||
@@ -68,28 +56,22 @@ jobs:
|
|||||||
provider_database:
|
provider_database:
|
||||||
name: Check provider database
|
name: Check provider database
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
|
||||||
- name: Install rustfmt
|
|
||||||
run: rustup component add --toolchain stable-x86_64-unknown-linux-gnu rustfmt
|
|
||||||
- name: Check provider database
|
- name: Check provider database
|
||||||
run: scripts/update-provider-database.sh
|
run: scripts/update-provider-database.sh
|
||||||
|
|
||||||
docs:
|
docs:
|
||||||
name: Rust doc comments
|
name: Rust doc comments
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
|
||||||
env:
|
env:
|
||||||
RUSTDOCFLAGS: -Dwarnings
|
RUSTDOCFLAGS: -Dwarnings
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
- name: Rustdoc
|
- name: Rustdoc
|
||||||
@@ -101,55 +83,32 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
rust: latest
|
rust: 1.77.1
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
rust: latest
|
rust: 1.77.1
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
rust: latest
|
rust: 1.77.1
|
||||||
|
|
||||||
# Minimum Supported Rust Version
|
# Minimum Supported Rust Version = 1.77.0
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
rust: minimum
|
rust: 1.77.0
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- run:
|
- uses: actions/checkout@v4
|
||||||
echo "RUSTUP_TOOLCHAIN=$MSRV" >> $GITHUB_ENV
|
|
||||||
shell: bash
|
|
||||||
if: matrix.rust == 'minimum'
|
|
||||||
- run:
|
|
||||||
echo "RUSTUP_TOOLCHAIN=$RUST_VERSION" >> $GITHUB_ENV
|
|
||||||
shell: bash
|
|
||||||
if: matrix.rust == 'latest'
|
|
||||||
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install Rust ${{ matrix.rust }}
|
- name: Install Rust ${{ matrix.rust }}
|
||||||
run: rustup toolchain install --profile minimal $RUSTUP_TOOLCHAIN
|
run: rustup toolchain install --profile minimal ${{ matrix.rust }}
|
||||||
shell: bash
|
- run: rustup override set ${{ matrix.rust }}
|
||||||
- run: rustup override set $RUSTUP_TOOLCHAIN
|
|
||||||
shell: bash
|
|
||||||
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Install nextest
|
|
||||||
uses: taiki-e/install-action@v2
|
|
||||||
with:
|
|
||||||
tool: nextest
|
|
||||||
|
|
||||||
- name: Tests
|
- name: Tests
|
||||||
env:
|
env:
|
||||||
RUST_BACKTRACE: 1
|
RUST_BACKTRACE: 1
|
||||||
run: cargo nextest run --workspace --locked
|
run: cargo test --workspace
|
||||||
|
|
||||||
- name: Doc-Tests
|
|
||||||
env:
|
|
||||||
RUST_BACKTRACE: 1
|
|
||||||
run: cargo test --workspace --locked --doc
|
|
||||||
|
|
||||||
- name: Test cargo vendor
|
- name: Test cargo vendor
|
||||||
run: cargo vendor
|
run: cargo vendor
|
||||||
@@ -160,21 +119,19 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest]
|
os: [ubuntu-latest, macos-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Build C library
|
- name: Build C library
|
||||||
run: cargo build -p deltachat_ffi
|
run: cargo build -p deltachat_ffi --features jsonrpc
|
||||||
|
|
||||||
- name: Upload C library
|
- name: Upload C library
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-libdeltachat.a
|
name: ${{ matrix.os }}-libdeltachat.a
|
||||||
path: target/debug/libdeltachat.a
|
path: target/debug/libdeltachat.a
|
||||||
@@ -186,12 +143,10 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest, windows-latest]
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Cache rust cargo artifacts
|
- name: Cache rust cargo artifacts
|
||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
@@ -200,7 +155,7 @@ jobs:
|
|||||||
run: cargo build -p deltachat-rpc-server
|
run: cargo build -p deltachat-rpc-server
|
||||||
|
|
||||||
- name: Upload deltachat-rpc-server
|
- name: Upload deltachat-rpc-server
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||||
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
|
path: ${{ matrix.os == 'windows-latest' && 'target/debug/deltachat-rpc-server.exe' || 'target/debug/deltachat-rpc-server' }}
|
||||||
@@ -209,12 +164,10 @@ jobs:
|
|||||||
python_lint:
|
python_lint:
|
||||||
name: Python lint
|
name: Python lint
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install tox
|
- name: Install tox
|
||||||
run: pip install tox
|
run: pip install tox
|
||||||
@@ -227,38 +180,6 @@ jobs:
|
|||||||
working-directory: deltachat-rpc-client
|
working-directory: deltachat-rpc-client
|
||||||
run: tox -e lint
|
run: tox -e lint
|
||||||
|
|
||||||
# mypy does not work with PyPy since mypy 1.19
|
|
||||||
# as it introduced native `librt` dependency
|
|
||||||
# that uses CPython internals.
|
|
||||||
# We only run mypy with CPython because of this.
|
|
||||||
cffi_python_mypy:
|
|
||||||
name: CFFI Python mypy
|
|
||||||
needs: ["c_library", "python_lint"]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Download libdeltachat.a
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: ubuntu-latest-libdeltachat.a
|
|
||||||
path: target/debug
|
|
||||||
|
|
||||||
- name: Install tox
|
|
||||||
run: pip install tox
|
|
||||||
|
|
||||||
- name: Run mypy
|
|
||||||
env:
|
|
||||||
DCC_RS_TARGET: debug
|
|
||||||
DCC_RS_DEV: ${{ github.workspace }}
|
|
||||||
working-directory: python
|
|
||||||
run: tox -e mypy
|
|
||||||
|
|
||||||
|
|
||||||
cffi_python_tests:
|
cffi_python_tests:
|
||||||
name: CFFI Python tests
|
name: CFFI Python tests
|
||||||
needs: ["c_library", "python_lint"]
|
needs: ["c_library", "python_lint"]
|
||||||
@@ -268,9 +189,9 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
# Currently used Rust version.
|
# Currently used Rust version.
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: 3.14
|
python: 3.12
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: 3.14
|
python: 3.12
|
||||||
|
|
||||||
# PyPy tests
|
# PyPy tests
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
@@ -278,28 +199,26 @@ jobs:
|
|||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: pypy3.10
|
python: pypy3.10
|
||||||
|
|
||||||
# Minimum Supported Python Version = 3.10
|
# Minimum Supported Python Version = 3.7
|
||||||
# This is the minimum version for which manylinux Python wheels are
|
# This is the minimum version for which manylinux Python wheels are
|
||||||
# built. Test it with minimum supported Rust version.
|
# built. Test it with minimum supported Rust version.
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: "3.10"
|
python: 3.7
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Download libdeltachat.a
|
- name: Download libdeltachat.a
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-libdeltachat.a
|
name: ${{ matrix.os }}-libdeltachat.a
|
||||||
path: target/debug
|
path: target/debug
|
||||||
|
|
||||||
- name: Install python
|
- name: Install python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
|
||||||
@@ -308,11 +227,11 @@ jobs:
|
|||||||
|
|
||||||
- name: Run python tests
|
- name: Run python tests
|
||||||
env:
|
env:
|
||||||
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
|
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
|
||||||
DCC_RS_TARGET: debug
|
DCC_RS_TARGET: debug
|
||||||
DCC_RS_DEV: ${{ github.workspace }}
|
DCC_RS_DEV: ${{ github.workspace }}
|
||||||
working-directory: python
|
working-directory: python
|
||||||
run: tox -e doc,py
|
run: tox -e mypy,doc,py
|
||||||
|
|
||||||
rpc_python_tests:
|
rpc_python_tests:
|
||||||
name: JSON-RPC Python tests
|
name: JSON-RPC Python tests
|
||||||
@@ -322,11 +241,11 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: 3.14
|
python: 3.12
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: 3.14
|
python: 3.12
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
python: 3.14
|
python: 3.12
|
||||||
|
|
||||||
# PyPy tests
|
# PyPy tests
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
@@ -334,20 +253,18 @@ jobs:
|
|||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: pypy3.10
|
python: pypy3.10
|
||||||
|
|
||||||
# Minimum Supported Python Version = 3.10
|
# Minimum Supported Python Version = 3.7
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: "3.10"
|
python: 3.7
|
||||||
|
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
timeout-minutes: 60
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install python
|
- name: Install python
|
||||||
uses: actions/setup-python@v6
|
uses: actions/setup-python@v5
|
||||||
with:
|
with:
|
||||||
python-version: ${{ matrix.python }}
|
python-version: ${{ matrix.python }}
|
||||||
|
|
||||||
@@ -355,7 +272,7 @@ jobs:
|
|||||||
run: pip install tox
|
run: pip install tox
|
||||||
|
|
||||||
- name: Download deltachat-rpc-server
|
- name: Download deltachat-rpc-server
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: ${{ matrix.os }}-deltachat-rpc-server
|
name: ${{ matrix.os }}-deltachat-rpc-server
|
||||||
path: target/debug
|
path: target/debug
|
||||||
@@ -375,6 +292,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Run deltachat-rpc-client tests
|
- name: Run deltachat-rpc-client tests
|
||||||
env:
|
env:
|
||||||
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
|
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
|
||||||
working-directory: deltachat-rpc-client
|
working-directory: deltachat-rpc-client
|
||||||
run: tox -e py
|
run: tox -e py
|
||||||
|
|||||||
371
.github/workflows/deltachat-rpc-server.yml
vendored
371
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -17,8 +17,6 @@ on:
|
|||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Build a version statically linked against musl libc
|
# Build a version statically linked against musl libc
|
||||||
# to avoid problems with glibc version incompatibility.
|
# to avoid problems with glibc version incompatibility.
|
||||||
@@ -30,46 +28,22 @@ jobs:
|
|||||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server binaries
|
- name: Build deltachat-rpc-server binaries
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||||
|
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}-linux
|
name: deltachat-rpc-server-${{ matrix.arch }}-linux
|
||||||
path: result/bin/deltachat-rpc-server
|
path: result/bin/deltachat-rpc-server
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build_linux_wheel:
|
|
||||||
name: Linux wheel
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
arch: [aarch64, armv7l, armv6l, i686, x86_64]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
persist-credentials: false
|
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server wheels
|
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
|
||||||
|
|
||||||
- name: Upload wheel
|
|
||||||
uses: actions/upload-artifact@v5
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}-linux-wheel
|
|
||||||
path: result/*.whl
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
build_windows:
|
build_windows:
|
||||||
name: Windows
|
name: Windows
|
||||||
strategy:
|
strategy:
|
||||||
@@ -78,46 +52,22 @@ jobs:
|
|||||||
arch: [win32, win64]
|
arch: [win32, win64]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server binaries
|
- name: Build deltachat-rpc-server binaries
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}
|
||||||
|
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}
|
name: deltachat-rpc-server-${{ matrix.arch }}
|
||||||
path: result/bin/deltachat-rpc-server.exe
|
path: result/bin/deltachat-rpc-server.exe
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build_windows_wheel:
|
|
||||||
name: Windows wheel
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
arch: [win32, win64]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
persist-credentials: false
|
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server wheels
|
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-wheel
|
|
||||||
|
|
||||||
- name: Upload wheel
|
|
||||||
uses: actions/upload-artifact@v5
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}-wheel
|
|
||||||
path: result/*.whl
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
build_macos:
|
build_macos:
|
||||||
name: macOS
|
name: macOS
|
||||||
strategy:
|
strategy:
|
||||||
@@ -127,10 +77,9 @@ jobs:
|
|||||||
|
|
||||||
runs-on: macos-latest
|
runs-on: macos-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Setup rust target
|
- name: Setup rust target
|
||||||
run: rustup target add ${{ matrix.arch }}-apple-darwin
|
run: rustup target add ${{ matrix.arch }}-apple-darwin
|
||||||
@@ -139,7 +88,7 @@ jobs:
|
|||||||
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
||||||
|
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}-macos
|
name: deltachat-rpc-server-${{ matrix.arch }}-macos
|
||||||
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
||||||
@@ -153,49 +102,25 @@ jobs:
|
|||||||
arch: [arm64-v8a, armeabi-v7a]
|
arch: [arm64-v8a, armeabi-v7a]
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server binaries
|
- name: Build deltachat-rpc-server binaries
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android
|
||||||
|
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}-android
|
name: deltachat-rpc-server-${{ matrix.arch }}-android
|
||||||
path: result/bin/deltachat-rpc-server
|
path: result/bin/deltachat-rpc-server
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build_android_wheel:
|
|
||||||
name: Android wheel
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
arch: [arm64-v8a, armeabi-v7a]
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
persist-credentials: false
|
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server wheels
|
|
||||||
run: nix build .#deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
|
||||||
|
|
||||||
- name: Upload wheel
|
|
||||||
uses: actions/upload-artifact@v5
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-${{ matrix.arch }}-android-wheel
|
|
||||||
path: result/*.whl
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Build wheels and upload binaries to the release
|
name: Build wheels and upload binaries to the release
|
||||||
needs: ["build_linux", "build_linux_wheel", "build_windows", "build_windows_wheel", "build_macos", "build_android", "build_android_wheel"]
|
needs: ["build_linux", "build_windows", "build_macos"]
|
||||||
environment:
|
environment:
|
||||||
name: pypi
|
name: pypi
|
||||||
url: https://pypi.org/p/deltachat-rpc-server
|
url: https://pypi.org/p/deltachat-rpc-server
|
||||||
@@ -204,132 +129,78 @@ jobs:
|
|||||||
contents: write
|
contents: write
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
|
|
||||||
- name: Download Linux aarch64 binary
|
- name: Download Linux aarch64 binary
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-aarch64-linux
|
name: deltachat-rpc-server-aarch64-linux
|
||||||
path: deltachat-rpc-server-aarch64-linux.d
|
path: deltachat-rpc-server-aarch64-linux.d
|
||||||
|
|
||||||
- name: Download Linux aarch64 wheel
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-aarch64-linux-wheel
|
|
||||||
path: deltachat-rpc-server-aarch64-linux-wheel.d
|
|
||||||
|
|
||||||
- name: Download Linux armv7l binary
|
- name: Download Linux armv7l binary
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armv7l-linux
|
name: deltachat-rpc-server-armv7l-linux
|
||||||
path: deltachat-rpc-server-armv7l-linux.d
|
path: deltachat-rpc-server-armv7l-linux.d
|
||||||
|
|
||||||
- name: Download Linux armv7l wheel
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-armv7l-linux-wheel
|
|
||||||
path: deltachat-rpc-server-armv7l-linux-wheel.d
|
|
||||||
|
|
||||||
- name: Download Linux armv6l binary
|
- name: Download Linux armv6l binary
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armv6l-linux
|
name: deltachat-rpc-server-armv6l-linux
|
||||||
path: deltachat-rpc-server-armv6l-linux.d
|
path: deltachat-rpc-server-armv6l-linux.d
|
||||||
|
|
||||||
- name: Download Linux armv6l wheel
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-armv6l-linux-wheel
|
|
||||||
path: deltachat-rpc-server-armv6l-linux-wheel.d
|
|
||||||
|
|
||||||
- name: Download Linux i686 binary
|
- name: Download Linux i686 binary
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-i686-linux
|
name: deltachat-rpc-server-i686-linux
|
||||||
path: deltachat-rpc-server-i686-linux.d
|
path: deltachat-rpc-server-i686-linux.d
|
||||||
|
|
||||||
- name: Download Linux i686 wheel
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-i686-linux-wheel
|
|
||||||
path: deltachat-rpc-server-i686-linux-wheel.d
|
|
||||||
|
|
||||||
- name: Download Linux x86_64 binary
|
- name: Download Linux x86_64 binary
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-x86_64-linux
|
name: deltachat-rpc-server-x86_64-linux
|
||||||
path: deltachat-rpc-server-x86_64-linux.d
|
path: deltachat-rpc-server-x86_64-linux.d
|
||||||
|
|
||||||
- name: Download Linux x86_64 wheel
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-x86_64-linux-wheel
|
|
||||||
path: deltachat-rpc-server-x86_64-linux-wheel.d
|
|
||||||
|
|
||||||
- name: Download Win32 binary
|
- name: Download Win32 binary
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-win32
|
name: deltachat-rpc-server-win32
|
||||||
path: deltachat-rpc-server-win32.d
|
path: deltachat-rpc-server-win32.d
|
||||||
|
|
||||||
- name: Download Win32 wheel
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-win32-wheel
|
|
||||||
path: deltachat-rpc-server-win32-wheel.d
|
|
||||||
|
|
||||||
- name: Download Win64 binary
|
- name: Download Win64 binary
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-win64
|
name: deltachat-rpc-server-win64
|
||||||
path: deltachat-rpc-server-win64.d
|
path: deltachat-rpc-server-win64.d
|
||||||
|
|
||||||
- name: Download Win64 wheel
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-win64-wheel
|
|
||||||
path: deltachat-rpc-server-win64-wheel.d
|
|
||||||
|
|
||||||
- name: Download macOS binary for x86_64
|
- name: Download macOS binary for x86_64
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-x86_64-macos
|
name: deltachat-rpc-server-x86_64-macos
|
||||||
path: deltachat-rpc-server-x86_64-macos.d
|
path: deltachat-rpc-server-x86_64-macos.d
|
||||||
|
|
||||||
- name: Download macOS binary for aarch64
|
- name: Download macOS binary for aarch64
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-aarch64-macos
|
name: deltachat-rpc-server-aarch64-macos
|
||||||
path: deltachat-rpc-server-aarch64-macos.d
|
path: deltachat-rpc-server-aarch64-macos.d
|
||||||
|
|
||||||
- name: Download Android binary for arm64-v8a
|
- name: Download Android binary for arm64-v8a
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-arm64-v8a-android
|
name: deltachat-rpc-server-arm64-v8a-android
|
||||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
path: deltachat-rpc-server-arm64-v8a-android.d
|
||||||
|
|
||||||
- name: Download Android wheel for arm64-v8a
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-arm64-v8a-android-wheel
|
|
||||||
path: deltachat-rpc-server-arm64-v8a-android-wheel.d
|
|
||||||
|
|
||||||
- name: Download Android binary for armeabi-v7a
|
- name: Download Android binary for armeabi-v7a
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-armeabi-v7a-android
|
name: deltachat-rpc-server-armeabi-v7a-android
|
||||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
path: deltachat-rpc-server-armeabi-v7a-android.d
|
||||||
|
|
||||||
- name: Download Android wheel for armeabi-v7a
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-armeabi-v7a-android-wheel
|
|
||||||
path: deltachat-rpc-server-armeabi-v7a-android-wheel.d
|
|
||||||
|
|
||||||
- name: Create bin/ directory
|
- name: Create bin/ directory
|
||||||
run: |
|
run: |
|
||||||
mkdir -p bin
|
mkdir -p bin
|
||||||
@@ -348,21 +219,34 @@ jobs:
|
|||||||
- name: List binaries
|
- name: List binaries
|
||||||
run: ls -l bin/
|
run: ls -l bin/
|
||||||
|
|
||||||
|
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
|
||||||
|
- name: Install python 3.12
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: 3.12
|
||||||
|
|
||||||
- name: Install wheel
|
- name: Install wheel
|
||||||
run: pip install wheel
|
run: pip install wheel
|
||||||
|
|
||||||
- name: Build deltachat-rpc-server Python wheels
|
- name: Build deltachat-rpc-server Python wheels and source package
|
||||||
run: |
|
run: |
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
mv deltachat-rpc-server-aarch64-linux-wheel.d/*.whl dist/
|
nix build .#deltachat-rpc-server-x86_64-linux-wheel
|
||||||
mv deltachat-rpc-server-armv7l-linux-wheel.d/*.whl dist/
|
cp result/*.whl dist/
|
||||||
mv deltachat-rpc-server-armv6l-linux-wheel.d/*.whl dist/
|
nix build .#deltachat-rpc-server-armv7l-linux-wheel
|
||||||
mv deltachat-rpc-server-i686-linux-wheel.d/*.whl dist/
|
cp result/*.whl dist/
|
||||||
mv deltachat-rpc-server-x86_64-linux-wheel.d/*.whl dist/
|
nix build .#deltachat-rpc-server-armv6l-linux-wheel
|
||||||
mv deltachat-rpc-server-win64-wheel.d/*.whl dist/
|
cp result/*.whl dist/
|
||||||
mv deltachat-rpc-server-win32-wheel.d/*.whl dist/
|
nix build .#deltachat-rpc-server-aarch64-linux-wheel
|
||||||
mv deltachat-rpc-server-arm64-v8a-android-wheel.d/*.whl dist/
|
cp result/*.whl dist/
|
||||||
mv deltachat-rpc-server-armeabi-v7a-android-wheel.d/*.whl dist/
|
nix build .#deltachat-rpc-server-i686-linux-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-win64-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-win32-wheel
|
||||||
|
cp result/*.whl dist/
|
||||||
|
nix build .#deltachat-rpc-server-source
|
||||||
|
cp result/*.tar.gz dist/
|
||||||
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
|
python3 scripts/wheel-rpc-server.py x86_64-darwin bin/deltachat-rpc-server-x86_64-macos
|
||||||
python3 scripts/wheel-rpc-server.py aarch64-darwin bin/deltachat-rpc-server-aarch64-macos
|
python3 scripts/wheel-rpc-server.py aarch64-darwin bin/deltachat-rpc-server-aarch64-macos
|
||||||
mv *.whl dist/
|
mv *.whl dist/
|
||||||
@@ -374,158 +258,11 @@ jobs:
|
|||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
REF_NAME: ${{ github.ref_name }}
|
|
||||||
run: |
|
run: |
|
||||||
gh release upload "$REF_NAME" \
|
gh release upload ${{ github.ref_name }} \
|
||||||
--repo ${{ github.repository }} \
|
--repo ${{ github.repository }} \
|
||||||
bin/* dist/*
|
bin/* dist/*
|
||||||
|
|
||||||
- name: Publish deltachat-rpc-server to PyPI
|
- name: Publish deltachat-rpc-client to PyPI
|
||||||
if: github.event_name == 'release'
|
if: github.event_name == 'release'
|
||||||
uses: pypa/gh-action-pypi-publish@release/v1
|
uses: pypa/gh-action-pypi-publish@release/v1
|
||||||
|
|
||||||
publish_npm_package:
|
|
||||||
name: Build & Publish npm prebuilds and deltachat-rpc-server
|
|
||||||
needs: ["build_linux", "build_windows", "build_macos"]
|
|
||||||
runs-on: "ubuntu-latest"
|
|
||||||
environment:
|
|
||||||
name: npm-stdio-rpc-server
|
|
||||||
url: https://www.npmjs.com/package/@deltachat/stdio-rpc-server
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
# Needed to publish the binaries to the release.
|
|
||||||
contents: write
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
persist-credentials: false
|
|
||||||
- uses: actions/setup-python@v6
|
|
||||||
with:
|
|
||||||
python-version: "3.11"
|
|
||||||
|
|
||||||
- name: Download Linux aarch64 binary
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-aarch64-linux
|
|
||||||
path: deltachat-rpc-server-aarch64-linux.d
|
|
||||||
|
|
||||||
- name: Download Linux armv7l binary
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-armv7l-linux
|
|
||||||
path: deltachat-rpc-server-armv7l-linux.d
|
|
||||||
|
|
||||||
- name: Download Linux armv6l binary
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-armv6l-linux
|
|
||||||
path: deltachat-rpc-server-armv6l-linux.d
|
|
||||||
|
|
||||||
- name: Download Linux i686 binary
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-i686-linux
|
|
||||||
path: deltachat-rpc-server-i686-linux.d
|
|
||||||
|
|
||||||
- name: Download Linux x86_64 binary
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-x86_64-linux
|
|
||||||
path: deltachat-rpc-server-x86_64-linux.d
|
|
||||||
|
|
||||||
- name: Download Win32 binary
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-win32
|
|
||||||
path: deltachat-rpc-server-win32.d
|
|
||||||
|
|
||||||
- name: Download Win64 binary
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-win64
|
|
||||||
path: deltachat-rpc-server-win64.d
|
|
||||||
|
|
||||||
- name: Download macOS binary for x86_64
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-x86_64-macos
|
|
||||||
path: deltachat-rpc-server-x86_64-macos.d
|
|
||||||
|
|
||||||
- name: Download macOS binary for aarch64
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-aarch64-macos
|
|
||||||
path: deltachat-rpc-server-aarch64-macos.d
|
|
||||||
|
|
||||||
- name: Download Android binary for arm64-v8a
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-arm64-v8a-android
|
|
||||||
path: deltachat-rpc-server-arm64-v8a-android.d
|
|
||||||
|
|
||||||
- name: Download Android binary for armeabi-v7a
|
|
||||||
uses: actions/download-artifact@v6
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-armeabi-v7a-android
|
|
||||||
path: deltachat-rpc-server-armeabi-v7a-android.d
|
|
||||||
|
|
||||||
- name: make npm packets for prebuilds and `@deltachat/stdio-rpc-server`
|
|
||||||
run: |
|
|
||||||
cd deltachat-rpc-server/npm-package
|
|
||||||
|
|
||||||
python --version
|
|
||||||
|
|
||||||
python scripts/pack_binary_for_platform.py aarch64-unknown-linux-musl ../../deltachat-rpc-server-aarch64-linux.d/deltachat-rpc-server
|
|
||||||
python scripts/pack_binary_for_platform.py armv7-unknown-linux-musleabihf ../../deltachat-rpc-server-armv7l-linux.d/deltachat-rpc-server
|
|
||||||
python scripts/pack_binary_for_platform.py arm-unknown-linux-musleabihf ../../deltachat-rpc-server-armv6l-linux.d/deltachat-rpc-server
|
|
||||||
python scripts/pack_binary_for_platform.py i686-unknown-linux-musl ../../deltachat-rpc-server-i686-linux.d/deltachat-rpc-server
|
|
||||||
python scripts/pack_binary_for_platform.py x86_64-unknown-linux-musl ../../deltachat-rpc-server-x86_64-linux.d/deltachat-rpc-server
|
|
||||||
python scripts/pack_binary_for_platform.py i686-pc-windows-gnu ../../deltachat-rpc-server-win32.d/deltachat-rpc-server.exe
|
|
||||||
python scripts/pack_binary_for_platform.py x86_64-pc-windows-gnu ../../deltachat-rpc-server-win64.d/deltachat-rpc-server.exe
|
|
||||||
python scripts/pack_binary_for_platform.py x86_64-apple-darwin ../../deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server
|
|
||||||
python scripts/pack_binary_for_platform.py aarch64-apple-darwin ../../deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server
|
|
||||||
python scripts/pack_binary_for_platform.py aarch64-linux-android ../../deltachat-rpc-server-arm64-v8a-android.d/deltachat-rpc-server
|
|
||||||
python scripts/pack_binary_for_platform.py armv7-linux-androideabi ../../deltachat-rpc-server-armeabi-v7a-android.d/deltachat-rpc-server
|
|
||||||
|
|
||||||
ls -lah platform_package
|
|
||||||
|
|
||||||
for platform in ./platform_package/*; do npm pack "$platform"; done
|
|
||||||
npm pack
|
|
||||||
ls -lah
|
|
||||||
|
|
||||||
- name: Upload to artifacts
|
|
||||||
uses: actions/upload-artifact@v5
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-npm-package
|
|
||||||
path: deltachat-rpc-server/npm-package/*.tgz
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload npm packets to the GitHub release
|
|
||||||
if: github.event_name == 'release'
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
REF_NAME: ${{ github.ref_name }}
|
|
||||||
run: |
|
|
||||||
gh release upload "$REF_NAME" \
|
|
||||||
--repo ${{ github.repository }} \
|
|
||||||
deltachat-rpc-server/npm-package/*.tgz
|
|
||||||
|
|
||||||
# Configure Node.js for publishing.
|
|
||||||
- uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: 20
|
|
||||||
registry-url: "https://registry.npmjs.org"
|
|
||||||
|
|
||||||
# Ensure npm 11.5.1 or later is installed.
|
|
||||||
# It is needed for <https://docs.npmjs.com/trusted-publishers>
|
|
||||||
- name: Update npm
|
|
||||||
run: npm install -g npm@latest
|
|
||||||
|
|
||||||
- name: Publish npm packets for prebuilds and `@deltachat/stdio-rpc-server`
|
|
||||||
if: github.event_name == 'release'
|
|
||||||
working-directory: deltachat-rpc-server/npm-package
|
|
||||||
run: |
|
|
||||||
ls -lah platform_package
|
|
||||||
for platform in *.tgz; do npm publish --provenance "$platform" --access public; done
|
|
||||||
|
|||||||
2
.github/workflows/dependabot.yml
vendored
2
.github/workflows/dependabot.yml
vendored
@@ -14,7 +14,7 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- name: Dependabot metadata
|
- name: Dependabot metadata
|
||||||
id: metadata
|
id: metadata
|
||||||
uses: dependabot/fetch-metadata@v2.4.0
|
uses: dependabot/fetch-metadata@v1.1.1
|
||||||
with:
|
with:
|
||||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||||
- name: Approve a PR
|
- name: Approve a PR
|
||||||
|
|||||||
97
.github/workflows/jsonrpc-client-npm-package.yml
vendored
97
.github/workflows/jsonrpc-client-npm-package.yml
vendored
@@ -1,47 +1,82 @@
|
|||||||
name: "Publish @deltachat/jsonrpc-client"
|
name: "jsonrpc js client build"
|
||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
pull_request:
|
||||||
release:
|
push:
|
||||||
types: [published]
|
tags:
|
||||||
|
- "*"
|
||||||
permissions: {}
|
- "!py-*"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
pack-module:
|
pack-module:
|
||||||
name: "Publish @deltachat/jsonrpc-client"
|
name: "Package @deltachat/jsonrpc-client and upload to download.delta.chat"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-20.04
|
||||||
environment:
|
|
||||||
name: npm-jsonrpc-client
|
|
||||||
url: https://www.npmjs.com/package/@deltachat/jsonrpc-client
|
|
||||||
permissions:
|
|
||||||
id-token: write
|
|
||||||
contents: read
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- name: Install tree
|
||||||
|
run: sudo apt install tree
|
||||||
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
- uses: actions/setup-node@v4
|
||||||
|
|
||||||
- uses: actions/setup-node@v6
|
|
||||||
with:
|
with:
|
||||||
node-version: 20
|
node-version: "18"
|
||||||
registry-url: "https://registry.npmjs.org"
|
- name: Get tag
|
||||||
|
id: tag
|
||||||
# Ensure npm 11.5.1 or later is installed.
|
uses: dawidd6/action-get-tag@v1
|
||||||
# It is needed for <https://docs.npmjs.com/trusted-publishers>
|
continue-on-error: true
|
||||||
- name: Update npm
|
- name: Get Pull Request ID
|
||||||
run: npm install -g npm@latest
|
id: prepare
|
||||||
|
run: |
|
||||||
|
tag=${{ steps.tag.outputs.tag }}
|
||||||
|
if [ -z "$tag" ]; then
|
||||||
|
node -e "console.log('DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-' + '${{ github.ref }}'.split('/')[2] + '.tar.gz')" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "DELTACHAT_JSONRPC_TAR_GZ=deltachat-jsonrpc-client-${{ steps.tag.outputs.tag }}.tar.gz" >> $GITHUB_ENV
|
||||||
|
echo "No preview will be uploaded this time, but the $tag release"
|
||||||
|
fi
|
||||||
|
- name: System info
|
||||||
|
run: |
|
||||||
|
npm --version
|
||||||
|
node --version
|
||||||
|
echo $DELTACHAT_JSONRPC_TAR_GZ
|
||||||
- name: Install dependencies without running scripts
|
- name: Install dependencies without running scripts
|
||||||
working-directory: deltachat-jsonrpc/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm install --ignore-scripts
|
run: npm install --ignore-scripts
|
||||||
|
|
||||||
- name: Package
|
- name: Package
|
||||||
|
shell: bash
|
||||||
working-directory: deltachat-jsonrpc/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: |
|
run: |
|
||||||
npm run build
|
npm run build
|
||||||
npm pack .
|
npm pack .
|
||||||
|
ls -lah
|
||||||
- name: Publish
|
mv $(find deltachat-jsonrpc-client-*) $DELTACHAT_JSONRPC_TAR_GZ
|
||||||
working-directory: deltachat-jsonrpc/typescript
|
- name: Upload Prebuild
|
||||||
run: npm publish --provenance deltachat-jsonrpc-client-* --access public
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-jsonrpc-client.tgz
|
||||||
|
path: deltachat-jsonrpc/typescript/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
|
||||||
|
# Upload to download.delta.chat/node/preview/
|
||||||
|
- name: Upload deltachat-jsonrpc-client preview to download.delta.chat/node/preview/
|
||||||
|
if: ${{ ! steps.tag.outputs.tag }}
|
||||||
|
id: upload-preview
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
|
||||||
|
chmod 600 __TEMP_INPUT_KEY_FILE
|
||||||
|
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
|
||||||
|
continue-on-error: true
|
||||||
|
- name: Post links to details
|
||||||
|
if: steps.upload-preview.outcome == 'success'
|
||||||
|
run: node ./node/scripts/postLinksToDetails.js
|
||||||
|
env:
|
||||||
|
URL: preview/${{ env.DELTACHAT_JSONRPC_TAR_GZ }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
MSG_CONTEXT: Download the deltachat-jsonrpc-client.tgz
|
||||||
|
# Upload to download.delta.chat/node/
|
||||||
|
- name: Upload deltachat-jsonrpc-client build to download.delta.chat/node/
|
||||||
|
if: ${{ steps.tag.outputs.tag }}
|
||||||
|
id: upload
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
|
||||||
|
chmod 600 __TEMP_INPUT_KEY_FILE
|
||||||
|
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r deltachat-jsonrpc/typescript/$DELTACHAT_JSONRPC_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/"
|
||||||
|
|||||||
12
.github/workflows/jsonrpc.yml
vendored
12
.github/workflows/jsonrpc.yml
vendored
@@ -6,8 +6,6 @@ on:
|
|||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
RUST_MIN_STACK: "8388608"
|
RUST_MIN_STACK: "8388608"
|
||||||
@@ -16,12 +14,11 @@ jobs:
|
|||||||
build_and_test:
|
build_and_test:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
|
||||||
- name: Use Node.js 18.x
|
- name: Use Node.js 18.x
|
||||||
uses: actions/setup-node@v6
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: 18.x
|
node-version: 18.x
|
||||||
- name: Add Rust cache
|
- name: Add Rust cache
|
||||||
@@ -36,7 +33,10 @@ jobs:
|
|||||||
working-directory: deltachat-jsonrpc/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm run test
|
run: npm run test
|
||||||
env:
|
env:
|
||||||
CHATMAIL_DOMAIN: ${{ vars.CHATMAIL_DOMAIN }}
|
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
|
||||||
|
- name: make sure websocket server version still builds
|
||||||
|
working-directory: deltachat-jsonrpc
|
||||||
|
run: cargo build --bin deltachat-jsonrpc-server --features webserver
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
working-directory: deltachat-jsonrpc/typescript
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: npm run prettier:check
|
run: npm run prettier:check
|
||||||
|
|||||||
109
.github/workflows/nix.yml
vendored
109
.github/workflows/nix.yml
vendored
@@ -1,109 +0,0 @@
|
|||||||
name: Test Nix flake
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
paths:
|
|
||||||
- flake.nix
|
|
||||||
- flake.lock
|
|
||||||
- .github/workflows/nix.yml
|
|
||||||
push:
|
|
||||||
paths:
|
|
||||||
- flake.nix
|
|
||||||
- flake.lock
|
|
||||||
- .github/workflows/nix.yml
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
format:
|
|
||||||
name: check flake formatting
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
persist-credentials: false
|
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
|
||||||
- run: nix fmt flake.nix -- --check
|
|
||||||
|
|
||||||
build:
|
|
||||||
name: nix build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
installable:
|
|
||||||
# Ensure `nix develop` will work.
|
|
||||||
- devShells.x86_64-linux.default
|
|
||||||
|
|
||||||
- deltachat-python
|
|
||||||
- deltachat-repl
|
|
||||||
- deltachat-repl-aarch64-linux
|
|
||||||
- deltachat-repl-arm64-v8a-android
|
|
||||||
- deltachat-repl-armeabi-v7a-android
|
|
||||||
- deltachat-repl-armv6l-linux
|
|
||||||
- deltachat-repl-armv7l-linux
|
|
||||||
- deltachat-repl-i686-linux
|
|
||||||
- deltachat-repl-win32
|
|
||||||
- deltachat-repl-win64
|
|
||||||
- deltachat-repl-x86_64-linux
|
|
||||||
- deltachat-rpc-client
|
|
||||||
- deltachat-rpc-server
|
|
||||||
- deltachat-rpc-server-aarch64-linux
|
|
||||||
- deltachat-rpc-server-aarch64-linux-wheel
|
|
||||||
- deltachat-rpc-server-arm64-v8a-android
|
|
||||||
- deltachat-rpc-server-arm64-v8a-android-wheel
|
|
||||||
- deltachat-rpc-server-armeabi-v7a-android
|
|
||||||
- deltachat-rpc-server-armeabi-v7a-android-wheel
|
|
||||||
- deltachat-rpc-server-armv6l-linux
|
|
||||||
- deltachat-rpc-server-armv6l-linux-wheel
|
|
||||||
- deltachat-rpc-server-armv7l-linux
|
|
||||||
- deltachat-rpc-server-armv7l-linux-wheel
|
|
||||||
- deltachat-rpc-server-i686-linux
|
|
||||||
- deltachat-rpc-server-i686-linux-wheel
|
|
||||||
- deltachat-rpc-server-source
|
|
||||||
- deltachat-rpc-server-win32
|
|
||||||
- deltachat-rpc-server-win32-wheel
|
|
||||||
- deltachat-rpc-server-win64
|
|
||||||
- deltachat-rpc-server-win64-wheel
|
|
||||||
- deltachat-rpc-server-x86_64-linux
|
|
||||||
- deltachat-rpc-server-x86_64-linux-wheel
|
|
||||||
- docs
|
|
||||||
- libdeltachat
|
|
||||||
- python-docs
|
|
||||||
|
|
||||||
# Fails to build
|
|
||||||
#- deltachat-repl-x86_64-android
|
|
||||||
#- deltachat-repl-x86-android
|
|
||||||
#- deltachat-rpc-server-x86_64-android
|
|
||||||
#- deltachat-rpc-server-x86-android
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
persist-credentials: false
|
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
|
||||||
- run: nix build .#${{ matrix.installable }}
|
|
||||||
|
|
||||||
build-macos:
|
|
||||||
name: nix build on macOS
|
|
||||||
runs-on: macos-latest
|
|
||||||
strategy:
|
|
||||||
fail-fast: false
|
|
||||||
matrix:
|
|
||||||
installable:
|
|
||||||
- deltachat-rpc-server
|
|
||||||
- deltachat-rpc-server-x86_64-darwin
|
|
||||||
|
|
||||||
# Fails to build
|
|
||||||
# because of <https://github.com/NixOS/nixpkgs/issues/413910>.
|
|
||||||
# - deltachat-rpc-server-aarch64-darwin
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
persist-credentials: false
|
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
|
||||||
- run: nix build .#${{ matrix.installable }}
|
|
||||||
41
.github/workflows/node-docs.yml
vendored
Normal file
41
.github/workflows/node-docs.yml
vendored
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
# GitHub Actions workflow to build
|
||||||
|
# Node.js bindings documentation
|
||||||
|
# and upload it to the web server.
|
||||||
|
# Built documentation is available at <https://js.delta.chat/>
|
||||||
|
|
||||||
|
name: Generate & upload node.js documentation
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
generate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
|
||||||
|
- name: Use Node.js 18.x
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 18.x
|
||||||
|
|
||||||
|
- name: npm install and generate documentation
|
||||||
|
working-directory: node
|
||||||
|
run: |
|
||||||
|
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/"
|
||||||
235
.github/workflows/node-package.yml
vendored
Normal file
235
.github/workflows/node-package.yml
vendored
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
name: "node.js build"
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
- "!py-*"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
prebuild:
|
||||||
|
name: Prebuild
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [macos-latest, windows-latest]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
|
- name: System info
|
||||||
|
run: |
|
||||||
|
rustc -vV
|
||||||
|
rustup -vV
|
||||||
|
cargo -vV
|
||||||
|
npm --version
|
||||||
|
node --version
|
||||||
|
|
||||||
|
- name: Cache node modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ env.APPDATA }}/npm-cache
|
||||||
|
~/.npm
|
||||||
|
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||||
|
|
||||||
|
- name: Cache cargo index
|
||||||
|
uses: actions/cache@v4
|
||||||
|
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'
|
||||||
|
working-directory: node
|
||||||
|
run: npm install --verbose
|
||||||
|
|
||||||
|
- name: Build Prebuild
|
||||||
|
working-directory: node
|
||||||
|
run: |
|
||||||
|
npm run prebuildify
|
||||||
|
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
|
||||||
|
|
||||||
|
- name: Upload Prebuild
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: ${{ matrix.os }}
|
||||||
|
path: node/${{ matrix.os }}.tar.gz
|
||||||
|
|
||||||
|
prebuild-linux:
|
||||||
|
name: Prebuild Linux
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
# Build Linux prebuilds inside a container with old glibc for backwards compatibility.
|
||||||
|
# Debian 10 contained glibc 2.28: https://packages.debian.org/buster/libc6
|
||||||
|
container: debian:10
|
||||||
|
steps:
|
||||||
|
# Working directory is owned by 1001:1001 by default.
|
||||||
|
# Change it to our user.
|
||||||
|
- name: Change working directory owner
|
||||||
|
run: chown root:root .
|
||||||
|
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
|
- run: apt-get update
|
||||||
|
|
||||||
|
# Python is needed for node-gyp
|
||||||
|
- name: Install curl, python and compilers
|
||||||
|
run: apt-get install -y curl build-essential python3
|
||||||
|
- name: Install Rust
|
||||||
|
run: |
|
||||||
|
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
|
||||||
|
echo "$HOME/.cargo/bin" >> $GITHUB_PATH
|
||||||
|
- name: System info
|
||||||
|
run: |
|
||||||
|
rustc -vV
|
||||||
|
rustup -vV
|
||||||
|
cargo -vV
|
||||||
|
npm --version
|
||||||
|
node --version
|
||||||
|
|
||||||
|
- name: Cache node modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ env.APPDATA }}/npm-cache
|
||||||
|
~/.npm
|
||||||
|
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||||
|
|
||||||
|
- name: Cache cargo index
|
||||||
|
uses: actions/cache@v4
|
||||||
|
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'
|
||||||
|
working-directory: node
|
||||||
|
run: npm install --verbose
|
||||||
|
|
||||||
|
- name: Build Prebuild
|
||||||
|
working-directory: node
|
||||||
|
run: |
|
||||||
|
npm run prebuildify
|
||||||
|
tar -zcvf "linux.tar.gz" -C prebuilds .
|
||||||
|
|
||||||
|
- name: Upload Prebuild
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: linux
|
||||||
|
path: node/linux.tar.gz
|
||||||
|
|
||||||
|
pack-module:
|
||||||
|
needs: [prebuild, prebuild-linux]
|
||||||
|
name: Package deltachat-node and upload to download.delta.chat
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Install tree
|
||||||
|
run: sudo apt install tree
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
|
- name: Get tag
|
||||||
|
id: tag
|
||||||
|
uses: dawidd6/action-get-tag@v1
|
||||||
|
continue-on-error: true
|
||||||
|
- name: Get Pull Request ID
|
||||||
|
id: prepare
|
||||||
|
run: |
|
||||||
|
tag=${{ steps.tag.outputs.tag }}
|
||||||
|
if [ -z "$tag" ]; then
|
||||||
|
node -e "console.log('DELTACHAT_NODE_TAR_GZ=deltachat-node-' + '${{ github.ref }}'.split('/')[2] + '.tar.gz')" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "DELTACHAT_NODE_TAR_GZ=deltachat-node-${{ steps.tag.outputs.tag }}.tar.gz" >> $GITHUB_ENV
|
||||||
|
echo "No preview will be uploaded this time, but the $tag release"
|
||||||
|
fi
|
||||||
|
- name: System info
|
||||||
|
run: |
|
||||||
|
rustc -vV
|
||||||
|
rustup -vV
|
||||||
|
cargo -vV
|
||||||
|
npm --version
|
||||||
|
node --version
|
||||||
|
echo $DELTACHAT_NODE_TAR_GZ
|
||||||
|
- name: Download Linux prebuild
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: linux
|
||||||
|
- name: Download macOS prebuild
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: macos-latest
|
||||||
|
- name: Download Windows prebuild
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
name: windows-latest
|
||||||
|
- shell: bash
|
||||||
|
run: |
|
||||||
|
mkdir node/prebuilds
|
||||||
|
tar -xvzf linux.tar.gz -C node/prebuilds
|
||||||
|
tar -xvzf macos-latest.tar.gz -C node/prebuilds
|
||||||
|
tar -xvzf windows-latest.tar.gz -C node/prebuilds
|
||||||
|
tree node/prebuilds
|
||||||
|
rm -f linux.tar.gz macos-latest.tar.gz windows-latest.tar.gz
|
||||||
|
- name: Install dependencies without running scripts
|
||||||
|
run: |
|
||||||
|
npm install --ignore-scripts
|
||||||
|
- name: Build constants
|
||||||
|
run: |
|
||||||
|
npm run build:core:constants
|
||||||
|
- name: Build TypeScript part
|
||||||
|
run: |
|
||||||
|
npm run build:bindings:ts
|
||||||
|
- name: Package
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
mv node/README.md README.md
|
||||||
|
npm pack .
|
||||||
|
ls -lah
|
||||||
|
mv $(find deltachat-node-*) $DELTACHAT_NODE_TAR_GZ
|
||||||
|
- name: Upload prebuild
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: deltachat-node.tgz
|
||||||
|
path: ${{ env.DELTACHAT_NODE_TAR_GZ }}
|
||||||
|
# Upload to download.delta.chat/node/preview/
|
||||||
|
- name: Upload deltachat-node preview to download.delta.chat/node/preview/
|
||||||
|
if: ${{ ! steps.tag.outputs.tag }}
|
||||||
|
id: upload-preview
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
echo -e "${{ secrets.SSH_KEY }}" >__TEMP_INPUT_KEY_FILE
|
||||||
|
chmod 600 __TEMP_INPUT_KEY_FILE
|
||||||
|
scp -o StrictHostKeyChecking=no -v -i __TEMP_INPUT_KEY_FILE -P "22" -r $DELTACHAT_NODE_TAR_GZ "${{ secrets.USERNAME }}"@"download.delta.chat":"/var/www/html/download/node/preview/"
|
||||||
|
continue-on-error: true
|
||||||
|
- name: Post links to details
|
||||||
|
if: steps.upload-preview.outcome == 'success'
|
||||||
|
run: node ./node/scripts/postLinksToDetails.js
|
||||||
|
env:
|
||||||
|
URL: preview/${{ env.DELTACHAT_NODE_TAR_GZ }}
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
# 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/"
|
||||||
68
.github/workflows/node-tests.yml
vendored
Normal file
68
.github/workflows/node-tests.yml
vendored
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
# GitHub Actions workflow
|
||||||
|
# to test Node.js bindings.
|
||||||
|
|
||||||
|
name: "node.js tests"
|
||||||
|
|
||||||
|
# Cancel previously started workflow runs
|
||||||
|
# when the branch is updated.
|
||||||
|
concurrency:
|
||||||
|
group: ${{ github.workflow }}-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
tests:
|
||||||
|
name: Tests
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
show-progress: false
|
||||||
|
- uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: "18"
|
||||||
|
- name: System info
|
||||||
|
run: |
|
||||||
|
rustc -vV
|
||||||
|
rustup -vV
|
||||||
|
cargo -vV
|
||||||
|
npm --version
|
||||||
|
node --version
|
||||||
|
|
||||||
|
- name: Cache node modules
|
||||||
|
uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ env.APPDATA }}/npm-cache
|
||||||
|
~/.npm
|
||||||
|
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||||
|
|
||||||
|
- name: Cache cargo index
|
||||||
|
uses: actions/cache@v4
|
||||||
|
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'
|
||||||
|
working-directory: node
|
||||||
|
run: npm install --verbose
|
||||||
|
|
||||||
|
- name: Test
|
||||||
|
timeout-minutes: 10
|
||||||
|
working-directory: node
|
||||||
|
run: npm run test
|
||||||
|
env:
|
||||||
|
CHATMAIL_DOMAIN: ${{ secrets.CHATMAIL_DOMAIN }}
|
||||||
|
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"
|
||||||
@@ -5,25 +5,22 @@ on:
|
|||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
name: Build distribution
|
name: Build distribution
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
|
||||||
- name: Install pypa/build
|
- name: Install pypa/build
|
||||||
run: python3 -m pip install build
|
run: python3 -m pip install build
|
||||||
- name: Build a binary wheel and a source tarball
|
- name: Build a binary wheel and a source tarball
|
||||||
working-directory: deltachat-rpc-client
|
working-directory: deltachat-rpc-client
|
||||||
run: python3 -m build
|
run: python3 -m build
|
||||||
- name: Store the distribution packages
|
- name: Store the distribution packages
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: python-package-distributions
|
name: python-package-distributions
|
||||||
path: deltachat-rpc-client/dist/
|
path: deltachat-rpc-client/dist/
|
||||||
@@ -42,7 +39,7 @@ jobs:
|
|||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Download all the dists
|
- name: Download all the dists
|
||||||
uses: actions/download-artifact@v6
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: python-package-distributions
|
name: python-package-distributions
|
||||||
path: dist/
|
path: dist/
|
||||||
|
|||||||
10
.github/workflows/repl.yml
vendored
10
.github/workflows/repl.yml
vendored
@@ -7,22 +7,20 @@ name: Build Windows REPL .exe
|
|||||||
on:
|
on:
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_repl:
|
build_repl:
|
||||||
name: Build REPL example
|
name: Build REPL example
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
- name: Build
|
- name: Build
|
||||||
run: nix build .#deltachat-repl-win64
|
run: nix build .#deltachat-repl-win64
|
||||||
- name: Upload binary
|
- name: Upload binary
|
||||||
uses: actions/upload-artifact@v5
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: repl.exe
|
name: repl.exe
|
||||||
path: "result/bin/deltachat-repl.exe"
|
path: "result/bin/deltachat-repl.exe"
|
||||||
|
|||||||
63
.github/workflows/upload-docs.yml
vendored
63
.github/workflows/upload-docs.yml
vendored
@@ -1,42 +1,40 @@
|
|||||||
name: Build & deploy documentation on rs.delta.chat, c.delta.chat, and py.delta.chat
|
name: Build & Deploy Documentation on rs.delta.chat, c.delta.chat, py.delta.chat
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
- build_jsonrpc_docs_ci
|
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build-rs:
|
build-rs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
|
||||||
- name: Build the documentation with cargo
|
- name: Build the documentation with cargo
|
||||||
run: |
|
run: |
|
||||||
cargo doc --package deltachat --no-deps --document-private-items
|
cargo doc --package deltachat --no-deps --document-private-items
|
||||||
- name: Upload to rs.delta.chat
|
- name: Upload to rs.delta.chat
|
||||||
run: |
|
uses: up9cloud/action-rsync@v1.3
|
||||||
mkdir -p "$HOME/.ssh"
|
env:
|
||||||
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
USER: ${{ secrets.USERNAME }}
|
||||||
chmod 600 "$HOME/.ssh/key"
|
KEY: ${{ secrets.KEY }}
|
||||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/target/doc "${{ secrets.USERNAME }}@rs.delta.chat:/var/www/html/rs/"
|
HOST: "delta.chat"
|
||||||
|
SOURCE: "target/doc"
|
||||||
|
TARGET: "/var/www/html/rs/"
|
||||||
|
|
||||||
build-python:
|
build-python:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
|
||||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
- name: Build Python documentation
|
- name: Build Python documentation
|
||||||
run: nix build .#python-docs
|
run: nix build .#python-docs
|
||||||
- name: Upload to py.delta.chat
|
- name: Upload to py.delta.chat
|
||||||
@@ -50,46 +48,17 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
|
||||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
||||||
- uses: cachix/install-nix-action@0b0e072294b088b73964f1d72dfdac0951439dbd # v31
|
- uses: DeterminateSystems/nix-installer-action@main
|
||||||
|
- uses: DeterminateSystems/magic-nix-cache-action@main
|
||||||
- name: Build C documentation
|
- name: Build C documentation
|
||||||
run: nix build .#docs
|
run: nix build .#docs
|
||||||
- name: Upload to c.delta.chat
|
- name: Upload to py.delta.chat
|
||||||
run: |
|
run: |
|
||||||
mkdir -p "$HOME/.ssh"
|
mkdir -p "$HOME/.ssh"
|
||||||
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
|
echo "${{ secrets.CODESPEAK_KEY }}" > "$HOME/.ssh/key"
|
||||||
chmod 600 "$HOME/.ssh/key"
|
chmod 600 "$HOME/.ssh/key"
|
||||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@c.delta.chat:/home/delta/build-c/master"
|
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/result/html/ "delta@c.delta.chat:/home/delta/build-c/master"
|
||||||
|
|
||||||
build-ts:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
defaults:
|
|
||||||
run:
|
|
||||||
working-directory: ./deltachat-jsonrpc/typescript
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
show-progress: false
|
|
||||||
persist-credentials: false
|
|
||||||
fetch-depth: 0 # Fetch history to calculate VCS version number.
|
|
||||||
- name: Use Node.js
|
|
||||||
uses: actions/setup-node@v6
|
|
||||||
with:
|
|
||||||
node-version: '18'
|
|
||||||
- name: npm install
|
|
||||||
run: npm install
|
|
||||||
- name: npm run build
|
|
||||||
run: npm run build
|
|
||||||
- name: Run docs script
|
|
||||||
run: npm run docs
|
|
||||||
- name: Upload to js.jsonrpc.delta.chat
|
|
||||||
run: |
|
|
||||||
mkdir -p "$HOME/.ssh"
|
|
||||||
echo "${{ secrets.KEY }}" > "$HOME/.ssh/key"
|
|
||||||
chmod 600 "$HOME/.ssh/key"
|
|
||||||
rsync -avzh -e "ssh -i $HOME/.ssh/key -o StrictHostKeyChecking=no" $GITHUB_WORKSPACE/deltachat-jsonrpc/typescript/docs/ "${{ secrets.USERNAME }}@js.jsonrpc.delta.chat:/var/www/html/js-jsonrpc/"
|
|
||||||
|
|||||||
7
.github/workflows/upload-ffi-docs.yml
vendored
7
.github/workflows/upload-ffi-docs.yml
vendored
@@ -1,5 +1,5 @@
|
|||||||
# GitHub Actions workflow
|
# GitHub Actions workflow
|
||||||
# to build `deltachat_ffi` crate documentation
|
# to build `deltachat_fii` crate documentation
|
||||||
# and upload it to <https://cffi.delta.chat/>
|
# and upload it to <https://cffi.delta.chat/>
|
||||||
|
|
||||||
name: Build & Deploy Documentation on cffi.delta.chat
|
name: Build & Deploy Documentation on cffi.delta.chat
|
||||||
@@ -9,17 +9,14 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v6
|
- uses: actions/checkout@v4
|
||||||
with:
|
with:
|
||||||
show-progress: false
|
show-progress: false
|
||||||
persist-credentials: false
|
|
||||||
- name: Build the documentation with cargo
|
- name: Build the documentation with cargo
|
||||||
run: |
|
run: |
|
||||||
cargo doc --package deltachat_ffi --no-deps
|
cargo doc --package deltachat_ffi --no-deps
|
||||||
|
|||||||
31
.github/workflows/zizmor-scan.yml
vendored
31
.github/workflows/zizmor-scan.yml
vendored
@@ -1,31 +0,0 @@
|
|||||||
name: GitHub Actions Security Analysis with zizmor
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: ["main"]
|
|
||||||
pull_request:
|
|
||||||
branches: ["**"]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
zizmor:
|
|
||||||
name: zizmor latest via PyPI
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
security-events: write
|
|
||||||
steps:
|
|
||||||
- name: Checkout repository
|
|
||||||
uses: actions/checkout@v6
|
|
||||||
with:
|
|
||||||
persist-credentials: false
|
|
||||||
|
|
||||||
- name: Install the latest version of uv
|
|
||||||
uses: astral-sh/setup-uv@1e862dfacbd1d6d858c55d9b792c756523627244
|
|
||||||
|
|
||||||
- name: Run zizmor
|
|
||||||
run: uvx zizmor --format sarif . > results.sarif
|
|
||||||
|
|
||||||
- name: Upload SARIF file
|
|
||||||
uses: github/codeql-action/upload-sarif@v4
|
|
||||||
with:
|
|
||||||
sarif_file: results.sarif
|
|
||||||
category: zizmor
|
|
||||||
10
.gitignore
vendored
10
.gitignore
vendored
@@ -1,9 +1,7 @@
|
|||||||
target/
|
/target
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
/build
|
/build
|
||||||
/dist
|
/dist
|
||||||
/fuzz/fuzz_targets/corpus/
|
|
||||||
/fuzz/fuzz_targets/crashes/
|
|
||||||
|
|
||||||
# ignore vi temporaries
|
# ignore vi temporaries
|
||||||
*~
|
*~
|
||||||
@@ -35,8 +33,7 @@ deltachat-ffi/xml
|
|||||||
|
|
||||||
coverage/
|
coverage/
|
||||||
.DS_Store
|
.DS_Store
|
||||||
.vscode
|
.vscode/launch.json
|
||||||
.zed
|
|
||||||
python/accounts.txt
|
python/accounts.txt
|
||||||
python/all-testaccounts.txt
|
python/all-testaccounts.txt
|
||||||
tmp/
|
tmp/
|
||||||
@@ -53,5 +50,4 @@ result
|
|||||||
|
|
||||||
# direnv
|
# direnv
|
||||||
.envrc
|
.envrc
|
||||||
.direnv
|
.direnv
|
||||||
.aider*
|
|
||||||
3900
CHANGELOG.md
3900
CHANGELOG.md
File diff suppressed because it is too large
Load Diff
@@ -12,12 +12,6 @@ else()
|
|||||||
set(DYNAMIC_EXT "dll")
|
set(DYNAMIC_EXT "dll")
|
||||||
endif()
|
endif()
|
||||||
|
|
||||||
if(DEFINED ENV{CARGO_BUILD_TARGET})
|
|
||||||
set(ARCH_DIR "$ENV{CARGO_BUILD_TARGET}")
|
|
||||||
else()
|
|
||||||
set(ARCH_DIR "./")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_custom_command(
|
add_custom_command(
|
||||||
OUTPUT
|
OUTPUT
|
||||||
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
"${CMAKE_BINARY_DIR}/target/release/libdeltachat.a"
|
||||||
@@ -27,7 +21,7 @@ add_custom_command(
|
|||||||
PREFIX=${CMAKE_INSTALL_PREFIX}
|
PREFIX=${CMAKE_INSTALL_PREFIX}
|
||||||
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
||||||
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
||||||
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release
|
${CARGO} build --target-dir=${CMAKE_BINARY_DIR}/target --release --no-default-features --features jsonrpc
|
||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,6 +35,6 @@ add_custom_target(
|
|||||||
)
|
)
|
||||||
|
|
||||||
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
||||||
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
install(FILES "${CMAKE_BINARY_DIR}/target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||||
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
install(FILES "${CMAKE_BINARY_DIR}/target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
||||||
install(FILES "${CMAKE_BINARY_DIR}/target/${ARCH_DIR}/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
install(FILES "${CMAKE_BINARY_DIR}/target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
||||||
|
|||||||
160
CONTRIBUTING.md
160
CONTRIBUTING.md
@@ -1,122 +1,126 @@
|
|||||||
# Contributing to chatmail core
|
# Contributing guidelines
|
||||||
|
|
||||||
## Bug reports
|
## Reporting bugs
|
||||||
|
|
||||||
If you found a bug, [report it on GitHub](https://github.com/chatmail/core/issues).
|
If you found a bug, [report it on GitHub](https://github.com/deltachat/deltachat-core-rust/issues).
|
||||||
If the bug you found is specific to
|
If the bug you found is specific to
|
||||||
[Android](https://github.com/deltachat/deltachat-android/issues),
|
[Android](https://github.com/deltachat/deltachat-android/issues),
|
||||||
[iOS](https://github.com/deltachat/deltachat-ios/issues) or
|
[iOS](https://github.com/deltachat/deltachat-ios/issues) or
|
||||||
[Desktop](https://github.com/deltachat/deltachat-desktop/issues),
|
[Desktop](https://github.com/deltachat/deltachat-desktop/issues),
|
||||||
report it to the corresponding repository.
|
report it to the corresponding repository.
|
||||||
|
|
||||||
## Feature proposals
|
## Proposing features
|
||||||
|
|
||||||
If you have a feature request, create a new topic on the [forum](https://support.delta.chat/).
|
If you have a feature request, create a new topic on the [forum](https://support.delta.chat/).
|
||||||
|
|
||||||
## Code contributions
|
## Contributing code
|
||||||
|
|
||||||
If you want to contribute a code, follow this guide.
|
If you want to contribute a code, [open a Pull Request](https://github.com/deltachat/deltachat-core-rust/pulls).
|
||||||
|
|
||||||
1. **Select an issue to work on.**
|
If you have write access to the repository,
|
||||||
|
push a branch named `<username>/<feature>`
|
||||||
|
so it is clear who is responsible for the branch,
|
||||||
|
and open a PR proposing to merge the change.
|
||||||
|
Otherwise fork the repository and create a branch in your fork.
|
||||||
|
|
||||||
If you have an write access to the repository, assign the issue to yourself.
|
You can find the list of good first issues
|
||||||
Otherwise state in the comment that you are going to work on the issue
|
and a link to this guide
|
||||||
to avoid duplicate work.
|
on the contributing page: <https://github.com/deltachat/deltachat-core-rust/contribute>
|
||||||
|
|
||||||
If the issue does not exist yet, create it first.
|
### Coding conventions
|
||||||
|
|
||||||
2. **Write the code.**
|
We format the code using `rustfmt`. Run `cargo fmt` prior to committing the code.
|
||||||
|
Run `scripts/clippy.sh` to check the code for common mistakes with [Clippy].
|
||||||
|
|
||||||
Follow the [coding conventions](STYLE.md) when writing the code.
|
Commit messages follow the [Conventional Commits] notation.
|
||||||
|
We use [git-cliff] to generate the changelog from commit messages before the release.
|
||||||
|
|
||||||
3. **Commit the code.**
|
With **`git cliff --unreleased`**, you can check how the changelog entry for your commit will look.
|
||||||
|
|
||||||
If you have write access to the repository,
|
The following prefix types are used:
|
||||||
push a branch named `<username>/<feature>`
|
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
|
||||||
so it is clear who is responsible for the branch,
|
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is cancelled"
|
||||||
and open a PR proposing to merge the change.
|
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
|
||||||
Otherwise fork the repository and create a branch in your fork.
|
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
|
||||||
|
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
|
||||||
|
- `test`: Test changes and improvements to the testing framework.
|
||||||
|
- `build`: Build system and tool configuration changes, e.g. "build(git-cliff): put "ci" commits into "CI" section of changelog"
|
||||||
|
- `ci`: CI configuration changes, e.g. "ci: limit artifact retention time for `libdeltachat.a` to 1 day"
|
||||||
|
- `docs`: Documentation changes, e.g. "docs: add contributing guidelines"
|
||||||
|
- `chore`: miscellaneous tasks, e.g. "chore: add `.DS_Store` to `.gitignore`"
|
||||||
|
|
||||||
Commit messages follow the [Conventional Commits] notation.
|
Release preparation commits are marked as "chore(release): prepare for vX.Y.Z".
|
||||||
We use [git-cliff] to generate the changelog from commit messages before the release.
|
|
||||||
|
|
||||||
With **`git cliff --unreleased`**, you can check how the changelog entry for your commit will look.
|
If you intend to squash merge the PR from the web interface,
|
||||||
|
make sure the PR title follows the conventional commits notation
|
||||||
|
as it will end up being a commit title.
|
||||||
|
Otherwise make sure each commit title follows the conventional commit notation.
|
||||||
|
|
||||||
The following prefix types are used:
|
#### Breaking Changes
|
||||||
- `feat`: Features, e.g. "feat: Pause IO for BackupProvider". If you are unsure what's the category of your commit, you can often just use `feat`.
|
|
||||||
- `fix`: Bug fixes, e.g. "fix: delete `smtp` rows when message sending is canceled"
|
|
||||||
- `api`: API changes, e.g. "api(rust): add `get_msg_read_receipts(context, msg_id)`"
|
|
||||||
- `refactor`: Refactorings, e.g. "refactor: iterate over `msg_ids` without `.iter()`"
|
|
||||||
- `perf`: Performance improvements, e.g. "perf: improve SQLite performance with `PRAGMA synchronous=normal`"
|
|
||||||
- `test`: Test changes and improvements to the testing framework.
|
|
||||||
- `build`: Build system and tool configuration changes, e.g. "build(git-cliff): put "ci" commits into "CI" section of changelog"
|
|
||||||
- `ci`: CI configuration changes, e.g. "ci: limit artifact retention time for `libdeltachat.a` to 1 day"
|
|
||||||
- `docs`: Documentation changes, e.g. "docs: add contributing guidelines"
|
|
||||||
- `chore`: miscellaneous tasks, e.g. "chore: add `.DS_Store` to `.gitignore`"
|
|
||||||
|
|
||||||
Release preparation commits are marked as "chore(release): prepare for X.Y.Z"
|
Use a `!` to mark breaking changes, e.g. "api!: Remove `dc_chat_can_send`".
|
||||||
as described in [releasing guide](RELEASE.md).
|
|
||||||
|
|
||||||
Use a `!` to mark breaking changes, e.g. "api!: Remove `dc_chat_can_send`".
|
Alternatively, breaking changes can go into the commit description, e.g.:
|
||||||
|
|
||||||
Alternatively, breaking changes can go into the commit description, e.g.:
|
```
|
||||||
|
fix: Fix race condition and db corruption when a message was received during backup
|
||||||
|
|
||||||
```
|
BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`
|
||||||
fix: Fix race condition and db corruption when a message was received during backup
|
```
|
||||||
|
|
||||||
BREAKING CHANGE: You have to call `dc_stop_io()`/`dc_start_io()` before/after `dc_imex(DC_IMEX_EXPORT_BACKUP)`
|
#### Multiple Changes in one PR
|
||||||
```
|
|
||||||
|
|
||||||
4. [**Open a Pull Request**](https://github.com/chatmail/core/pulls).
|
If you have multiple changes in one PR, create multiple conventional commits, and then do a rebase merge. Otherwise, you should usually do a squash merge.
|
||||||
|
|
||||||
Refer to the corresponding issue.
|
[Clippy]: https://doc.rust-lang.org/clippy/
|
||||||
|
[Conventional Commits]: https://www.conventionalcommits.org/
|
||||||
|
[git-cliff]: https://git-cliff.org/
|
||||||
|
|
||||||
If you intend to squash merge the PR from the web interface,
|
### Errors
|
||||||
make sure the PR title follows the conventional commits notation
|
|
||||||
as it will end up being a commit title.
|
|
||||||
Otherwise make sure each commit title follows the conventional commit notation.
|
|
||||||
|
|
||||||
5. **Make sure all CI checks succeed.**
|
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
|
||||||
|
When using [`Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html),
|
||||||
|
capitalize it but do not add a full stop as the contexts will be separated by `:`.
|
||||||
|
For example:
|
||||||
|
```
|
||||||
|
.with_context(|| format!("Unable to trash message {msg_id}"))
|
||||||
|
```
|
||||||
|
|
||||||
CI runs the tests and checks code formatting.
|
All errors should be handled in one of these ways:
|
||||||
|
- With `if let Err() =` (incl. logging them into `warn!()`/`err!()`).
|
||||||
|
- With `.log_err().ok()`.
|
||||||
|
- Bubbled up with `?`.
|
||||||
|
|
||||||
While it is running, self-review your PR to make sure all the changes you expect are there
|
`backtrace` feature is enabled for `anyhow` crate
|
||||||
and there are no accidentally committed unrelated changes and files.
|
and `debug = 1` option is set in the test profile.
|
||||||
|
This allows to run `RUST_BACKTRACE=1 cargo test`
|
||||||
|
and get a backtrace with line numbers in resultified tests
|
||||||
|
which return `anyhow::Result`.
|
||||||
|
|
||||||
Push the necessary fixup commits or force-push to your branch if needed.
|
### Logging
|
||||||
|
|
||||||
6. **Ask for review.**
|
For logging, use `info!`, `warn!` and `error!` macros.
|
||||||
|
Log messages should be capitalized and have a full stop in the end. For example:
|
||||||
|
```
|
||||||
|
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
|
||||||
|
```
|
||||||
|
|
||||||
Use built-in GitHub feature to request a review from suggested reviewers.
|
Format anyhow errors with `{:#}` to print all the contexts like this:
|
||||||
|
```
|
||||||
|
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
||||||
|
```
|
||||||
|
|
||||||
If you do not have write access to the repository, ask for review in the comments.
|
### Reviewing
|
||||||
|
|
||||||
7. **Merge the PR.**
|
Once a PR has an approval and passes CI, it can be merged.
|
||||||
|
|
||||||
Once a PR has an approval and passes CI, it can be merged.
|
PRs from a branch created in the main repository, i.e. authored by those who have write access, are merged by their authors.
|
||||||
|
This is to ensure that PRs are merged as intended by the author,
|
||||||
|
e.g. as a squash merge, by rebasing from the web interface or manually from the command line.
|
||||||
|
|
||||||
PRs from a branch created in the main repository,
|
If you do not have access to the repository and created a PR from a fork,
|
||||||
i.e. authored by those who have write access, are merged by their authors.
|
ask the maintainers to merge the PR and say how it should be merged.
|
||||||
|
|
||||||
This is to ensure that PRs are merged as intended by the author,
|
|
||||||
e.g. as a squash merge, by rebasing from the web interface or manually from the command line.
|
|
||||||
|
|
||||||
If you have multiple changes in one PR, do a rebase merge.
|
|
||||||
Otherwise, you should usually do a squash merge.
|
|
||||||
|
|
||||||
If PR author does not have write access to the repository,
|
|
||||||
maintainers who reviewed the PR can merge it.
|
|
||||||
|
|
||||||
If you do not have access to the repository and created a PR from a fork,
|
|
||||||
ask the maintainers to merge the PR and say how it should be merged.
|
|
||||||
|
|
||||||
## Other ways to contribute
|
## Other ways to contribute
|
||||||
|
|
||||||
For other ways to contribute, refer to the [website](https://delta.chat/en/contribute).
|
For other ways to contribute, refer to the [website](https://delta.chat/en/contribute).
|
||||||
|
|
||||||
You can find the list of good first issues
|
|
||||||
and a link to this guide
|
|
||||||
on the contributing page: <https://github.com/chatmail/core/contribute>
|
|
||||||
|
|
||||||
[Conventional Commits]: https://www.conventionalcommits.org/
|
|
||||||
[git-cliff]: https://git-cliff.org/
|
|
||||||
|
|||||||
5459
Cargo.lock
generated
5459
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
191
Cargo.toml
191
Cargo.toml
@@ -1,10 +1,10 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "2.31.0"
|
version = "1.137.2"
|
||||||
edition = "2024"
|
edition = "2021"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
rust-version = "1.88"
|
rust-version = "1.77"
|
||||||
repository = "https://github.com/chatmail/core"
|
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
debug = 0
|
debug = 0
|
||||||
@@ -18,9 +18,6 @@ opt-level = 1
|
|||||||
debug = 1
|
debug = 1
|
||||||
opt-level = 0
|
opt-level = 0
|
||||||
|
|
||||||
[profile.fuzz]
|
|
||||||
inherits = "test"
|
|
||||||
|
|
||||||
# Always optimize dependencies.
|
# Always optimize dependencies.
|
||||||
# This does not apply to crates in the workspace.
|
# This does not apply to crates in the workspace.
|
||||||
# <https://doc.rust-lang.org/cargo/reference/profiles.html#overrides>
|
# <https://doc.rust-lang.org/cargo/reference/profiles.html#overrides>
|
||||||
@@ -37,93 +34,95 @@ strip = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
deltachat_derive = { path = "./deltachat_derive" }
|
deltachat_derive = { path = "./deltachat_derive" }
|
||||||
deltachat-time = { path = "./deltachat-time" }
|
deltachat-time = { path = "./deltachat-time" }
|
||||||
deltachat-contact-tools = { workspace = true }
|
|
||||||
format-flowed = { path = "./format-flowed" }
|
format-flowed = { path = "./format-flowed" }
|
||||||
ratelimit = { path = "./deltachat-ratelimit" }
|
ratelimit = { path = "./deltachat-ratelimit" }
|
||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = "1"
|
||||||
async-broadcast = "0.7.2"
|
async-channel = "2.0.0"
|
||||||
async-channel = { workspace = true }
|
async-imap = { version = "0.9.7", default-features = false, features = ["runtime-tokio"] }
|
||||||
async-imap = { version = "0.11.1", default-features = false, features = ["runtime-tokio", "compress"] }
|
|
||||||
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
async-native-tls = { version = "0.5", default-features = false, features = ["runtime-tokio"] }
|
||||||
async-smtp = { version = "0.10.2", default-features = false, features = ["runtime-tokio"] }
|
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
|
||||||
async_zip = { version = "0.0.18", default-features = false, features = ["deflate", "tokio-fs"] }
|
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
|
||||||
base64 = { workspace = true }
|
backtrace = "0.3"
|
||||||
blake3 = "1.8.2"
|
base64 = "0.21"
|
||||||
brotli = { version = "8", default-features=false, features = ["std"] }
|
brotli = { version = "4", default-features=false, features = ["std"] }
|
||||||
bytes = "1"
|
bitflags = "1.3"
|
||||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
bstr = { version = "1.4.0", default-features=false, features = ["std", "alloc"] }
|
||||||
colorutils-rs = { version = "0.7.5", default-features = false }
|
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
|
||||||
data-encoding = "2.9.0"
|
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||||
|
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
||||||
escaper = "0.1"
|
escaper = "0.1"
|
||||||
fast-socks5 = "0.10"
|
fast-socks5 = "0.9"
|
||||||
fd-lock = "4"
|
fd-lock = "4"
|
||||||
futures-lite = { workspace = true }
|
futures = "0.3"
|
||||||
futures = { workspace = true }
|
futures-lite = "2.3.0"
|
||||||
hex = "0.4.0"
|
hex = "0.4.0"
|
||||||
http-body-util = "0.1.3"
|
hickory-resolver = "0.24"
|
||||||
humansize = "2"
|
humansize = "2"
|
||||||
hyper = "1"
|
image = { version = "0.25.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||||
hyper-util = "0.1.16"
|
iroh = { version = "0.4.2", default-features = false }
|
||||||
image = { version = "0.25.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
kamadak-exif = "0.5"
|
||||||
iroh-gossip = { version = "0.35", default-features = false, features = ["net"] }
|
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||||
iroh = { version = "0.35", default-features = false }
|
libc = "0.2"
|
||||||
kamadak-exif = "0.6.1"
|
mailparse = "0.14"
|
||||||
libc = { workspace = true }
|
|
||||||
mail-builder = { version = "0.4.4", default-features = false }
|
|
||||||
mailparse = { workspace = true }
|
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
num_cpus = "1.17"
|
num_cpus = "1.16"
|
||||||
num-derive = "0.4"
|
num-derive = "0.4"
|
||||||
num-traits = { workspace = true }
|
num-traits = "0.2"
|
||||||
parking_lot = "0.12.4"
|
once_cell = "1.18.0"
|
||||||
percent-encoding = "2.3"
|
percent-encoding = "2.3"
|
||||||
pgp = { version = "0.18.0", default-features = false }
|
parking_lot = "0.12"
|
||||||
|
pgp = { version = "0.11", default-features = false }
|
||||||
pin-project = "1"
|
pin-project = "1"
|
||||||
|
pretty_env_logger = { version = "0.5", optional = true }
|
||||||
qrcodegen = "1.7.0"
|
qrcodegen = "1.7.0"
|
||||||
quick-xml = { version = "0.38", features = ["escape-html"] }
|
quick-xml = "0.31"
|
||||||
rand-old = { package = "rand", version = "0.8" }
|
quoted_printable = "0.5"
|
||||||
rand = { workspace = true }
|
rand = "0.8"
|
||||||
regex = { workspace = true }
|
regex = "1.10"
|
||||||
rusqlite = { workspace = true, features = ["sqlcipher"] }
|
reqwest = { version = "0.12.2", features = ["json"] }
|
||||||
rustls-pki-types = "1.12.0"
|
rusqlite = { version = "0.31", features = ["sqlcipher"] }
|
||||||
sanitize-filename = { workspace = true }
|
rust-hsluv = "0.1"
|
||||||
sdp = "0.10.0"
|
sanitize-filename = "0.5"
|
||||||
serde_json = { workspace = true }
|
serde_json = "1"
|
||||||
serde_urlencoded = "0.7.1"
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
|
||||||
sha-1 = "0.10"
|
sha-1 = "0.10"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
shadowsocks = { version = "1.23.1", default-features = false, features = ["aead-cipher", "aead-cipher-2022"] }
|
smallvec = "1"
|
||||||
smallvec = "1.15.1"
|
strum = "0.26"
|
||||||
strum = "0.27"
|
strum_macros = "0.26"
|
||||||
strum_macros = "0.27"
|
|
||||||
tagger = "4.3.4"
|
tagger = "4.3.4"
|
||||||
textwrap = "0.16.2"
|
textwrap = "0.16.1"
|
||||||
thiserror = { workspace = true }
|
thiserror = "1"
|
||||||
tokio-io-timeout = "1.2.1"
|
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||||
tokio-rustls = { version = "0.26.2", default-features = false }
|
tokio-io-timeout = "1.2.0"
|
||||||
tokio-stream = { version = "0.1.17", features = ["fs"] }
|
tokio-stream = { version = "0.1.15", features = ["fs"] }
|
||||||
astral-tokio-tar = { version = "0.5.6", default-features = false }
|
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||||
tokio-util = { workspace = true }
|
tokio-util = "0.7.9"
|
||||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
toml = "0.8"
|
||||||
toml = "0.9"
|
|
||||||
tracing = "0.1.41"
|
|
||||||
url = "2"
|
url = "2"
|
||||||
uuid = { version = "1", features = ["serde", "v4"] }
|
uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
webpki-roots = "0.26.8"
|
|
||||||
|
# Pin OpenSSL to 3.1 releases.
|
||||||
|
# OpenSSL 3.2 has a regression tracked at <https://github.com/openssl/openssl/issues/23376>
|
||||||
|
# which results in broken `deltachat-rpc-server` binaries when cross-compiled using Zig toolchain.
|
||||||
|
# See <https://github.com/deltachat/deltachat-core-rust/issues/5206> for Delta Chat issue.
|
||||||
|
# According to <https://www.openssl.org/policies/releasestrat.html>
|
||||||
|
# 3.1 branch will be supported until 2025-03-14.
|
||||||
|
openssl-src = "~300.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
ansi_term = "0.12.0"
|
||||||
criterion = { version = "0.7.0", features = ["async_tokio"] }
|
anyhow = { version = "1", features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
||||||
futures-lite = { workspace = true }
|
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||||
log = { workspace = true }
|
futures-lite = "2.3.0"
|
||||||
nu-ansi-term = { workspace = true }
|
log = "0.4"
|
||||||
pretty_assertions = "1.4.1"
|
pretty_env_logger = "0.5"
|
||||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||||
tempfile = { workspace = true }
|
tempfile = "3"
|
||||||
testdir = "0.9.3"
|
testdir = "0.9.0"
|
||||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
||||||
|
pretty_assertions = "1.3.0"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
@@ -135,8 +134,6 @@ members = [
|
|||||||
"deltachat-repl",
|
"deltachat-repl",
|
||||||
"deltachat-time",
|
"deltachat-time",
|
||||||
"format-flowed",
|
"format-flowed",
|
||||||
"deltachat-contact-tools",
|
|
||||||
"fuzz",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
@@ -153,22 +150,12 @@ harness = false
|
|||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "receive_emails"
|
name = "receive_emails"
|
||||||
required-features = ["internals"]
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "decrypting"
|
|
||||||
required-features = ["internals"]
|
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "get_chat_msgs"
|
name = "get_chat_msgs"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "marknoticed_chat"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "get_chatlist"
|
name = "get_chatlist"
|
||||||
harness = false
|
harness = false
|
||||||
@@ -177,41 +164,11 @@ harness = false
|
|||||||
name = "send_events"
|
name = "send_events"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[workspace.dependencies]
|
|
||||||
anyhow = "1"
|
|
||||||
async-channel = "2.5.0"
|
|
||||||
base64 = "0.22"
|
|
||||||
chrono = { version = "0.4.42", default-features = false }
|
|
||||||
deltachat-contact-tools = { path = "deltachat-contact-tools" }
|
|
||||||
deltachat-jsonrpc = { path = "deltachat-jsonrpc", default-features = false }
|
|
||||||
deltachat = { path = ".", default-features = false }
|
|
||||||
futures = "0.3.31"
|
|
||||||
futures-lite = "2.6.1"
|
|
||||||
libc = "0.2"
|
|
||||||
log = "0.4"
|
|
||||||
mailparse = "0.16.1"
|
|
||||||
nu-ansi-term = "0.50"
|
|
||||||
num-traits = "0.2"
|
|
||||||
rand = "0.9"
|
|
||||||
regex = "1.10"
|
|
||||||
rusqlite = "0.37"
|
|
||||||
sanitize-filename = "0.6"
|
|
||||||
serde = "1.0"
|
|
||||||
serde_json = "1"
|
|
||||||
tempfile = "3.23.0"
|
|
||||||
thiserror = "2"
|
|
||||||
tokio = "1"
|
|
||||||
tokio-util = "0.7.17"
|
|
||||||
tracing-subscriber = "0.3"
|
|
||||||
yerpc = "0.6.4"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
internals = []
|
internals = []
|
||||||
vendored = [
|
vendored = [
|
||||||
|
"async-native-tls/vendored",
|
||||||
"rusqlite/bundled-sqlcipher-vendored-openssl",
|
"rusqlite/bundled-sqlcipher-vendored-openssl",
|
||||||
"async-native-tls/vendored"
|
"reqwest/native-tls-vendored"
|
||||||
]
|
]
|
||||||
|
|
||||||
[lints.rust]
|
|
||||||
unexpected_cfgs = { level = "warn", check-cfg = ['cfg(fuzzing)'] }
|
|
||||||
|
|||||||
112
README.md
112
README.md
@@ -1,41 +1,16 @@
|
|||||||
<p align="center">
|
<p align="center">
|
||||||
<img alt="Chatmail logo" src="https://github.com/user-attachments/assets/25742da7-a837-48cd-a503-b303af55f10d" width="300" style="float:middle;" />
|
<img alt="Delta Chat Logo" height="200px" src="https://raw.githubusercontent.com/deltachat/deltachat-pages/master/assets/blog/rust-delta.png">
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/chatmail/core/actions/workflows/ci.yml">
|
<a href="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml">
|
||||||
<img alt="Rust CI" src="https://github.com/chatmail/core/actions/workflows/ci.yml/badge.svg">
|
<img alt="Rust CI" src="https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml/badge.svg">
|
||||||
</a>
|
|
||||||
<a href="https://deps.rs/repo/github/chatmail/core">
|
|
||||||
<img alt="dependency status" src="https://deps.rs/repo/github/chatmail/core/status.svg">
|
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
The chatmail core library implements low-level network and encryption protocols,
|
<p align="center">
|
||||||
integrated by many chat bots and higher level applications,
|
The core library for Delta Chat, written in Rust
|
||||||
allowing to securely participate in the globally scaled e-mail server network.
|
</p>
|
||||||
We provide reproducibly-built `deltachat-rpc-server` static binaries
|
|
||||||
that offer a stdio-based high-level JSON-RPC API for instant messaging purposes.
|
|
||||||
|
|
||||||
The following protocols are handled without requiring API users to know much about them:
|
|
||||||
|
|
||||||
- secure TLS setup with DNS caching and shadowsocks/proxy support
|
|
||||||
|
|
||||||
- robust [SMTP](https://github.com/chatmail/async-imap)
|
|
||||||
and [IMAP](https://github.com/chatmail/async-smtp) handling
|
|
||||||
|
|
||||||
- safe and interoperable [MIME parsing](https://github.com/staktrace/mailparse)
|
|
||||||
and [MIME building](https://github.com/stalwartlabs/mail-builder).
|
|
||||||
|
|
||||||
- security-audited end-to-end encryption with [rPGP](https://github.com/rpgp/rpgp)
|
|
||||||
and [Autocrypt and SecureJoin protocols](https://securejoin.rtfd.io)
|
|
||||||
|
|
||||||
- ephemeral [Peer-to-Peer networking using Iroh](https://iroh.computer) for multi-device setup and
|
|
||||||
[webxdc realtime data](https://delta.chat/en/2024-11-20-webxdc-realtime).
|
|
||||||
|
|
||||||
- a simulation- and real-world tested [P2P group membership
|
|
||||||
protocol without requiring server state](https://github.com/chatmail/models/tree/main/group-membership).
|
|
||||||
|
|
||||||
|
|
||||||
## Installing Rust and Cargo
|
## Installing Rust and Cargo
|
||||||
|
|
||||||
@@ -49,26 +24,26 @@ $ curl https://sh.rustup.rs -sSf | sh
|
|||||||
|
|
||||||
## Using the CLI client
|
## Using the CLI client
|
||||||
|
|
||||||
Compile and run the command line utility, using `cargo`:
|
Compile and run Delta Chat Core command line utility, using `cargo`:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ cargo run --locked -p deltachat-repl -- ~/profile-db
|
$ cargo run -p deltachat-repl -- ~/deltachat-db
|
||||||
```
|
```
|
||||||
where ~/profile-db is the database file. The utility will create it if it does not exist.
|
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
|
||||||
|
|
||||||
Optionally, install `deltachat-repl` binary with
|
Optionally, install `deltachat-repl` binary with
|
||||||
```
|
```
|
||||||
$ cargo install --locked --path deltachat-repl/
|
$ cargo install --path deltachat-repl/
|
||||||
```
|
```
|
||||||
and run as
|
and run as
|
||||||
```
|
```
|
||||||
$ deltachat-repl ~/profile-db
|
$ deltachat-repl ~/deltachat-db
|
||||||
```
|
```
|
||||||
|
|
||||||
Configure your account (if not already configured):
|
Configure your account (if not already configured):
|
||||||
|
|
||||||
```
|
```
|
||||||
Chatmail is awaiting your commands.
|
Delta Chat Core is awaiting your commands.
|
||||||
> set addr your@email.org
|
> set addr your@email.org
|
||||||
> set mail_pw yourpassword
|
> set mail_pw yourpassword
|
||||||
> configure
|
> configure
|
||||||
@@ -80,43 +55,37 @@ Connect to your mail server (if already configured):
|
|||||||
> connect
|
> connect
|
||||||
```
|
```
|
||||||
|
|
||||||
Export your public key to a vCard file:
|
Create a contact:
|
||||||
|
|
||||||
```
|
|
||||||
> make-vcard my.vcard 1
|
|
||||||
```
|
|
||||||
|
|
||||||
Create contacts by address or vCard file:
|
|
||||||
|
|
||||||
```
|
```
|
||||||
> addcontact yourfriends@email.org
|
> addcontact yourfriends@email.org
|
||||||
> import-vcard key-contact.vcard
|
Command executed successfully.
|
||||||
```
|
```
|
||||||
|
|
||||||
List contacts:
|
List contacts:
|
||||||
|
|
||||||
```
|
```
|
||||||
> listcontacts
|
> listcontacts
|
||||||
Contact#Contact#11: key-contact@email.org <key-contact@email.org>
|
Contact#10: <name unset> <yourfriends@email.org>
|
||||||
Contact#Contact#Self: Me √ <your@email.org>
|
Contact#1: Me √√ <your@email.org>
|
||||||
2 key contacts.
|
|
||||||
Contact#Contact#10: yourfriends@email.org <yourfriends@email.org>
|
|
||||||
1 address contacts.
|
|
||||||
```
|
```
|
||||||
|
|
||||||
Create a chat with your friend and send a message:
|
Create a chat with your friend and send a message:
|
||||||
|
|
||||||
```
|
```
|
||||||
> createchat 10
|
> createchat 10
|
||||||
Single#Chat#12 created successfully.
|
Single#10 created successfully.
|
||||||
> chat 12
|
> chat 10
|
||||||
Selecting chat Chat#12
|
Single#10: yourfriends@email.org [yourfriends@email.org]
|
||||||
Single#Chat#12: yourfriends@email.org [yourfriends@email.org] Icon: profile-db-blobs/4138c52e5bc1c576cda7dd44d088c07.png
|
|
||||||
0 messages.
|
|
||||||
81.252µs to create this list, 123.625µs to mark all messages as noticed.
|
|
||||||
> send hi
|
> send hi
|
||||||
|
Message sent.
|
||||||
```
|
```
|
||||||
|
|
||||||
|
If `yourfriend@email.org` uses DeltaChat, but does not receive message just
|
||||||
|
sent, it is advisable to check `Spam` folder. It is known that at least
|
||||||
|
`gmx.com` treat such test messages as spam, unless told otherwise with web
|
||||||
|
interface.
|
||||||
|
|
||||||
List messages when inside a chat:
|
List messages when inside a chat:
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -132,7 +101,7 @@ For more commands type:
|
|||||||
## Installing libdeltachat system wide
|
## Installing libdeltachat system wide
|
||||||
|
|
||||||
```
|
```
|
||||||
$ git clone https://github.com/chatmail/core.git
|
$ git clone https://github.com/deltachat/deltachat-core-rust.git
|
||||||
$ cd deltachat-core-rust
|
$ cd deltachat-core-rust
|
||||||
$ cmake -B build . -DCMAKE_INSTALL_PREFIX=/usr
|
$ cmake -B build . -DCMAKE_INSTALL_PREFIX=/usr
|
||||||
$ cmake --build build
|
$ cmake --build build
|
||||||
@@ -173,7 +142,7 @@ $ cargo install cargo-bolero
|
|||||||
Run fuzzing tests with
|
Run fuzzing tests with
|
||||||
```sh
|
```sh
|
||||||
$ cd fuzz
|
$ cd fuzz
|
||||||
$ cargo bolero test fuzz_mailparse -s NONE
|
$ cargo bolero test fuzz_mailparse --release=false -s NONE
|
||||||
```
|
```
|
||||||
|
|
||||||
Corpus is created at `fuzz/fuzz_targets/corpus`,
|
Corpus is created at `fuzz/fuzz_targets/corpus`,
|
||||||
@@ -181,26 +150,38 @@ you can add initial inputs there.
|
|||||||
For `fuzz_mailparse` target corpus can be populated with
|
For `fuzz_mailparse` target corpus can be populated with
|
||||||
`../test-data/message/*.eml`.
|
`../test-data/message/*.eml`.
|
||||||
|
|
||||||
|
To run with AFL instead of libFuzzer:
|
||||||
|
```sh
|
||||||
|
$ cargo bolero test fuzz_format_flowed --release=false -e afl -s NONE
|
||||||
|
```
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
||||||
|
- `nightly`: Enable nightly only performance and security related features.
|
||||||
|
|
||||||
## Update Provider Data
|
## Update Provider Data
|
||||||
|
|
||||||
To add the updates from the
|
To add the updates from the
|
||||||
[provider-db](https://github.com/chatmail/provider-db) to the core,
|
[provider-db](https://github.com/deltachat/provider-db) to the core, run:
|
||||||
check line `REV=` inside `./scripts/update-provider-database.sh`
|
|
||||||
and then run the script.
|
```
|
||||||
|
./src/provider/update.py ../provider-db/_providers/ > src/provider/data.rs
|
||||||
|
```
|
||||||
|
|
||||||
## Language bindings and frontend projects
|
## Language bindings and frontend projects
|
||||||
|
|
||||||
Language bindings are available for:
|
Language bindings are available for:
|
||||||
|
|
||||||
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
||||||
- -> libdeltachat is going to be deprecated and only exists because Android, iOS and Ubuntu Touch are still using it. If you build a new project, then please use the jsonrpc api instead.
|
- **Node.js**
|
||||||
- **JS**: \[[📂 source](./deltachat-rpc-client) | [📦 npm](https://www.npmjs.com/package/@deltachat/jsonrpc-client) | [📚 docs](https://js.jsonrpc.delta.chat/)\]
|
- over cffi: \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
|
||||||
|
- over jsonrpc built with napi.rs (experimental): \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
|
||||||
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
|
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
|
||||||
- **Go** \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
|
- **Go**
|
||||||
|
- over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
|
||||||
|
- over cffi[^1]: \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
|
||||||
|
- **Free Pascal**[^1] \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
|
||||||
- **Java** and **Swift** (contained in the Android/iOS repos)
|
- **Java** and **Swift** (contained in the Android/iOS repos)
|
||||||
|
|
||||||
The following "frontend" projects make use of the Rust-library
|
The following "frontend" projects make use of the Rust-library
|
||||||
@@ -211,5 +192,6 @@ or its language bindings:
|
|||||||
- [Desktop](https://github.com/deltachat/deltachat-desktop)
|
- [Desktop](https://github.com/deltachat/deltachat-desktop)
|
||||||
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
|
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
|
||||||
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
||||||
- [Ubuntu Touch](https://codeberg.org/lk108/deltatouch)
|
|
||||||
- several **Bots**
|
- several **Bots**
|
||||||
|
|
||||||
|
[^1]: Out of date / unmaintained, if you like those languages feel free to start maintaining them. If you have questions we'll help you, please ask in the issues.
|
||||||
|
|||||||
21
RELEASE.md
21
RELEASE.md
@@ -2,20 +2,23 @@
|
|||||||
|
|
||||||
For example, to release version 1.116.0 of the core, do the following steps.
|
For example, to release version 1.116.0 of the core, do the following steps.
|
||||||
|
|
||||||
1. Resolve all [blocker issues](https://github.com/chatmail/core/labels/blocker).
|
1. Resolve all [blocker issues](https://github.com/deltachat/deltachat-core-rust/labels/blocker).
|
||||||
|
|
||||||
2. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
|
2. Run `npm run build:core:constants` in the root of the repository
|
||||||
|
and commit generated `node/constants.js`, `node/events.js` and `node/lib/constants.js`.
|
||||||
|
|
||||||
3. add a link to compare previous with current version to the end of CHANGELOG.md:
|
3. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
|
||||||
`[1.116.0]: https://github.com/chatmail/core/compare/v1.115.2...v1.116.0`
|
|
||||||
|
|
||||||
4. Update the version by running `scripts/set_core_version.py 1.116.0`.
|
4. add a link to compare previous with current version to the end of CHANGELOG.md:
|
||||||
|
`[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.2...v1.116.0`
|
||||||
|
|
||||||
5. Commit the changes as `chore(release): prepare for 1.116.0`.
|
5. Update the version by running `scripts/set_core_version.py 1.116.0`.
|
||||||
|
|
||||||
|
6. Commit the changes as `chore(release): prepare for 1.116.0`.
|
||||||
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
|
Optionally, use a separate branch like `prep-1.116.0` for this commit and open a PR for review.
|
||||||
|
|
||||||
6. Tag the release: `git tag --annotate v1.116.0`.
|
7. Tag the release: `git tag -a v1.116.0`.
|
||||||
|
|
||||||
7. Push the release tag: `git push origin v1.116.0`.
|
8. Push the release tag: `git push origin v1.116.0`.
|
||||||
|
|
||||||
8. Create a GitHub release: `gh release create v1.116.0 --notes ''`.
|
9. Create a GitHub release: `gh release create v1.116.0 -n ''`.
|
||||||
|
|||||||
152
STYLE.md
152
STYLE.md
@@ -1,152 +0,0 @@
|
|||||||
# Coding conventions
|
|
||||||
|
|
||||||
We format the code using `rustfmt`. Run `cargo fmt` prior to committing the code.
|
|
||||||
Run `scripts/clippy.sh` to check the code for common mistakes with [Clippy].
|
|
||||||
|
|
||||||
[Clippy]: https://doc.rust-lang.org/clippy/
|
|
||||||
|
|
||||||
## SQL
|
|
||||||
|
|
||||||
Multi-line SQL statements should be formatted using string literals,
|
|
||||||
for example
|
|
||||||
```
|
|
||||||
sql.execute(
|
|
||||||
"CREATE TABLE messages (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
||||||
text TEXT DEFAULT '' NOT NULL -- message text
|
|
||||||
) STRICT",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
```
|
|
||||||
|
|
||||||
Do not use macros like [`concat!`](https://doc.rust-lang.org/std/macro.concat.html)
|
|
||||||
or [`indoc!](https://docs.rs/indoc).
|
|
||||||
Do not escape newlines like this:
|
|
||||||
```
|
|
||||||
sql.execute(
|
|
||||||
"CREATE TABLE messages ( \
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT, \
|
|
||||||
text TEXT DEFAULT '' NOT NULL \
|
|
||||||
) STRICT",
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
```
|
|
||||||
Escaping newlines
|
|
||||||
is prone to errors like this if space before backslash is missing:
|
|
||||||
```
|
|
||||||
"SELECT foo\
|
|
||||||
FROM bar"
|
|
||||||
```
|
|
||||||
Literal above results in `SELECT fooFROM bar` string.
|
|
||||||
This style also does not allow using `--` comments.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Declare new SQL tables with [`STRICT`](https://sqlite.org/stricttables.html) keyword
|
|
||||||
to make SQLite check column types.
|
|
||||||
|
|
||||||
Declare primary keys with [`AUTOINCREMENT`](https://www.sqlite.org/autoinc.html) keyword.
|
|
||||||
This avoids reuse of the row IDs and can avoid dangerous bugs
|
|
||||||
like forwarding wrong message because the message was deleted
|
|
||||||
and another message took its row ID.
|
|
||||||
|
|
||||||
Declare all new columns as `NOT NULL`
|
|
||||||
and set the `DEFAULT` value if it is optional so the column can be skipped in `INSERT` statements.
|
|
||||||
Dealing with `NULL` values both in SQL and in Rust is tricky and we try to avoid it.
|
|
||||||
If column is already declared without `NOT NULL`, use `IFNULL` function to provide default value when selecting it.
|
|
||||||
Use `HAVING COUNT(*) > 0` clause
|
|
||||||
to [prevent aggregate functions such as `MIN` and `MAX` from returning `NULL`](https://stackoverflow.com/questions/66527856/aggregate-functions-max-etc-return-null-instead-of-no-rows).
|
|
||||||
|
|
||||||
Don't delete unused columns too early, but maybe after several months/releases, unused columns are
|
|
||||||
still used by older versions, so deleting them breaks downgrading the core or importing a backup in
|
|
||||||
an older version. Also don't change the column type, consider adding a new column with another name
|
|
||||||
instead. Finally, never change column semantics, this is especially dangerous because the `STRICT`
|
|
||||||
keyword doesn't help here.
|
|
||||||
|
|
||||||
## Errors
|
|
||||||
|
|
||||||
Delta Chat core mostly uses [`anyhow`](https://docs.rs/anyhow/) errors.
|
|
||||||
When using [`Context`](https://docs.rs/anyhow/latest/anyhow/trait.Context.html),
|
|
||||||
capitalize it but do not add a full stop as the contexts will be separated by `:`.
|
|
||||||
For example:
|
|
||||||
```
|
|
||||||
.with_context(|| format!("Unable to trash message {msg_id}"))
|
|
||||||
```
|
|
||||||
|
|
||||||
All errors should be handled in one of these ways:
|
|
||||||
- With `if let Err() =` (incl. logging them into `warn!()`/`err!()`).
|
|
||||||
- With `.log_err().ok()`.
|
|
||||||
- Bubbled up with `?`.
|
|
||||||
|
|
||||||
When working with [async streams](https://docs.rs/futures/0.3.31/futures/stream/index.html),
|
|
||||||
prefer [`try_next`](https://docs.rs/futures/0.3.31/futures/stream/trait.TryStreamExt.html#method.try_next) over `next()`, e.g. do
|
|
||||||
```
|
|
||||||
while let Some(event) = stream.try_next().await? {
|
|
||||||
todo!();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
instead of
|
|
||||||
```
|
|
||||||
while let Some(event_res) = stream.next().await {
|
|
||||||
todo!();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
as it allows bubbling up the error early with `?`
|
|
||||||
with no way to accidentally skip error processing
|
|
||||||
with early `continue` or `break`.
|
|
||||||
Some streams reading from a connection
|
|
||||||
return infinite number of `Some(Err(_))`
|
|
||||||
items when connection breaks and not processing
|
|
||||||
errors may result in infinite loop.
|
|
||||||
|
|
||||||
`backtrace` feature is enabled for `anyhow` crate
|
|
||||||
and `debug = 1` option is set in the test profile.
|
|
||||||
This allows to run `RUST_BACKTRACE=1 cargo test`
|
|
||||||
and get a backtrace with line numbers in resultified tests
|
|
||||||
which return `anyhow::Result`.
|
|
||||||
|
|
||||||
`unwrap` and `expect` are not used in the library
|
|
||||||
because panics are difficult to debug on user devices.
|
|
||||||
However, in the tests `.expect` may be used.
|
|
||||||
Follow
|
|
||||||
<https://doc.rust-lang.org/core/error/index.html#common-message-styles>
|
|
||||||
for `.expect` message style.
|
|
||||||
|
|
||||||
## BTreeMap vs HashMap
|
|
||||||
|
|
||||||
Prefer [BTreeMap](https://doc.rust-lang.org/std/collections/struct.BTreeMap.html)
|
|
||||||
over [HashMap](https://doc.rust-lang.org/std/collections/struct.HashMap.html)
|
|
||||||
and [BTreeSet](https://doc.rust-lang.org/std/collections/struct.BTreeSet.html)
|
|
||||||
over [HashSet](https://doc.rust-lang.org/std/collections/struct.HashSet.html)
|
|
||||||
as iterating over these structures returns items in deterministic order.
|
|
||||||
|
|
||||||
Non-deterministic code may result in difficult to reproduce bugs,
|
|
||||||
flaky tests, regression tests that miss bugs
|
|
||||||
or different behavior on different devices when processing the same messages.
|
|
||||||
|
|
||||||
## Logging
|
|
||||||
|
|
||||||
For logging, use `info!`, `warn!` and `error!` macros.
|
|
||||||
Log messages should be capitalized and have a full stop in the end. For example:
|
|
||||||
```
|
|
||||||
info!(context, "Ignoring addition of {added_addr:?} to {chat_id}.");
|
|
||||||
```
|
|
||||||
|
|
||||||
Format anyhow errors with `{:#}` to print all the contexts like this:
|
|
||||||
```
|
|
||||||
error!(context, "Failed to set selfavatar timestamp: {err:#}.");
|
|
||||||
```
|
|
||||||
|
|
||||||
## Documentation comments
|
|
||||||
|
|
||||||
All public modules, methods and fields should be documented.
|
|
||||||
This is checked by [`missing_docs`](https://doc.rust-lang.org/rustdoc/lints.html#missing_docs) lint.
|
|
||||||
|
|
||||||
Private items do not have to be documented,
|
|
||||||
but CI uses `cargo doc --document-private-items`
|
|
||||||
to build the documentation,
|
|
||||||
so it is preferred that new items
|
|
||||||
are documented.
|
|
||||||
|
|
||||||
Follow Rust guidelines for the documentation comments:
|
|
||||||
<https://rust-lang.github.io/rfcs/1574-more-api-documentation-conventions.html#summary-sentence>
|
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.2 KiB |
@@ -1,47 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
height="480"
|
|
||||||
viewBox="0 -960 9600 9600"
|
|
||||||
width="480"
|
|
||||||
fill="#ffffff"
|
|
||||||
version="1.1"
|
|
||||||
id="svg1"
|
|
||||||
sodipodi:docname="icon-email.svg"
|
|
||||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg">
|
|
||||||
<defs
|
|
||||||
id="defs1" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview1"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:showpageshadow="2"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pagecheckerboard="0"
|
|
||||||
inkscape:deskcolor="#d1d1d1"
|
|
||||||
inkscape:zoom="0.99091847"
|
|
||||||
inkscape:cx="263.392"
|
|
||||||
inkscape:cy="177.613"
|
|
||||||
inkscape:window-width="1884"
|
|
||||||
inkscape:window-height="1052"
|
|
||||||
inkscape:window-x="36"
|
|
||||||
inkscape:window-y="0"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="svg1" />
|
|
||||||
<rect
|
|
||||||
style="fill:#8c8c8c;fill-opacity:1;stroke:none;stroke-width:680.523;stroke-dasharray:none;paint-order:markers fill stroke"
|
|
||||||
id="rect1"
|
|
||||||
width="9951.9541"
|
|
||||||
height="9767.4756"
|
|
||||||
x="-71.697792"
|
|
||||||
y="-1012.83"
|
|
||||||
ry="0.43547946" />
|
|
||||||
<path
|
|
||||||
d="m 2948.0033,5553.6941 q -130.7292,0 -228.7761,-96.3953 -98.0468,-96.3953 -98.0468,-224.9223 V 2447.6234 q 0,-128.527 98.0468,-224.9223 98.0469,-96.3953 228.7761,-96.3953 h 3703.9934 q 130.7292,0 228.776,96.3953 98.0469,96.3953 98.0469,224.9223 v 2784.7531 q 0,128.527 -98.0469,224.9223 -98.0468,96.3953 -228.776,96.3953 z M 4800,3936.3952 2948.0033,2742.1646 V 5232.3765 H 6651.9967 V 2742.1646 Z m 0,-321.3176 1830.2085,-1167.4541 h -3654.97 z m -1851.9967,-872.913 v -294.5412 2784.7531 z"
|
|
||||||
id="path1"
|
|
||||||
style="stroke-width:5.40098" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
@@ -1,12 +0,0 @@
|
|||||||
<path
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke:none"
|
|
||||||
d="m 24.015419,1.2870249 c -12.549421,0 -22.7283936,10.1789711 -22.7283936,22.7283931 0,12.549422 10.1789726,22.728395 22.7283936,22.728395 14.337742,-0.342877 9.614352,-4.702705 23.697556,0.969161 -7.545453,-13.001555 -1.082973,-13.32964 -0.969161,-23.697556 0,-12.549422 -10.178973,-22.7283931 -22.728395,-22.7283931 z" />
|
|
||||||
<path
|
|
||||||
style="fill:#000000;fill-opacity:1;stroke:none"
|
|
||||||
d="M 23.982249,5.3106163 C 13.645822,5.4364005 5.2618355,13.92999 5.2618355,24.275753 c 0,10.345764 8.3839865,18.635301 18.7204135,18.509516 9.827724,-0.03951 7.516769,-5.489695 18.380082,-0.443187 -5.950849,-9.296115 0.201753,-10.533667 0.340336,-18.521947 0,-10.345766 -8.383989,-18.6353031 -18.720418,-18.5095187 z" />
|
|
||||||
<g
|
|
||||||
style="fill:#ffffff"
|
|
||||||
transform="scale(1.1342891,0.88160947)">
|
|
||||||
<path
|
|
||||||
d="m 21.360141,23.513382 q -1.218487,-1.364705 -3.387392,-3.265543 -2.388233,-2.095797 -3.216804,-3.289913 -0.828571,-1.218486 -0.828571,-2.6563 0,-2.144536 1.998318,-3.363022 1.998317,-1.2428565 5.215121,-1.2428565 3.216804,0 5.605037,1.0966375 2.412603,1.096638 2.412603,3.021846 0,0.92605 -0.584873,1.535293 -0.584874,0.609243 -1.364705,0.609243 -1.121008,0 -2.631931,-1.681511 -1.535292,-1.705881 -2.60756,-2.388233 -1.047898,-0.706722 -2.461343,-0.706722 -1.803359,0 -2.973106,0.804201 -1.145377,0.804201 -1.145377,2.047057 0,1.169747 0.950419,2.193275 0.950419,1.023529 4.898315,3.728568 4.215963,2.899998 5.946213,4.532769 1.75462,1.632772 2.851258,3.972265 1.096638,2.339494 1.096638,4.947055 0,4.581508 -3.241174,8.090749 -3.216804,3.484871 -7.530245,3.484871 -3.923526,0 -6.628566,-2.802519 -2.705039,-2.802518 -2.705039,-7.481506 0,-4.508399 2.973106,-7.530245 2.997477,-3.021846 7.359658,-3.655459 z m 1.072268,1.121008 q -6.994112,1.145377 -6.994112,9.601672 0,4.36218 1.730251,6.774783 1.75462,2.412603 4.069744,2.412603 2.412603,0 3.972265,-2.315124 1.559663,-2.339493 1.559663,-6.311759 0,-5.751255 -4.337811,-10.162175 z" />
|
|
||||||
</g>
|
|
||||||
BIN
assets/root-certificates/letsencrypt/isrgrootx1.der
Normal file
BIN
assets/root-certificates/letsencrypt/isrgrootx1.der
Normal file
Binary file not shown.
@@ -1,7 +0,0 @@
|
|||||||
BEGIN:VCARD
|
|
||||||
VERSION:4.0
|
|
||||||
EMAIL:self_reporting@testrun.org
|
|
||||||
FN:Statistics bot
|
|
||||||
KEY:data:application/pgp-keys;base64,xjMEZbfBlBYJKwYBBAHaRw8BAQdABpLWS2PUIGGo4pslVt4R8sylP5wZihmhf1DTDr3oCMPNHDxzZWxmX3JlcG9ydGluZ0B0ZXN0cnVuLm9yZz7CiwQQFggAMwIZAQUCZbfBlAIbAwQLCQgHBhUICQoLAgMWAgEWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohD8dAQCQV7CoH6UP4PD+NqI4kW5tbbqdh2AnDROg60qotmLExAEAxDfd3QHAK9f8b9qQUbLmHIztCLxhEuVbWPBEYeVW0gvOOARlt8GUEgorBgEEAZdVAQUBAQdAMBUhYoAAcI625vGZqnM5maPX4sGJ7qvJxPAFILPy6AcDAQgHwngEGBYIACAFAmW3wZQCGwwWIQTS2i16sHeYTckGn284K3M5Z4oohAAKCRA4K3M5Z4oohPwCAQCvzk1ObIkj2GqsuIfaULlgdnfdZY8LNary425CEfHZDQD5AblXVrlMO1frdlc/Vo9z3pEeCrfYdD7ITD3/OeVoiQ4=
|
|
||||||
REV:20250412T195751Z
|
|
||||||
END:VCARD
|
|
||||||
@@ -1,11 +1,8 @@
|
|||||||
#![recursion_limit = "256"]
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
use std::hint::black_box;
|
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
|
||||||
use deltachat::Events;
|
|
||||||
use deltachat::contact::Contact;
|
use deltachat::contact::Contact;
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
|
use deltachat::Events;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
async fn address_book_benchmark(n: u32, read_count: u32) {
|
async fn address_book_benchmark(n: u32, read_count: u32) {
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
#![recursion_limit = "256"]
|
|
||||||
use std::hint::black_box;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
use deltachat::accounts::Accounts;
|
use deltachat::accounts::Accounts;
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
|||||||
@@ -1,201 +0,0 @@
|
|||||||
//! Benchmarks for message decryption,
|
|
||||||
//! comparing decryption of symmetrically-encrypted messages
|
|
||||||
//! to decryption of asymmetrically-encrypted messages.
|
|
||||||
//!
|
|
||||||
//! Call with
|
|
||||||
//!
|
|
||||||
//! ```text
|
|
||||||
//! cargo bench --bench decrypting --features="internals"
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! or, if you want to only run e.g. the 'Decrypt a symmetrically encrypted message' benchmark:
|
|
||||||
//!
|
|
||||||
//! ```text
|
|
||||||
//! cargo bench --bench decrypting --features="internals" -- 'Decrypt a symmetrically encrypted message'
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! You can also pass a substring.
|
|
||||||
//! So, you can run all 'Decrypt and parse' benchmarks with:
|
|
||||||
//!
|
|
||||||
//! ```text
|
|
||||||
//! cargo bench --bench decrypting --features="internals" -- 'Decrypt and parse'
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! Symmetric decryption has to try out all known secrets,
|
|
||||||
//! You can benchmark this by adapting the `NUM_SECRETS` variable.
|
|
||||||
|
|
||||||
use std::hint::black_box;
|
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
|
||||||
use deltachat::internals_for_benches::create_broadcast_secret;
|
|
||||||
use deltachat::internals_for_benches::create_dummy_keypair;
|
|
||||||
use deltachat::internals_for_benches::save_broadcast_secret;
|
|
||||||
use deltachat::{
|
|
||||||
Events,
|
|
||||||
chat::ChatId,
|
|
||||||
config::Config,
|
|
||||||
context::Context,
|
|
||||||
internals_for_benches::key_from_asc,
|
|
||||||
internals_for_benches::parse_and_get_text,
|
|
||||||
internals_for_benches::store_self_keypair,
|
|
||||||
pgp::{KeyPair, SeipdVersion, decrypt, pk_encrypt, symm_encrypt_message},
|
|
||||||
stock_str::StockStrings,
|
|
||||||
};
|
|
||||||
use rand::{Rng, rng};
|
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
const NUM_SECRETS: usize = 500;
|
|
||||||
|
|
||||||
async fn create_context() -> Context {
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
let dbfile = dir.path().join("db.sqlite");
|
|
||||||
let context = Context::new(dbfile.as_path(), 100, Events::new(), StockStrings::new())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
context
|
|
||||||
.set_config(Config::ConfiguredAddr, Some("bob@example.net"))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let secret = key_from_asc(include_str!("../test-data/key/bob-secret.asc")).unwrap();
|
|
||||||
let public = secret.signed_public_key();
|
|
||||||
let key_pair = KeyPair { public, secret };
|
|
||||||
store_self_keypair(&context, &key_pair)
|
|
||||||
.await
|
|
||||||
.expect("Failed to save key");
|
|
||||||
|
|
||||||
context
|
|
||||||
}
|
|
||||||
|
|
||||||
fn criterion_benchmark(c: &mut Criterion) {
|
|
||||||
let mut group = c.benchmark_group("Decrypt");
|
|
||||||
|
|
||||||
// ===========================================================================================
|
|
||||||
// Benchmarks for decryption only, without any other parsing
|
|
||||||
// ===========================================================================================
|
|
||||||
|
|
||||||
group.sample_size(10);
|
|
||||||
|
|
||||||
group.bench_function("Decrypt a symmetrically encrypted message", |b| {
|
|
||||||
let plain = generate_plaintext();
|
|
||||||
let secrets = generate_secrets();
|
|
||||||
let encrypted = tokio::runtime::Runtime::new().unwrap().block_on(async {
|
|
||||||
let secret = secrets[NUM_SECRETS / 2].clone();
|
|
||||||
symm_encrypt_message(
|
|
||||||
plain.clone(),
|
|
||||||
create_dummy_keypair("alice@example.org").unwrap().secret,
|
|
||||||
black_box(&secret),
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
b.iter(|| {
|
|
||||||
let mut msg =
|
|
||||||
decrypt(encrypted.clone().into_bytes(), &[], black_box(&secrets)).unwrap();
|
|
||||||
let decrypted = msg.as_data_vec().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(black_box(decrypted), plain);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group.bench_function("Decrypt a public-key encrypted message", |b| {
|
|
||||||
let plain = generate_plaintext();
|
|
||||||
let key_pair = create_dummy_keypair("alice@example.org").unwrap();
|
|
||||||
let secrets = generate_secrets();
|
|
||||||
let encrypted = tokio::runtime::Runtime::new().unwrap().block_on(async {
|
|
||||||
pk_encrypt(
|
|
||||||
plain.clone(),
|
|
||||||
vec![black_box(key_pair.public.clone())],
|
|
||||||
key_pair.secret.clone(),
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
SeipdVersion::V2,
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
b.iter(|| {
|
|
||||||
let mut msg = decrypt(
|
|
||||||
encrypted.clone().into_bytes(),
|
|
||||||
std::slice::from_ref(&key_pair.secret),
|
|
||||||
black_box(&secrets),
|
|
||||||
)
|
|
||||||
.unwrap();
|
|
||||||
let decrypted = msg.as_data_vec().unwrap();
|
|
||||||
|
|
||||||
assert_eq!(black_box(decrypted), plain);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// ===========================================================================================
|
|
||||||
// Benchmarks for the whole parsing pipeline, incl. decryption (but excl. receive_imf())
|
|
||||||
// ===========================================================================================
|
|
||||||
|
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
||||||
let mut secrets = generate_secrets();
|
|
||||||
|
|
||||||
// "secret" is the shared secret that was used to encrypt text_symmetrically_encrypted.eml.
|
|
||||||
// Put it into the middle of our secrets:
|
|
||||||
secrets[NUM_SECRETS / 2] = "secret".to_string();
|
|
||||||
|
|
||||||
let context = rt.block_on(async {
|
|
||||||
let context = create_context().await;
|
|
||||||
for (i, secret) in secrets.iter().enumerate() {
|
|
||||||
save_broadcast_secret(&context, ChatId::new(10 + i as u32), secret)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
context
|
|
||||||
});
|
|
||||||
|
|
||||||
group.bench_function("Decrypt and parse a symmetrically encrypted message", |b| {
|
|
||||||
b.to_async(&rt).iter(|| {
|
|
||||||
let ctx = context.clone();
|
|
||||||
async move {
|
|
||||||
let text = parse_and_get_text(
|
|
||||||
&ctx,
|
|
||||||
include_bytes!("../test-data/message/text_symmetrically_encrypted.eml"),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(text, "Symmetrically encrypted message");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group.bench_function("Decrypt and parse a public-key encrypted message", |b| {
|
|
||||||
b.to_async(&rt).iter(|| {
|
|
||||||
let ctx = context.clone();
|
|
||||||
async move {
|
|
||||||
let text = parse_and_get_text(
|
|
||||||
&ctx,
|
|
||||||
include_bytes!("../test-data/message/text_from_alice_encrypted.eml"),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(text, "hi");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_secrets() -> Vec<String> {
|
|
||||||
let secrets: Vec<String> = (0..NUM_SECRETS)
|
|
||||||
.map(|_| create_broadcast_secret())
|
|
||||||
.collect();
|
|
||||||
secrets
|
|
||||||
}
|
|
||||||
|
|
||||||
fn generate_plaintext() -> Vec<u8> {
|
|
||||||
let mut plain: Vec<u8> = vec![0; 500];
|
|
||||||
rng().fill(&mut plain[..]);
|
|
||||||
plain
|
|
||||||
}
|
|
||||||
|
|
||||||
criterion_group!(benches, criterion_benchmark);
|
|
||||||
criterion_main!(benches);
|
|
||||||
@@ -1,13 +1,11 @@
|
|||||||
#![recursion_limit = "256"]
|
|
||||||
use std::hint::black_box;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
use deltachat::Events;
|
|
||||||
use deltachat::chat::{self, ChatId};
|
use deltachat::chat::{self, ChatId};
|
||||||
use deltachat::chatlist::Chatlist;
|
use deltachat::chatlist::Chatlist;
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
|
use deltachat::Events;
|
||||||
|
|
||||||
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
|
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
|
||||||
let id = 100;
|
let id = 100;
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
#![recursion_limit = "256"]
|
|
||||||
use std::hint::black_box;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
use deltachat::Events;
|
|
||||||
use deltachat::chatlist::Chatlist;
|
use deltachat::chatlist::Chatlist;
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
|
use deltachat::Events;
|
||||||
|
|
||||||
async fn get_chat_list_benchmark(context: &Context) {
|
async fn get_chat_list_benchmark(context: &Context) {
|
||||||
Chatlist::try_load(context, 0, None, None).await.unwrap();
|
Chatlist::try_load(context, 0, None, None).await.unwrap();
|
||||||
|
|||||||
@@ -1,95 +0,0 @@
|
|||||||
#![recursion_limit = "256"]
|
|
||||||
use std::hint::black_box;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use criterion::{BatchSize, Criterion, criterion_group, criterion_main};
|
|
||||||
use deltachat::Events;
|
|
||||||
use deltachat::chat::{self, ChatId};
|
|
||||||
use deltachat::chatlist::Chatlist;
|
|
||||||
use deltachat::context::Context;
|
|
||||||
use deltachat::stock_str::StockStrings;
|
|
||||||
use futures_lite::future::block_on;
|
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
async fn marknoticed_chat_benchmark(context: &Context, chats: &[ChatId]) {
|
|
||||||
for c in chats.iter().take(20) {
|
|
||||||
chat::marknoticed_chat(context, *c).await.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn criterion_benchmark(c: &mut Criterion) {
|
|
||||||
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
|
|
||||||
// messages, such as your primary account.
|
|
||||||
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
|
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
|
||||||
|
|
||||||
let chats: Vec<_> = rt.block_on(async {
|
|
||||||
let context = Context::new(Path::new(&path), 100, Events::new(), StockStrings::new())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap();
|
|
||||||
let len = chatlist.len();
|
|
||||||
(1..len).map(|i| chatlist.get_chat_id(i).unwrap()).collect()
|
|
||||||
});
|
|
||||||
|
|
||||||
// This mainly tests the performance of marknoticed_chat()
|
|
||||||
// when nothing has to be done
|
|
||||||
c.bench_function(
|
|
||||||
"chat::marknoticed_chat (mark 20 chats as noticed repeatedly)",
|
|
||||||
|b| {
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
let dir = dir.path();
|
|
||||||
let new_db = dir.join("dc.db");
|
|
||||||
std::fs::copy(&path, &new_db).unwrap();
|
|
||||||
|
|
||||||
let context = block_on(async {
|
|
||||||
Context::new(Path::new(&new_db), 100, Events::new(), StockStrings::new())
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
|
|
||||||
b.to_async(&rt)
|
|
||||||
.iter(|| marknoticed_chat_benchmark(&context, black_box(&chats)))
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// If the first 20 chats contain fresh messages or reactions,
|
|
||||||
// this tests the performance of marking them as noticed.
|
|
||||||
c.bench_function(
|
|
||||||
"chat::marknoticed_chat (mark 20 chats as noticed, resetting after every iteration)",
|
|
||||||
|b| {
|
|
||||||
b.to_async(&rt).iter_batched(
|
|
||||||
|| {
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
let new_db = dir.path().join("dc.db");
|
|
||||||
std::fs::copy(&path, &new_db).unwrap();
|
|
||||||
|
|
||||||
let context = block_on(async {
|
|
||||||
Context::new(
|
|
||||||
Path::new(&new_db),
|
|
||||||
100,
|
|
||||||
Events::new(),
|
|
||||||
StockStrings::new(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
});
|
|
||||||
(dir, context)
|
|
||||||
},
|
|
||||||
|(_dir, context)| {
|
|
||||||
let chats = &chats;
|
|
||||||
async move {
|
|
||||||
marknoticed_chat_benchmark(black_box(&context), black_box(chats)).await
|
|
||||||
}
|
|
||||||
},
|
|
||||||
BatchSize::PerIteration,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
criterion_group!(benches, criterion_benchmark);
|
|
||||||
criterion_main!(benches);
|
|
||||||
@@ -1,30 +1,28 @@
|
|||||||
#![recursion_limit = "256"]
|
|
||||||
use std::hint::black_box;
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
use deltachat::{
|
use deltachat::{
|
||||||
Events,
|
|
||||||
config::Config,
|
config::Config,
|
||||||
context::Context,
|
context::Context,
|
||||||
imex::{ImexMode, imex},
|
imex::{imex, ImexMode},
|
||||||
receive_imf::receive_imf,
|
receive_imf::receive_imf,
|
||||||
stock_str::StockStrings,
|
stock_str::StockStrings,
|
||||||
|
Events,
|
||||||
};
|
};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
async fn recv_all_emails(context: Context, iteration: u32) -> Context {
|
async fn recv_all_emails(context: Context) -> Context {
|
||||||
for i in 0..100 {
|
for i in 0..100 {
|
||||||
let imf_raw = format!(
|
let imf_raw = format!(
|
||||||
"Subject: Benchmark
|
"Subject: Benchmark
|
||||||
Message-ID: Mr.{iteration}.{i}@testrun.org
|
Message-ID: Mr.OssSYnOFkhR.{i}@testrun.org
|
||||||
Date: Sat, 07 Dec 2019 19:00:27 +0000
|
Date: Sat, 07 Dec 2019 19:00:27 +0000
|
||||||
To: alice@example.com
|
To: alice@example.com
|
||||||
From: sender@testrun.org
|
From: sender@testrun.org
|
||||||
Chat-Version: 1.0
|
Chat-Version: 1.0
|
||||||
Chat-Disposition-Notification-To: sender@testrun.org
|
Chat-Disposition-Notification-To: sender@testrun.org
|
||||||
Chat-User-Avatar: 0
|
Chat-User-Avatar: 0
|
||||||
In-Reply-To: Mr.{iteration}.{i_dec}@testrun.org
|
In-Reply-To: Mr.OssSYnOFkhR.{i_dec}@testrun.org
|
||||||
MIME-Version: 1.0
|
MIME-Version: 1.0
|
||||||
|
|
||||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||||
@@ -42,11 +40,11 @@ Hello {i}",
|
|||||||
|
|
||||||
/// Receive 100 emails that remove charlie@example.com and add
|
/// Receive 100 emails that remove charlie@example.com and add
|
||||||
/// him back
|
/// him back
|
||||||
async fn recv_groupmembership_emails(context: Context, iteration: u32) -> Context {
|
async fn recv_groupmembership_emails(context: Context) -> Context {
|
||||||
for i in 0..50 {
|
for i in 0..50 {
|
||||||
let imf_raw = format!(
|
let imf_raw = format!(
|
||||||
"Subject: Benchmark
|
"Subject: Benchmark
|
||||||
Message-ID: Gr.{iteration}.ADD.{i}@testrun.org
|
Message-ID: Gr.OssSYnOFkhR.{i}@testrun.org
|
||||||
Date: Sat, 07 Dec 2019 19:00:27 +0000
|
Date: Sat, 07 Dec 2019 19:00:27 +0000
|
||||||
To: alice@example.com, b@example.com, c@example.com, d@example.com, e@example.com, f@example.com
|
To: alice@example.com, b@example.com, c@example.com, d@example.com, e@example.com, f@example.com
|
||||||
From: sender@testrun.org
|
From: sender@testrun.org
|
||||||
@@ -54,12 +52,13 @@ Chat-Version: 1.0
|
|||||||
Chat-Disposition-Notification-To: sender@testrun.org
|
Chat-Disposition-Notification-To: sender@testrun.org
|
||||||
Chat-User-Avatar: 0
|
Chat-User-Avatar: 0
|
||||||
Chat-Group-Member-Added: charlie@example.com
|
Chat-Group-Member-Added: charlie@example.com
|
||||||
In-Reply-To: Gr.{iteration}.REMOVE.{i_dec}@testrun.org
|
In-Reply-To: Gr.OssSYnOFkhR.{i_dec}@testrun.org
|
||||||
MIME-Version: 1.0
|
MIME-Version: 1.0
|
||||||
|
|
||||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||||
|
|
||||||
Hello {i}",
|
Hello {i}",
|
||||||
|
i = i,
|
||||||
i_dec = i - 1,
|
i_dec = i - 1,
|
||||||
);
|
);
|
||||||
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
|
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
|
||||||
@@ -68,7 +67,7 @@ Hello {i}",
|
|||||||
|
|
||||||
let imf_raw = format!(
|
let imf_raw = format!(
|
||||||
"Subject: Benchmark
|
"Subject: Benchmark
|
||||||
Message-ID: Gr.{iteration}.REMOVE.{i}@testrun.org
|
Message-ID: Gr.OssSYnOFkhR.{i}@testrun.org
|
||||||
Date: Sat, 07 Dec 2019 19:00:27 +0000
|
Date: Sat, 07 Dec 2019 19:00:27 +0000
|
||||||
To: alice@example.com, b@example.com, c@example.com, d@example.com, e@example.com, f@example.com
|
To: alice@example.com, b@example.com, c@example.com, d@example.com, e@example.com, f@example.com
|
||||||
From: sender@testrun.org
|
From: sender@testrun.org
|
||||||
@@ -76,12 +75,14 @@ Chat-Version: 1.0
|
|||||||
Chat-Disposition-Notification-To: sender@testrun.org
|
Chat-Disposition-Notification-To: sender@testrun.org
|
||||||
Chat-User-Avatar: 0
|
Chat-User-Avatar: 0
|
||||||
Chat-Group-Member-Removed: charlie@example.com
|
Chat-Group-Member-Removed: charlie@example.com
|
||||||
In-Reply-To: Gr.{iteration}.ADD.{i}@testrun.org
|
In-Reply-To: Gr.OssSYnOFkhR.{i_dec}@testrun.org
|
||||||
MIME-Version: 1.0
|
MIME-Version: 1.0
|
||||||
|
|
||||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
||||||
|
|
||||||
Hello {i}"
|
Hello {i}",
|
||||||
|
i = i,
|
||||||
|
i_dec = i - 1,
|
||||||
);
|
);
|
||||||
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
|
receive_imf(&context, black_box(imf_raw.as_bytes()), false)
|
||||||
.await
|
.await
|
||||||
@@ -127,13 +128,11 @@ fn criterion_benchmark(c: &mut Criterion) {
|
|||||||
group.bench_function("Receive 100 simple text msgs", |b| {
|
group.bench_function("Receive 100 simple text msgs", |b| {
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
let context = rt.block_on(create_context());
|
let context = rt.block_on(create_context());
|
||||||
let mut i = 0;
|
|
||||||
|
|
||||||
b.to_async(&rt).iter(|| {
|
b.to_async(&rt).iter(|| {
|
||||||
let ctx = context.clone();
|
let ctx = context.clone();
|
||||||
i += 1;
|
|
||||||
async move {
|
async move {
|
||||||
recv_all_emails(black_box(ctx), i).await;
|
recv_all_emails(black_box(ctx)).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -142,13 +141,11 @@ fn criterion_benchmark(c: &mut Criterion) {
|
|||||||
|b| {
|
|b| {
|
||||||
let rt = tokio::runtime::Runtime::new().unwrap();
|
let rt = tokio::runtime::Runtime::new().unwrap();
|
||||||
let context = rt.block_on(create_context());
|
let context = rt.block_on(create_context());
|
||||||
let mut i = 0;
|
|
||||||
|
|
||||||
b.to_async(&rt).iter(|| {
|
b.to_async(&rt).iter(|| {
|
||||||
let ctx = context.clone();
|
let ctx = context.clone();
|
||||||
i += 1;
|
|
||||||
async move {
|
async move {
|
||||||
recv_groupmembership_emails(black_box(ctx), i).await;
|
recv_groupmembership_emails(black_box(ctx)).await;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,11 +1,9 @@
|
|||||||
#![recursion_limit = "256"]
|
|
||||||
use std::hint::black_box;
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
use deltachat::Events;
|
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
|
use deltachat::Events;
|
||||||
|
|
||||||
async fn search_benchmark(dbfile: impl AsRef<Path>) {
|
async fn search_benchmark(dbfile: impl AsRef<Path>) {
|
||||||
let id = 100;
|
let id = 100;
|
||||||
|
|||||||
@@ -1,17 +1,16 @@
|
|||||||
#![recursion_limit = "256"]
|
use criterion::{criterion_group, criterion_main, Criterion};
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
|
||||||
|
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::stock_str::StockStrings;
|
use deltachat::stock_str::StockStrings;
|
||||||
use deltachat::{Event, EventType, Events};
|
use deltachat::{info, Event, EventType, Events};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
async fn send_events_benchmark(context: &Context) {
|
async fn send_events_benchmark(context: &Context) {
|
||||||
let emitter = context.get_event_emitter();
|
let emitter = context.get_event_emitter();
|
||||||
for _i in 0..1_000_000 {
|
for _i in 0..1_000_000 {
|
||||||
context.emit_event(EventType::Info("interesting event...".to_string()));
|
info!(context, "interesting event...");
|
||||||
}
|
}
|
||||||
context.emit_event(EventType::Info("DONE".to_string()));
|
info!(context, "DONE");
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
match emitter.recv().await.unwrap() {
|
match emitter.recv().await.unwrap() {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ filter_unconventional = false
|
|||||||
split_commits = false
|
split_commits = false
|
||||||
# regex for preprocessing the commit messages
|
# regex for preprocessing the commit messages
|
||||||
commit_preprocessors = [
|
commit_preprocessors = [
|
||||||
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/chatmail/core/pull/${2}))"}, # replace pull request / issue numbers
|
{ pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/deltachat/deltachat-core-rust/pull/${2}))"}, # replace pull request / issue numbers
|
||||||
]
|
]
|
||||||
# regex for parsing and grouping commits
|
# regex for parsing and grouping commits
|
||||||
commit_parsers = [
|
commit_parsers = [
|
||||||
@@ -82,11 +82,11 @@ footer = """
|
|||||||
{% if release.version -%}
|
{% if release.version -%}
|
||||||
{% if release.previous.version -%}
|
{% if release.previous.version -%}
|
||||||
[{{ release.version | trim_start_matches(pat="v") }}]: \
|
[{{ release.version | trim_start_matches(pat="v") }}]: \
|
||||||
https://github.com/chatmail/core\
|
https://github.com/deltachat/deltachat-core-rust\
|
||||||
/compare/{{ release.previous.version }}..{{ release.version }}
|
/compare/{{ release.previous.version }}..{{ release.version }}
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
{% else -%}
|
{% else -%}
|
||||||
[unreleased]: https://github.com/chatmail/core\
|
[unreleased]: https://github.com/deltachat/deltachat-core-rust\
|
||||||
/compare/{{ release.previous.version }}..HEAD
|
/compare/{{ release.previous.version }}..HEAD
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
80
contrib/proxy.py
Normal file
80
contrib/proxy.py
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
# Examples:
|
||||||
|
#
|
||||||
|
# Original server that doesn't use SSL:
|
||||||
|
# ./proxy.py 8080 imap.nauta.cu 143
|
||||||
|
# ./proxy.py 8081 smtp.nauta.cu 25
|
||||||
|
#
|
||||||
|
# Original server that uses SSL:
|
||||||
|
# ./proxy.py 8080 testrun.org 993 --ssl
|
||||||
|
# ./proxy.py 8081 testrun.org 465 --ssl
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
|
import argparse
|
||||||
|
import selectors
|
||||||
|
import ssl
|
||||||
|
import socket
|
||||||
|
import socketserver
|
||||||
|
|
||||||
|
|
||||||
|
class Proxy(socketserver.ThreadingTCPServer):
|
||||||
|
allow_reuse_address = True
|
||||||
|
|
||||||
|
def __init__(self, proxy_host, proxy_port, real_host, real_port, use_ssl):
|
||||||
|
self.real_host = real_host
|
||||||
|
self.real_port = real_port
|
||||||
|
self.use_ssl = use_ssl
|
||||||
|
super().__init__((proxy_host, proxy_port), RequestHandler)
|
||||||
|
|
||||||
|
|
||||||
|
class RequestHandler(socketserver.BaseRequestHandler):
|
||||||
|
|
||||||
|
def handle(self):
|
||||||
|
print('{} - {} CONNECTED.'.format(datetime.now(), self.client_address))
|
||||||
|
|
||||||
|
total = 0
|
||||||
|
real_server = (self.server.real_host, self.server.real_port)
|
||||||
|
with socket.create_connection(real_server) as sock:
|
||||||
|
if self.server.use_ssl:
|
||||||
|
context = ssl.create_default_context()
|
||||||
|
sock = context.wrap_socket(
|
||||||
|
sock, server_hostname=real_server[0])
|
||||||
|
|
||||||
|
forward = {self.request: sock, sock: self.request}
|
||||||
|
|
||||||
|
sel = selectors.DefaultSelector()
|
||||||
|
sel.register(self.request, selectors.EVENT_READ,
|
||||||
|
self.client_address)
|
||||||
|
sel.register(sock, selectors.EVENT_READ, real_server)
|
||||||
|
|
||||||
|
active = True
|
||||||
|
while active:
|
||||||
|
events = sel.select()
|
||||||
|
for key, mask in events:
|
||||||
|
print('\n{} - {} wrote:'.format(datetime.now(), key.data))
|
||||||
|
data = key.fileobj.recv(1024)
|
||||||
|
received = len(data)
|
||||||
|
total += received
|
||||||
|
print(data)
|
||||||
|
print('{} Bytes\nTotal: {} Bytes'.format(received, total))
|
||||||
|
if data:
|
||||||
|
forward[key.fileobj].sendall(data)
|
||||||
|
else:
|
||||||
|
print('\nCLOSING CONNECTION.\n\n')
|
||||||
|
forward[key.fileobj].close()
|
||||||
|
key.fileobj.close()
|
||||||
|
active = False
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
p = argparse.ArgumentParser(description='Simple Python Proxy')
|
||||||
|
p.add_argument(
|
||||||
|
"proxy_port", help="the port where the proxy will listen", type=int)
|
||||||
|
p.add_argument('host', help="the real host")
|
||||||
|
p.add_argument('port', help="the port of the real host", type=int)
|
||||||
|
p.add_argument("--ssl", help="use ssl to connect to the real host",
|
||||||
|
action="store_true")
|
||||||
|
args = p.parse_args()
|
||||||
|
|
||||||
|
with Proxy('', args.proxy_port, args.host, args.port, args.ssl) as proxy:
|
||||||
|
proxy.serve_forever()
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "deltachat-contact-tools"
|
|
||||||
version = "0.0.0" # No semver-stable versioning
|
|
||||||
edition = "2021"
|
|
||||||
description = "Contact-related tools, like parsing vcards and sanitizing name and address. Meant for internal use in the deltachat crate."
|
|
||||||
license = "MPL-2.0"
|
|
||||||
|
|
||||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
anyhow = { workspace = true }
|
|
||||||
regex = { workspace = true }
|
|
||||||
rusqlite = { workspace = true } # Needed in order to `impl rusqlite::types::ToSql for EmailAddress`. Could easily be put behind a feature.
|
|
||||||
chrono = { workspace = true, features = ["alloc", "clock", "std"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
anyhow = { workspace = true, features = ["backtrace"] } # Enable `backtrace` feature in tests.
|
|
||||||
@@ -1,401 +0,0 @@
|
|||||||
//! Contact-related tools, like parsing vcards and sanitizing name and address
|
|
||||||
|
|
||||||
#![forbid(unsafe_code)]
|
|
||||||
#![warn(
|
|
||||||
unused,
|
|
||||||
clippy::correctness,
|
|
||||||
missing_debug_implementations,
|
|
||||||
missing_docs,
|
|
||||||
clippy::all,
|
|
||||||
clippy::wildcard_imports,
|
|
||||||
clippy::needless_borrow,
|
|
||||||
clippy::cast_lossless,
|
|
||||||
clippy::unused_async,
|
|
||||||
clippy::explicit_iter_loop,
|
|
||||||
clippy::explicit_into_iter_loop,
|
|
||||||
clippy::cloned_instead_of_copied
|
|
||||||
)]
|
|
||||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
|
||||||
#![cfg_attr(not(test), forbid(clippy::string_slice))]
|
|
||||||
#![allow(
|
|
||||||
clippy::match_bool,
|
|
||||||
clippy::mixed_read_write_in_expression,
|
|
||||||
clippy::bool_assert_comparison,
|
|
||||||
clippy::manual_split_once,
|
|
||||||
clippy::format_push_string,
|
|
||||||
clippy::bool_to_int_with_if,
|
|
||||||
clippy::manual_range_contains
|
|
||||||
)]
|
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
use std::ops::Deref;
|
|
||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
use anyhow::bail;
|
|
||||||
use anyhow::Result;
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
mod vcard;
|
|
||||||
pub use vcard::{make_vcard, parse_vcard, VcardContact};
|
|
||||||
|
|
||||||
/// Valid contact address.
|
|
||||||
#[derive(Debug, Clone)]
|
|
||||||
pub struct ContactAddress(String);
|
|
||||||
|
|
||||||
impl Deref for ContactAddress {
|
|
||||||
type Target = str;
|
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AsRef<str> for ContactAddress {
|
|
||||||
fn as_ref(&self) -> &str {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for ContactAddress {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}", self.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ContactAddress {
|
|
||||||
/// Constructs a new contact address from string,
|
|
||||||
/// normalizing and validating it.
|
|
||||||
pub fn new(s: &str) -> Result<Self> {
|
|
||||||
let addr = addr_normalize(s);
|
|
||||||
if !may_be_valid_addr(&addr) {
|
|
||||||
bail!("invalid address {s:?}");
|
|
||||||
}
|
|
||||||
Ok(Self(addr.to_string()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Allow converting [`ContactAddress`] to an SQLite type.
|
|
||||||
impl rusqlite::types::ToSql for ContactAddress {
|
|
||||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
|
||||||
let val = rusqlite::types::Value::Text(self.0.to_string());
|
|
||||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Takes a name and an address and sanitizes them:
|
|
||||||
/// - Extracts a name from the addr if the addr is in form "Alice <alice@example.org>"
|
|
||||||
/// - Removes special characters from the name, see [`sanitize_name()`]
|
|
||||||
/// - Removes the name if it is equal to the address by setting it to ""
|
|
||||||
pub fn sanitize_name_and_addr(name: &str, addr: &str) -> (String, String) {
|
|
||||||
static ADDR_WITH_NAME_REGEX: LazyLock<Regex> =
|
|
||||||
LazyLock::new(|| Regex::new("(.*)<(.*)>").unwrap());
|
|
||||||
let (name, addr) = if let Some(captures) = ADDR_WITH_NAME_REGEX.captures(addr.as_ref()) {
|
|
||||||
(
|
|
||||||
if name.is_empty() {
|
|
||||||
captures.get(1).map_or("", |m| m.as_str())
|
|
||||||
} else {
|
|
||||||
name
|
|
||||||
},
|
|
||||||
captures
|
|
||||||
.get(2)
|
|
||||||
.map_or("".to_string(), |m| m.as_str().to_string()),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(name, addr.to_string())
|
|
||||||
};
|
|
||||||
let mut name = sanitize_name(name);
|
|
||||||
|
|
||||||
// If the 'display name' is just the address, remove it:
|
|
||||||
// Otherwise, the contact would sometimes be shown as "alice@example.com (alice@example.com)" (see `get_name_n_addr()`).
|
|
||||||
// If the display name is empty, DC will just show the address when it needs a display name.
|
|
||||||
if name == addr {
|
|
||||||
name = "".to_string();
|
|
||||||
}
|
|
||||||
|
|
||||||
(name, addr)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sanitizes a name.
|
|
||||||
///
|
|
||||||
/// - Removes newlines and trims the string
|
|
||||||
/// - Removes quotes (come from some bad MUA implementations)
|
|
||||||
/// - Removes potentially-malicious bidi characters
|
|
||||||
pub fn sanitize_name(name: &str) -> String {
|
|
||||||
let name = sanitize_single_line(name);
|
|
||||||
|
|
||||||
match name.as_bytes() {
|
|
||||||
[b'\'', .., b'\''] | [b'\"', .., b'\"'] | [b'<', .., b'>'] => name
|
|
||||||
.get(1..name.len() - 1)
|
|
||||||
.map_or("".to_string(), |s| s.trim().to_string()),
|
|
||||||
_ => name.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Sanitizes user input
|
|
||||||
///
|
|
||||||
/// - Removes newlines and trims the string
|
|
||||||
/// - Removes potentially-malicious bidi characters
|
|
||||||
pub fn sanitize_single_line(input: &str) -> String {
|
|
||||||
sanitize_bidi_characters(input.replace(['\n', '\r'], " ").trim())
|
|
||||||
}
|
|
||||||
|
|
||||||
const RTLO_CHARACTERS: [char; 5] = ['\u{202A}', '\u{202B}', '\u{202C}', '\u{202D}', '\u{202E}'];
|
|
||||||
const ISOLATE_CHARACTERS: [char; 3] = ['\u{2066}', '\u{2067}', '\u{2068}'];
|
|
||||||
const POP_ISOLATE_CHARACTER: char = '\u{2069}';
|
|
||||||
/// Some control unicode characters can influence whether adjacent text is shown from
|
|
||||||
/// left to right or from right to left.
|
|
||||||
///
|
|
||||||
/// Since user input is not supposed to influence how adjacent text looks,
|
|
||||||
/// this function removes some of these characters.
|
|
||||||
///
|
|
||||||
/// Also see https://github.com/deltachat/deltachat-core-rust/issues/3479.
|
|
||||||
pub fn sanitize_bidi_characters(input_str: &str) -> String {
|
|
||||||
// RTLO_CHARACTERS are apparently rarely used in practice.
|
|
||||||
// They can impact all following text, so, better remove them all:
|
|
||||||
let input_str = input_str.replace(|char| RTLO_CHARACTERS.contains(&char), "");
|
|
||||||
|
|
||||||
// If the ISOLATE characters are not ended with a POP DIRECTIONAL ISOLATE character,
|
|
||||||
// we regard the input as potentially malicious and simply remove all ISOLATE characters.
|
|
||||||
// See https://en.wikipedia.org/wiki/Bidirectional_text#Unicode_bidi_support
|
|
||||||
// and https://www.w3.org/International/questions/qa-bidi-unicode-controls.en
|
|
||||||
// for an explanation about ISOLATE characters.
|
|
||||||
fn isolate_characters_are_valid(input_str: &str) -> bool {
|
|
||||||
let mut isolate_character_nesting: i32 = 0;
|
|
||||||
for char in input_str.chars() {
|
|
||||||
if ISOLATE_CHARACTERS.contains(&char) {
|
|
||||||
isolate_character_nesting += 1;
|
|
||||||
} else if char == POP_ISOLATE_CHARACTER {
|
|
||||||
isolate_character_nesting -= 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
// According to Wikipedia, 125 levels are allowed:
|
|
||||||
// https://en.wikipedia.org/wiki/Unicode_control_characters
|
|
||||||
// (although, in practice, we could also significantly lower this number)
|
|
||||||
if isolate_character_nesting < 0 || isolate_character_nesting > 125 {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
isolate_character_nesting == 0
|
|
||||||
}
|
|
||||||
|
|
||||||
if isolate_characters_are_valid(&input_str) {
|
|
||||||
input_str
|
|
||||||
} else {
|
|
||||||
input_str.replace(
|
|
||||||
|char| ISOLATE_CHARACTERS.contains(&char) || POP_ISOLATE_CHARACTER == char,
|
|
||||||
"",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns false if addr is an invalid address, otherwise true.
|
|
||||||
pub fn may_be_valid_addr(addr: &str) -> bool {
|
|
||||||
let res = EmailAddress::new(addr);
|
|
||||||
res.is_ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns address lowercased,
|
|
||||||
/// with whitespace trimmed and `mailto:` prefix removed.
|
|
||||||
pub fn addr_normalize(addr: &str) -> String {
|
|
||||||
let norm = addr.trim().to_lowercase();
|
|
||||||
|
|
||||||
if norm.starts_with("mailto:") {
|
|
||||||
norm.get(7..).unwrap_or(&norm).to_string()
|
|
||||||
} else {
|
|
||||||
norm
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Compares two email addresses, normalizing them beforehand.
|
|
||||||
pub fn addr_cmp(addr1: &str, addr2: &str) -> bool {
|
|
||||||
let norm1 = addr_normalize(addr1);
|
|
||||||
let norm2 = addr_normalize(addr2);
|
|
||||||
|
|
||||||
norm1 == norm2
|
|
||||||
}
|
|
||||||
|
|
||||||
///
|
|
||||||
/// Represents an email address, right now just the `name@domain` portion.
|
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// ```
|
|
||||||
/// use deltachat_contact_tools::EmailAddress;
|
|
||||||
/// let email = match EmailAddress::new("someone@example.com") {
|
|
||||||
/// Ok(addr) => addr,
|
|
||||||
/// Err(e) => panic!("Error parsing address, error was {}", e),
|
|
||||||
/// };
|
|
||||||
/// assert_eq!(&email.local, "someone");
|
|
||||||
/// assert_eq!(&email.domain, "example.com");
|
|
||||||
/// assert_eq!(email.to_string(), "someone@example.com");
|
|
||||||
/// ```
|
|
||||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
|
||||||
pub struct EmailAddress {
|
|
||||||
/// Local part of the email address.
|
|
||||||
pub local: String,
|
|
||||||
|
|
||||||
/// Email address domain.
|
|
||||||
pub domain: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Display for EmailAddress {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
|
||||||
write!(f, "{}@{}", self.local, self.domain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl EmailAddress {
|
|
||||||
/// Performs a dead-simple parse of an email address.
|
|
||||||
pub fn new(input: &str) -> Result<EmailAddress> {
|
|
||||||
if input.is_empty() {
|
|
||||||
bail!("empty string is not valid");
|
|
||||||
}
|
|
||||||
let parts: Vec<&str> = input.rsplitn(2, '@').collect();
|
|
||||||
|
|
||||||
if input
|
|
||||||
.chars()
|
|
||||||
.any(|c| c.is_whitespace() || c == '<' || c == '>')
|
|
||||||
{
|
|
||||||
bail!("Email {input:?} must not contain whitespaces, '>' or '<'");
|
|
||||||
}
|
|
||||||
|
|
||||||
match &parts[..] {
|
|
||||||
[domain, local] => {
|
|
||||||
if local.is_empty() {
|
|
||||||
bail!("empty string is not valid for local part in {input:?}");
|
|
||||||
}
|
|
||||||
if domain.is_empty() {
|
|
||||||
bail!("missing domain after '@' in {input:?}");
|
|
||||||
}
|
|
||||||
if domain.ends_with('.') {
|
|
||||||
bail!("Domain {domain:?} should not contain the dot in the end");
|
|
||||||
}
|
|
||||||
Ok(EmailAddress {
|
|
||||||
local: (*local).to_string(),
|
|
||||||
domain: (*domain).to_string(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
_ => bail!("Email {input:?} must contain '@' character"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl rusqlite::types::ToSql for EmailAddress {
|
|
||||||
fn to_sql(&self) -> rusqlite::Result<rusqlite::types::ToSqlOutput<'_>> {
|
|
||||||
let val = rusqlite::types::Value::Text(self.to_string());
|
|
||||||
let out = rusqlite::types::ToSqlOutput::Owned(val);
|
|
||||||
Ok(out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod tests {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_contact_address() -> Result<()> {
|
|
||||||
let alice_addr = "alice@example.org";
|
|
||||||
let contact_address = ContactAddress::new(alice_addr)?;
|
|
||||||
assert_eq!(contact_address.as_ref(), alice_addr);
|
|
||||||
|
|
||||||
let invalid_addr = "<> foobar";
|
|
||||||
assert!(ContactAddress::new(invalid_addr).is_err());
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_emailaddress_parse() {
|
|
||||||
assert_eq!(EmailAddress::new("").is_ok(), false);
|
|
||||||
assert_eq!(
|
|
||||||
EmailAddress::new("user@domain.tld").unwrap(),
|
|
||||||
EmailAddress {
|
|
||||||
local: "user".into(),
|
|
||||||
domain: "domain.tld".into(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
EmailAddress::new("user@localhost").unwrap(),
|
|
||||||
EmailAddress {
|
|
||||||
local: "user".into(),
|
|
||||||
domain: "localhost".into()
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert_eq!(EmailAddress::new("uuu").is_ok(), false);
|
|
||||||
assert_eq!(EmailAddress::new("dd.tt").is_ok(), false);
|
|
||||||
assert!(EmailAddress::new("tt.dd@uu").is_ok());
|
|
||||||
assert!(EmailAddress::new("u@d").is_ok());
|
|
||||||
assert!(EmailAddress::new("u@d.").is_err());
|
|
||||||
assert!(EmailAddress::new("u@d.t").is_ok());
|
|
||||||
assert_eq!(
|
|
||||||
EmailAddress::new("u@d.tt").unwrap(),
|
|
||||||
EmailAddress {
|
|
||||||
local: "u".into(),
|
|
||||||
domain: "d.tt".into(),
|
|
||||||
}
|
|
||||||
);
|
|
||||||
assert!(EmailAddress::new("u@tt").is_ok());
|
|
||||||
assert_eq!(EmailAddress::new("@d.tt").is_ok(), false);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sanitize_name() {
|
|
||||||
assert_eq!(&sanitize_name(" hello world "), "hello world");
|
|
||||||
assert_eq!(&sanitize_name("<"), "<");
|
|
||||||
assert_eq!(&sanitize_name(">"), ">");
|
|
||||||
assert_eq!(&sanitize_name("'"), "'");
|
|
||||||
assert_eq!(&sanitize_name("\""), "\"");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sanitize_single_line() {
|
|
||||||
assert_eq!(sanitize_single_line("Hi\naiae "), "Hi aiae");
|
|
||||||
assert_eq!(sanitize_single_line("\r\nahte\n\r"), "ahte");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_sanitize_bidi_characters() {
|
|
||||||
// Legit inputs:
|
|
||||||
assert_eq!(
|
|
||||||
&sanitize_bidi_characters("Tes\u{2067}ting Delta Chat\u{2069}"),
|
|
||||||
"Tes\u{2067}ting Delta Chat\u{2069}"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
&sanitize_bidi_characters("Tes\u{2067}ting \u{2068} Delta Chat\u{2069}\u{2069}"),
|
|
||||||
"Tes\u{2067}ting \u{2068} Delta Chat\u{2069}\u{2069}"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
&sanitize_bidi_characters("Tes\u{2067}ting\u{2069} Delta Chat\u{2067}\u{2069}"),
|
|
||||||
"Tes\u{2067}ting\u{2069} Delta Chat\u{2067}\u{2069}"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Potentially-malicious inputs:
|
|
||||||
assert_eq!(
|
|
||||||
&sanitize_bidi_characters("Tes\u{202C}ting Delta Chat"),
|
|
||||||
"Testing Delta Chat"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
&sanitize_bidi_characters("Testing Delta Chat\u{2069}"),
|
|
||||||
"Testing Delta Chat"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
&sanitize_bidi_characters("Tes\u{2067}ting Delta Chat"),
|
|
||||||
"Testing Delta Chat"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
&sanitize_bidi_characters("Tes\u{2069}ting Delta Chat\u{2067}"),
|
|
||||||
"Testing Delta Chat"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(
|
|
||||||
&sanitize_bidi_characters("Tes\u{2068}ting Delta Chat"),
|
|
||||||
"Testing Delta Chat"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,247 +0,0 @@
|
|||||||
use std::sync::LazyLock;
|
|
||||||
|
|
||||||
use anyhow::Context as _;
|
|
||||||
use anyhow::Result;
|
|
||||||
use chrono::DateTime;
|
|
||||||
use chrono::NaiveDateTime;
|
|
||||||
use regex::Regex;
|
|
||||||
|
|
||||||
use crate::sanitize_name_and_addr;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
/// A Contact, as represented in a VCard.
|
|
||||||
pub struct VcardContact {
|
|
||||||
/// The email address, vcard property `email`
|
|
||||||
pub addr: String,
|
|
||||||
/// This must be the name authorized by the contact itself, not a locally given name. Vcard
|
|
||||||
/// property `fn`. Can be empty, one should use `display_name()` to obtain the display name.
|
|
||||||
pub authname: String,
|
|
||||||
/// The contact's public PGP key in Base64, vcard property `key`
|
|
||||||
pub key: Option<String>,
|
|
||||||
/// The contact's profile image (=avatar) in Base64, vcard property `photo`
|
|
||||||
pub profile_image: Option<String>,
|
|
||||||
/// The biography, stored in the vcard property `note`
|
|
||||||
pub biography: Option<String>,
|
|
||||||
/// The timestamp when the vcard was created / last updated, vcard property `rev`
|
|
||||||
pub timestamp: Result<i64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VcardContact {
|
|
||||||
/// Returns the contact's display name.
|
|
||||||
pub fn display_name(&self) -> &str {
|
|
||||||
match self.authname.is_empty() {
|
|
||||||
false => &self.authname,
|
|
||||||
true => &self.addr,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns a vCard containing given contacts.
|
|
||||||
///
|
|
||||||
/// Calling [`parse_vcard()`] on the returned result is a reverse operation.
|
|
||||||
pub fn make_vcard(contacts: &[VcardContact]) -> String {
|
|
||||||
fn format_timestamp(c: &VcardContact) -> Option<String> {
|
|
||||||
let timestamp = *c.timestamp.as_ref().ok()?;
|
|
||||||
let datetime = DateTime::from_timestamp(timestamp, 0)?;
|
|
||||||
Some(datetime.format("%Y%m%dT%H%M%SZ").to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn escape(s: &str) -> String {
|
|
||||||
s.replace(',', "\\,")
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut res = "".to_string();
|
|
||||||
for c in contacts {
|
|
||||||
// Mustn't contain ',', but it's easier to escape than to error out.
|
|
||||||
let addr = escape(&c.addr);
|
|
||||||
let display_name = escape(c.display_name());
|
|
||||||
res += &format!(
|
|
||||||
"BEGIN:VCARD\r\n\
|
|
||||||
VERSION:4.0\r\n\
|
|
||||||
EMAIL:{addr}\r\n\
|
|
||||||
FN:{display_name}\r\n"
|
|
||||||
);
|
|
||||||
if let Some(key) = &c.key {
|
|
||||||
res += &format!("KEY:data:application/pgp-keys;base64\\,{key}\r\n");
|
|
||||||
}
|
|
||||||
if let Some(profile_image) = &c.profile_image {
|
|
||||||
res += &format!("PHOTO:data:image/jpeg;base64\\,{profile_image}\r\n");
|
|
||||||
}
|
|
||||||
if let Some(biography) = &c.biography {
|
|
||||||
res += &format!("NOTE:{}\r\n", escape(biography));
|
|
||||||
}
|
|
||||||
if let Some(timestamp) = format_timestamp(c) {
|
|
||||||
res += &format!("REV:{timestamp}\r\n");
|
|
||||||
}
|
|
||||||
res += "END:VCARD\r\n";
|
|
||||||
}
|
|
||||||
res
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Parses `VcardContact`s from a given `&str`.
|
|
||||||
pub fn parse_vcard(vcard: &str) -> Vec<VcardContact> {
|
|
||||||
fn remove_prefix<'a>(s: &'a str, prefix: &str) -> Option<&'a str> {
|
|
||||||
let start_of_s = s.get(..prefix.len())?;
|
|
||||||
|
|
||||||
if start_of_s.eq_ignore_ascii_case(prefix) {
|
|
||||||
s.get(prefix.len()..)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/// Returns (parameters, raw value) tuple.
|
|
||||||
fn vcard_property_raw<'a>(line: &'a str, property: &str) -> Option<(&'a str, &'a str)> {
|
|
||||||
let remainder = remove_prefix(line, property)?;
|
|
||||||
// If `s` is `EMAIL;TYPE=work:alice@example.com` and `property` is `EMAIL`,
|
|
||||||
// then `remainder` is now `;TYPE=work:alice@example.com`
|
|
||||||
|
|
||||||
// Note: This doesn't handle the case where there are quotes around a colon,
|
|
||||||
// like `NAME;Foo="Some quoted text: that contains a colon":value`.
|
|
||||||
// This could be improved in the future, but for now, the parsing is good enough.
|
|
||||||
let (mut params, value) = remainder.split_once(':')?;
|
|
||||||
// In the example from above, `params` is now `;TYPE=work`
|
|
||||||
// and `value` is now `alice@example.com`
|
|
||||||
|
|
||||||
if params
|
|
||||||
.chars()
|
|
||||||
.next()
|
|
||||||
.filter(|c| !c.is_ascii_punctuation() || *c == '_')
|
|
||||||
.is_some()
|
|
||||||
{
|
|
||||||
// `s` started with `property`, but the next character after it was not punctuation,
|
|
||||||
// so this line's property is actually something else
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
if let Some(p) = remove_prefix(params, ";") {
|
|
||||||
params = p;
|
|
||||||
}
|
|
||||||
if let Some(p) = remove_prefix(params, "PREF=1") {
|
|
||||||
params = p;
|
|
||||||
}
|
|
||||||
Some((params, value))
|
|
||||||
}
|
|
||||||
/// Returns (parameters, unescaped value) tuple.
|
|
||||||
fn vcard_property<'a>(line: &'a str, property: &str) -> Option<(&'a str, String)> {
|
|
||||||
let (params, value) = vcard_property_raw(line, property)?;
|
|
||||||
// Some fields can't contain commas, but unescape them everywhere for safety.
|
|
||||||
Some((params, value.replace("\\,", ",")))
|
|
||||||
}
|
|
||||||
fn base64_key(line: &str) -> Option<&str> {
|
|
||||||
let (params, value) = vcard_property_raw(line, "key")?;
|
|
||||||
if params.eq_ignore_ascii_case("PGP;ENCODING=BASE64")
|
|
||||||
|| params.eq_ignore_ascii_case("TYPE=PGP;ENCODING=b")
|
|
||||||
{
|
|
||||||
return Some(value);
|
|
||||||
}
|
|
||||||
remove_prefix(value, "data:application/pgp-keys;base64\\,")
|
|
||||||
// Old Delta Chat format.
|
|
||||||
.or_else(|| remove_prefix(value, "data:application/pgp-keys;base64,"))
|
|
||||||
}
|
|
||||||
fn base64_photo(line: &str) -> Option<&str> {
|
|
||||||
let (params, value) = vcard_property_raw(line, "photo")?;
|
|
||||||
if params.eq_ignore_ascii_case("JPEG;ENCODING=BASE64")
|
|
||||||
|| params.eq_ignore_ascii_case("ENCODING=BASE64;JPEG")
|
|
||||||
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=b")
|
|
||||||
|| params.eq_ignore_ascii_case("ENCODING=b;TYPE=JPEG")
|
|
||||||
|| params.eq_ignore_ascii_case("ENCODING=BASE64;TYPE=JPEG")
|
|
||||||
|| params.eq_ignore_ascii_case("TYPE=JPEG;ENCODING=BASE64")
|
|
||||||
{
|
|
||||||
return Some(value);
|
|
||||||
}
|
|
||||||
remove_prefix(value, "data:image/jpeg;base64\\,")
|
|
||||||
// Old Delta Chat format.
|
|
||||||
.or_else(|| remove_prefix(value, "data:image/jpeg;base64,"))
|
|
||||||
}
|
|
||||||
fn parse_datetime(datetime: &str) -> Result<i64> {
|
|
||||||
// According to https://www.rfc-editor.org/rfc/rfc6350#section-4.3.5, the timestamp
|
|
||||||
// is in ISO.8601.2004 format. DateTime::parse_from_rfc3339() apparently parses
|
|
||||||
// ISO.8601, but fails to parse any of the examples given.
|
|
||||||
// So, instead just parse using a format string.
|
|
||||||
|
|
||||||
// Parses 19961022T140000Z, 19961022T140000-05, or 19961022T140000-0500.
|
|
||||||
let timestamp = match DateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S%#z") {
|
|
||||||
Ok(datetime) => datetime.timestamp(),
|
|
||||||
// Parses 19961022T140000.
|
|
||||||
Err(e) => match NaiveDateTime::parse_from_str(datetime, "%Y%m%dT%H%M%S") {
|
|
||||||
Ok(datetime) => datetime
|
|
||||||
.and_local_timezone(chrono::offset::Local)
|
|
||||||
.single()
|
|
||||||
.context("Could not apply local timezone to parsed date and time")?
|
|
||||||
.timestamp(),
|
|
||||||
Err(_) => return Err(e.into()),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
Ok(timestamp)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove line folding, see https://datatracker.ietf.org/doc/html/rfc6350#section-3.2
|
|
||||||
static NEWLINE_AND_SPACE_OR_TAB: LazyLock<Regex> =
|
|
||||||
LazyLock::new(|| Regex::new("\r?\n[\t ]").unwrap());
|
|
||||||
let unfolded_lines = NEWLINE_AND_SPACE_OR_TAB.replace_all(vcard, "");
|
|
||||||
|
|
||||||
let mut lines = unfolded_lines.lines().peekable();
|
|
||||||
let mut contacts = Vec::new();
|
|
||||||
|
|
||||||
while lines.peek().is_some() {
|
|
||||||
// Skip to the start of the vcard:
|
|
||||||
for line in lines.by_ref() {
|
|
||||||
if line.eq_ignore_ascii_case("BEGIN:VCARD") {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut display_name = None;
|
|
||||||
let mut addr = None;
|
|
||||||
let mut key = None;
|
|
||||||
let mut photo = None;
|
|
||||||
let mut biography = None;
|
|
||||||
let mut datetime = None;
|
|
||||||
|
|
||||||
for mut line in lines.by_ref() {
|
|
||||||
if let Some(remainder) = remove_prefix(line, "item1.") {
|
|
||||||
// Remove the group name, if the group is called "item1".
|
|
||||||
// If necessary, we can improve this to also remove groups that are called something different that "item1".
|
|
||||||
//
|
|
||||||
// Search "group name" at https://datatracker.ietf.org/doc/html/rfc6350 for more infos.
|
|
||||||
line = remainder;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some((_params, email)) = vcard_property(line, "email") {
|
|
||||||
addr.get_or_insert(email);
|
|
||||||
} else if let Some((_params, name)) = vcard_property(line, "fn") {
|
|
||||||
display_name.get_or_insert(name);
|
|
||||||
} else if let Some(k) = base64_key(line) {
|
|
||||||
key.get_or_insert(k);
|
|
||||||
} else if let Some(p) = base64_photo(line) {
|
|
||||||
photo.get_or_insert(p);
|
|
||||||
} else if let Some((_params, bio)) = vcard_property(line, "note") {
|
|
||||||
biography.get_or_insert(bio);
|
|
||||||
} else if let Some((_params, rev)) = vcard_property(line, "rev") {
|
|
||||||
datetime.get_or_insert(rev);
|
|
||||||
} else if line.eq_ignore_ascii_case("END:VCARD") {
|
|
||||||
let (authname, addr) = sanitize_name_and_addr(
|
|
||||||
&display_name.unwrap_or_default(),
|
|
||||||
&addr.unwrap_or_default(),
|
|
||||||
);
|
|
||||||
|
|
||||||
contacts.push(VcardContact {
|
|
||||||
authname,
|
|
||||||
addr,
|
|
||||||
key: key.map(|s| s.to_string()),
|
|
||||||
profile_image: photo.map(|s| s.to_string()),
|
|
||||||
biography,
|
|
||||||
timestamp: datetime
|
|
||||||
.as_deref()
|
|
||||||
.context("No timestamp in vcard")
|
|
||||||
.and_then(parse_datetime),
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
contacts
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod vcard_tests;
|
|
||||||
@@ -1,278 +0,0 @@
|
|||||||
use chrono::TimeZone as _;
|
|
||||||
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_vcard_thunderbird() {
|
|
||||||
let contacts = parse_vcard(
|
|
||||||
"BEGIN:VCARD
|
|
||||||
VERSION:4.0
|
|
||||||
FN:'Alice Mueller'
|
|
||||||
EMAIL;PREF=1:alice.mueller@posteo.de
|
|
||||||
UID:a8083264-ca47-4be7-98a8-8ec3db1447ca
|
|
||||||
END:VCARD
|
|
||||||
BEGIN:VCARD
|
|
||||||
VERSION:4.0
|
|
||||||
FN:'bobzzz@freenet.de'
|
|
||||||
EMAIL;PREF=1:bobzzz@freenet.de
|
|
||||||
UID:cac4fef4-6351-4854-bbe4-9b6df857eaed
|
|
||||||
END:VCARD
|
|
||||||
",
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(contacts[0].addr, "alice.mueller@posteo.de".to_string());
|
|
||||||
assert_eq!(contacts[0].authname, "Alice Mueller".to_string());
|
|
||||||
assert_eq!(contacts[0].key, None);
|
|
||||||
assert_eq!(contacts[0].profile_image, None);
|
|
||||||
assert!(contacts[0].timestamp.is_err());
|
|
||||||
|
|
||||||
assert_eq!(contacts[1].addr, "bobzzz@freenet.de".to_string());
|
|
||||||
assert_eq!(contacts[1].authname, "".to_string());
|
|
||||||
assert_eq!(contacts[1].key, None);
|
|
||||||
assert_eq!(contacts[1].profile_image, None);
|
|
||||||
assert!(contacts[1].timestamp.is_err());
|
|
||||||
|
|
||||||
assert_eq!(contacts.len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_vcard_simple_example() {
|
|
||||||
let contacts = parse_vcard(
|
|
||||||
"BEGIN:VCARD
|
|
||||||
VERSION:4.0
|
|
||||||
FN:Alice Wonderland
|
|
||||||
N:Wonderland;Alice;;;Ms.
|
|
||||||
GENDER:W
|
|
||||||
EMAIL;TYPE=work:alice@example.com
|
|
||||||
KEY;TYPE=PGP;ENCODING=b:[base64-data]
|
|
||||||
REV:20240418T184242Z
|
|
||||||
|
|
||||||
END:VCARD",
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
|
|
||||||
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
|
|
||||||
assert_eq!(contacts[0].key, Some("[base64-data]".to_string()));
|
|
||||||
assert_eq!(contacts[0].profile_image, None);
|
|
||||||
assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762);
|
|
||||||
|
|
||||||
assert_eq!(contacts.len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_vcard_with_trailing_newline() {
|
|
||||||
let contacts = parse_vcard(
|
|
||||||
"BEGIN:VCARD\r
|
|
||||||
VERSION:4.0\r
|
|
||||||
FN:Alice Wonderland\r
|
|
||||||
N:Wonderland;Alice;;;Ms.\r
|
|
||||||
GENDER:W\r
|
|
||||||
EMAIL;TYPE=work:alice@example.com\r
|
|
||||||
KEY;TYPE=PGP;ENCODING=b:[base64-data]\r
|
|
||||||
REV:20240418T184242Z\r
|
|
||||||
END:VCARD\r
|
|
||||||
\r",
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(contacts[0].addr, "alice@example.com".to_string());
|
|
||||||
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
|
|
||||||
assert_eq!(contacts[0].key, Some("[base64-data]".to_string()));
|
|
||||||
assert_eq!(contacts[0].profile_image, None);
|
|
||||||
assert_eq!(*contacts[0].timestamp.as_ref().unwrap(), 1713465762);
|
|
||||||
|
|
||||||
assert_eq!(contacts.len(), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_make_and_parse_vcard() {
|
|
||||||
let contacts = [
|
|
||||||
VcardContact {
|
|
||||||
addr: "alice@example.org".to_string(),
|
|
||||||
authname: "Alice Wonderland".to_string(),
|
|
||||||
key: Some("[base64-data]".to_string()),
|
|
||||||
profile_image: Some("image in Base64".to_string()),
|
|
||||||
biography: Some("Hi, I'm Alice".to_string()),
|
|
||||||
timestamp: Ok(1713465762),
|
|
||||||
},
|
|
||||||
VcardContact {
|
|
||||||
addr: "bob@example.com".to_string(),
|
|
||||||
authname: "".to_string(),
|
|
||||||
key: None,
|
|
||||||
profile_image: None,
|
|
||||||
biography: None,
|
|
||||||
timestamp: Ok(0),
|
|
||||||
},
|
|
||||||
];
|
|
||||||
let items = [
|
|
||||||
"BEGIN:VCARD\r\n\
|
|
||||||
VERSION:4.0\r\n\
|
|
||||||
EMAIL:alice@example.org\r\n\
|
|
||||||
FN:Alice Wonderland\r\n\
|
|
||||||
KEY:data:application/pgp-keys;base64\\,[base64-data]\r\n\
|
|
||||||
PHOTO:data:image/jpeg;base64\\,image in Base64\r\n\
|
|
||||||
NOTE:Hi\\, I'm Alice\r\n\
|
|
||||||
REV:20240418T184242Z\r\n\
|
|
||||||
END:VCARD\r\n",
|
|
||||||
"BEGIN:VCARD\r\n\
|
|
||||||
VERSION:4.0\r\n\
|
|
||||||
EMAIL:bob@example.com\r\n\
|
|
||||||
FN:bob@example.com\r\n\
|
|
||||||
REV:19700101T000000Z\r\n\
|
|
||||||
END:VCARD\r\n",
|
|
||||||
];
|
|
||||||
let mut expected = "".to_string();
|
|
||||||
for len in 0..=contacts.len() {
|
|
||||||
let contacts = &contacts[0..len];
|
|
||||||
let vcard = make_vcard(contacts);
|
|
||||||
if len > 0 {
|
|
||||||
expected += items[len - 1];
|
|
||||||
}
|
|
||||||
assert_eq!(vcard, expected);
|
|
||||||
let parsed = parse_vcard(&vcard);
|
|
||||||
assert_eq!(parsed.len(), contacts.len());
|
|
||||||
for i in 0..parsed.len() {
|
|
||||||
assert_eq!(parsed[i].addr, contacts[i].addr);
|
|
||||||
assert_eq!(parsed[i].authname, contacts[i].authname);
|
|
||||||
assert_eq!(parsed[i].key, contacts[i].key);
|
|
||||||
assert_eq!(parsed[i].profile_image, contacts[i].profile_image);
|
|
||||||
assert_eq!(
|
|
||||||
parsed[i].timestamp.as_ref().unwrap(),
|
|
||||||
contacts[i].timestamp.as_ref().unwrap()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_vcard_android() {
|
|
||||||
let contacts = parse_vcard(
|
|
||||||
"BEGIN:VCARD
|
|
||||||
VERSION:2.1
|
|
||||||
N:;Bob;;;
|
|
||||||
FN:Bob
|
|
||||||
TEL;CELL:+1-234-567-890
|
|
||||||
EMAIL;HOME:bob@example.org
|
|
||||||
END:VCARD
|
|
||||||
BEGIN:VCARD
|
|
||||||
VERSION:2.1
|
|
||||||
N:;Alice;;;
|
|
||||||
FN:Alice
|
|
||||||
EMAIL;HOME:alice@example.org
|
|
||||||
END:VCARD
|
|
||||||
",
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
|
||||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
|
||||||
assert_eq!(contacts[0].key, None);
|
|
||||||
assert_eq!(contacts[0].profile_image, None);
|
|
||||||
|
|
||||||
assert_eq!(contacts[1].addr, "alice@example.org".to_string());
|
|
||||||
assert_eq!(contacts[1].authname, "Alice".to_string());
|
|
||||||
assert_eq!(contacts[1].key, None);
|
|
||||||
assert_eq!(contacts[1].profile_image, None);
|
|
||||||
|
|
||||||
assert_eq!(contacts.len(), 2);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_vcard_local_datetime() {
|
|
||||||
let contacts = parse_vcard(
|
|
||||||
"BEGIN:VCARD\n\
|
|
||||||
VERSION:4.0\n\
|
|
||||||
FN:Alice Wonderland\n\
|
|
||||||
EMAIL;TYPE=work:alice@example.org\n\
|
|
||||||
REV:20240418T184242\n\
|
|
||||||
END:VCARD",
|
|
||||||
);
|
|
||||||
assert_eq!(contacts.len(), 1);
|
|
||||||
assert_eq!(contacts[0].addr, "alice@example.org".to_string());
|
|
||||||
assert_eq!(contacts[0].authname, "Alice Wonderland".to_string());
|
|
||||||
assert_eq!(
|
|
||||||
*contacts[0].timestamp.as_ref().unwrap(),
|
|
||||||
chrono::offset::Local
|
|
||||||
.with_ymd_and_hms(2024, 4, 18, 18, 42, 42)
|
|
||||||
.unwrap()
|
|
||||||
.timestamp()
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_vcard_with_base64_avatar() {
|
|
||||||
// This is not an actual base64-encoded avatar, it's just to test the parsing.
|
|
||||||
// This one is Android-like.
|
|
||||||
let vcard0 = "BEGIN:VCARD
|
|
||||||
VERSION:2.1
|
|
||||||
N:;Bob;;;
|
|
||||||
FN:Bob
|
|
||||||
EMAIL;HOME:bob@example.org
|
|
||||||
PHOTO;ENCODING=BASE64;JPEG:/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEU
|
|
||||||
AAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAA
|
|
||||||
L8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==
|
|
||||||
|
|
||||||
END:VCARD
|
|
||||||
";
|
|
||||||
// This one is DOS-like.
|
|
||||||
let vcard1 = vcard0.replace('\n', "\r\n");
|
|
||||||
for vcard in [vcard0, vcard1.as_str()] {
|
|
||||||
let contacts = parse_vcard(vcard);
|
|
||||||
assert_eq!(contacts.len(), 1);
|
|
||||||
assert_eq!(contacts[0].addr, "bob@example.org".to_string());
|
|
||||||
assert_eq!(contacts[0].authname, "Bob".to_string());
|
|
||||||
assert_eq!(contacts[0].key, None);
|
|
||||||
assert_eq!(contacts[0].profile_image.as_deref().unwrap(), "/9j/4AAQSkZJRgABAQAAAQABAAD/4gIoSUNDX1BST0ZJTEUAAQEAAAIYAAAAAAQwAABtbnRyUkdCIFhZWiAAAAAAAAAAAAAAAABhY3NwAAAAAAAAAAAAAAAAL8bRuAJYoZUYrI4ZY3VWwxw4Ay28AAGBISScmf/2Q==");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_protonmail_vcard() {
|
|
||||||
let contacts = parse_vcard(
|
|
||||||
"BEGIN:VCARD
|
|
||||||
VERSION:4.0
|
|
||||||
FN;PREF=1:Alice Wonderland
|
|
||||||
UID:proton-web-03747582-328d-38dc-5ddd-000000000000
|
|
||||||
ITEM1.EMAIL;PREF=1:alice@example.org
|
|
||||||
ITEM1.KEY;PREF=1:data:application/pgp-keys;base64,aaaaaaaaaaaaaaaaaaaaaaaaa
|
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
||||||
ITEM1.KEY;PREF=2:data:application/pgp-keys;base64,bbbbbbbbbbbbbbbbbbbbbbbbb
|
|
||||||
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb
|
|
||||||
ITEM1.X-PM-ENCRYPT:true
|
|
||||||
ITEM1.X-PM-SIGN:true
|
|
||||||
END:VCARD",
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(contacts.len(), 1);
|
|
||||||
assert_eq!(&contacts[0].addr, "alice@example.org");
|
|
||||||
assert_eq!(&contacts[0].authname, "Alice Wonderland");
|
|
||||||
assert_eq!(contacts[0].key.as_ref().unwrap(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
|
|
||||||
assert!(contacts[0].timestamp.is_err());
|
|
||||||
assert_eq!(contacts[0].profile_image, None);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Proton at some point slightly changed the format of their vcards.
|
|
||||||
/// This also tests unescaped commas in PHOTO and KEY (old Delta Chat format).
|
|
||||||
#[test]
|
|
||||||
fn test_protonmail_vcard2() {
|
|
||||||
let contacts = parse_vcard(
|
|
||||||
r"BEGIN:VCARD
|
|
||||||
VERSION:4.0
|
|
||||||
FN;PREF=1:Alice
|
|
||||||
PHOTO;PREF=1:data:image/jpeg;base64,/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z
|
|
||||||
REV:Invalid Date
|
|
||||||
ITEM1.EMAIL;PREF=1:alice@example.org
|
|
||||||
KEY;PREF=1:data:application/pgp-keys;base64,xsaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
|
|
||||||
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==
|
|
||||||
UID:proton-web-aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa
|
|
||||||
END:VCARD",
|
|
||||||
);
|
|
||||||
|
|
||||||
assert_eq!(contacts.len(), 1);
|
|
||||||
assert_eq!(&contacts[0].addr, "alice@example.org");
|
|
||||||
assert_eq!(&contacts[0].authname, "Alice");
|
|
||||||
assert_eq!(contacts[0].key.as_ref().unwrap(), "xsaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa==");
|
|
||||||
assert!(contacts[0].timestamp.is_err());
|
|
||||||
assert_eq!(contacts[0].profile_image.as_ref().unwrap(), "/9aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Z");
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat_ffi"
|
name = "deltachat_ffi"
|
||||||
version = "2.31.0"
|
version = "1.137.2"
|
||||||
description = "Deltachat FFI"
|
description = "Deltachat FFI"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -14,19 +14,21 @@ name = "deltachat"
|
|||||||
crate-type = ["cdylib", "staticlib"]
|
crate-type = ["cdylib", "staticlib"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
deltachat = { workspace = true, default-features = false }
|
deltachat = { path = "../", default-features = false }
|
||||||
deltachat-jsonrpc = { workspace = true }
|
deltachat-jsonrpc = { path = "../deltachat-jsonrpc", optional = true }
|
||||||
libc = { workspace = true }
|
libc = "0.2"
|
||||||
human-panic = { version = "2", default-features = false }
|
human-panic = { version = "1", default-features = false }
|
||||||
num-traits = { workspace = true }
|
num-traits = "0.2"
|
||||||
serde_json = { workspace = true }
|
serde_json = "1.0"
|
||||||
tokio = { workspace = true, features = ["rt-multi-thread"] }
|
tokio = { version = "1", features = ["rt-multi-thread"] }
|
||||||
anyhow = { workspace = true }
|
anyhow = "1"
|
||||||
thiserror = { workspace = true }
|
thiserror = "1"
|
||||||
rand = { workspace = true }
|
rand = "0.8"
|
||||||
yerpc = { workspace = true, features = ["anyhow_expose"] }
|
once_cell = "1.18.0"
|
||||||
|
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
vendored = ["deltachat/vendored", "deltachat-jsonrpc/vendored"]
|
vendored = ["deltachat/vendored"]
|
||||||
|
jsonrpc = ["dep:deltachat-jsonrpc"]
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<doxygenlayout version="1.0">
|
||||||
<doxygenlayout version="2.0">
|
<!-- Generated by doxygen 1.8.20 -->
|
||||||
<!-- Generated by doxygen 1.13.2 -->
|
|
||||||
<!-- Navigation index tabs for HTML output -->
|
<!-- Navigation index tabs for HTML output -->
|
||||||
<navindex>
|
<navindex>
|
||||||
<tab type="mainpage" visible="yes" title=""/>
|
<tab type="mainpage" visible="yes" title=""/>
|
||||||
@@ -12,16 +11,10 @@
|
|||||||
</tab>
|
</tab>
|
||||||
<tab type="topics" visible="yes" title="Constants" intro="Here is a list of constants:"/>
|
<tab type="topics" visible="yes" title="Constants" intro="Here is a list of constants:"/>
|
||||||
<tab type="pages" visible="yes" title="" intro=""/>
|
<tab type="pages" visible="yes" title="" intro=""/>
|
||||||
<tab type="modules" visible="yes" title="" intro="">
|
|
||||||
<tab type="modulelist" visible="yes" title="" intro=""/>
|
|
||||||
<tab type="modulemembers" visible="yes" title="" intro=""/>
|
|
||||||
</tab>
|
|
||||||
<tab type="namespaces" visible="yes" title="">
|
<tab type="namespaces" visible="yes" title="">
|
||||||
<tab type="namespacelist" visible="yes" title="" intro=""/>
|
<tab type="namespacelist" visible="yes" title="" intro=""/>
|
||||||
<tab type="namespacemembers" visible="yes" title="" intro=""/>
|
<tab type="namespacemembers" visible="yes" title="" intro=""/>
|
||||||
</tab>
|
</tab>
|
||||||
<tab type="concepts" visible="yes" title="">
|
|
||||||
</tab>
|
|
||||||
<tab type="interfaces" visible="yes" title="">
|
<tab type="interfaces" visible="yes" title="">
|
||||||
<tab type="interfacelist" visible="yes" title="" intro=""/>
|
<tab type="interfacelist" visible="yes" title="" intro=""/>
|
||||||
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
||||||
@@ -42,228 +35,4 @@
|
|||||||
</tab>
|
</tab>
|
||||||
<tab type="examples" visible="yes" title="" intro=""/>
|
<tab type="examples" visible="yes" title="" intro=""/>
|
||||||
</navindex>
|
</navindex>
|
||||||
|
|
||||||
<!-- Layout definition for a class page -->
|
|
||||||
<class>
|
|
||||||
<briefdescription visible="yes"/>
|
|
||||||
<includes visible="$SHOW_HEADERFILE"/>
|
|
||||||
<inheritancegraph visible="yes"/>
|
|
||||||
<collaborationgraph visible="yes"/>
|
|
||||||
<memberdecl>
|
|
||||||
<nestedclasses visible="yes" title=""/>
|
|
||||||
<publictypes visible="yes" title=""/>
|
|
||||||
<services visible="yes" title=""/>
|
|
||||||
<interfaces visible="yes" title=""/>
|
|
||||||
<publicslots visible="yes" title=""/>
|
|
||||||
<signals visible="yes" title=""/>
|
|
||||||
<publicmethods visible="yes" title=""/>
|
|
||||||
<publicstaticmethods visible="yes" title=""/>
|
|
||||||
<publicattributes visible="yes" title=""/>
|
|
||||||
<publicstaticattributes visible="yes" title=""/>
|
|
||||||
<protectedtypes visible="yes" title=""/>
|
|
||||||
<protectedslots visible="yes" title=""/>
|
|
||||||
<protectedmethods visible="yes" title=""/>
|
|
||||||
<protectedstaticmethods visible="yes" title=""/>
|
|
||||||
<protectedattributes visible="yes" title=""/>
|
|
||||||
<protectedstaticattributes visible="yes" title=""/>
|
|
||||||
<packagetypes visible="yes" title=""/>
|
|
||||||
<packagemethods visible="yes" title=""/>
|
|
||||||
<packagestaticmethods visible="yes" title=""/>
|
|
||||||
<packageattributes visible="yes" title=""/>
|
|
||||||
<packagestaticattributes visible="yes" title=""/>
|
|
||||||
<properties visible="yes" title=""/>
|
|
||||||
<events visible="yes" title=""/>
|
|
||||||
<privatetypes visible="yes" title=""/>
|
|
||||||
<privateslots visible="yes" title=""/>
|
|
||||||
<privatemethods visible="yes" title=""/>
|
|
||||||
<privatestaticmethods visible="yes" title=""/>
|
|
||||||
<privateattributes visible="yes" title=""/>
|
|
||||||
<privatestaticattributes visible="yes" title=""/>
|
|
||||||
<friends visible="yes" title=""/>
|
|
||||||
<related visible="yes" title="" subtitle=""/>
|
|
||||||
<membergroups visible="yes"/>
|
|
||||||
</memberdecl>
|
|
||||||
<detaileddescription visible="yes" title=""/>
|
|
||||||
<memberdef>
|
|
||||||
<inlineclasses visible="yes" title=""/>
|
|
||||||
<typedefs visible="yes" title=""/>
|
|
||||||
<enums visible="yes" title=""/>
|
|
||||||
<services visible="yes" title=""/>
|
|
||||||
<interfaces visible="yes" title=""/>
|
|
||||||
<constructors visible="yes" title=""/>
|
|
||||||
<functions visible="yes" title=""/>
|
|
||||||
<related visible="yes" title=""/>
|
|
||||||
<variables visible="yes" title=""/>
|
|
||||||
<properties visible="yes" title=""/>
|
|
||||||
<events visible="yes" title=""/>
|
|
||||||
</memberdef>
|
|
||||||
<allmemberslink visible="yes"/>
|
|
||||||
<usedfiles visible="$SHOW_USED_FILES"/>
|
|
||||||
<authorsection visible="yes"/>
|
|
||||||
</class>
|
|
||||||
|
|
||||||
<!-- Layout definition for a namespace page -->
|
|
||||||
<namespace>
|
|
||||||
<briefdescription visible="yes"/>
|
|
||||||
<memberdecl>
|
|
||||||
<nestednamespaces visible="yes" title=""/>
|
|
||||||
<constantgroups visible="yes" title=""/>
|
|
||||||
<interfaces visible="yes" title=""/>
|
|
||||||
<classes visible="yes" title=""/>
|
|
||||||
<concepts visible="yes" title=""/>
|
|
||||||
<structs visible="yes" title=""/>
|
|
||||||
<exceptions visible="yes" title=""/>
|
|
||||||
<typedefs visible="yes" title=""/>
|
|
||||||
<sequences visible="yes" title=""/>
|
|
||||||
<dictionaries visible="yes" title=""/>
|
|
||||||
<enums visible="yes" title=""/>
|
|
||||||
<functions visible="yes" title=""/>
|
|
||||||
<variables visible="yes" title=""/>
|
|
||||||
<properties visible="yes" title=""/>
|
|
||||||
<membergroups visible="yes" visible="yes"/>
|
|
||||||
</memberdecl>
|
|
||||||
<detaileddescription visible="yes" title=""/>
|
|
||||||
<memberdef>
|
|
||||||
<inlineclasses visible="yes" title=""/>
|
|
||||||
<typedefs visible="yes" title=""/>
|
|
||||||
<sequences visible="yes" title=""/>
|
|
||||||
<dictionaries visible="yes" title=""/>
|
|
||||||
<enums visible="yes" title=""/>
|
|
||||||
<functions visible="yes" title=""/>
|
|
||||||
<variables visible="yes" title=""/>
|
|
||||||
<properties visible="yes" title=""/>
|
|
||||||
</memberdef>
|
|
||||||
<authorsection visible="yes"/>
|
|
||||||
</namespace>
|
|
||||||
|
|
||||||
<!-- Layout definition for a concept page -->
|
|
||||||
<concept>
|
|
||||||
<briefdescription visible="yes"/>
|
|
||||||
<includes visible="$SHOW_HEADERFILE"/>
|
|
||||||
<definition visible="yes" title=""/>
|
|
||||||
<detaileddescription visible="yes" title=""/>
|
|
||||||
<authorsection visible="yes"/>
|
|
||||||
</concept>
|
|
||||||
|
|
||||||
<!-- Layout definition for a file page -->
|
|
||||||
<file>
|
|
||||||
<briefdescription visible="yes"/>
|
|
||||||
<includes visible="$SHOW_INCLUDE_FILES"/>
|
|
||||||
<includegraph visible="yes"/>
|
|
||||||
<includedbygraph visible="yes"/>
|
|
||||||
<sourcelink visible="yes"/>
|
|
||||||
<memberdecl>
|
|
||||||
<interfaces visible="yes" title=""/>
|
|
||||||
<classes visible="yes" title=""/>
|
|
||||||
<structs visible="yes" title=""/>
|
|
||||||
<exceptions visible="yes" title=""/>
|
|
||||||
<namespaces visible="yes" title=""/>
|
|
||||||
<concepts visible="yes" title=""/>
|
|
||||||
<constantgroups visible="yes" title=""/>
|
|
||||||
<defines visible="yes" title=""/>
|
|
||||||
<typedefs visible="yes" title=""/>
|
|
||||||
<sequences visible="yes" title=""/>
|
|
||||||
<dictionaries visible="yes" title=""/>
|
|
||||||
<enums visible="yes" title=""/>
|
|
||||||
<functions visible="yes" title=""/>
|
|
||||||
<variables visible="yes" title=""/>
|
|
||||||
<properties visible="yes" title=""/>
|
|
||||||
<membergroups visible="yes" visible="yes"/>
|
|
||||||
</memberdecl>
|
|
||||||
<detaileddescription visible="yes" title=""/>
|
|
||||||
<memberdef>
|
|
||||||
<inlineclasses visible="yes" title=""/>
|
|
||||||
<defines visible="yes" title=""/>
|
|
||||||
<typedefs visible="yes" title=""/>
|
|
||||||
<sequences visible="yes" title=""/>
|
|
||||||
<dictionaries visible="yes" title=""/>
|
|
||||||
<enums visible="yes" title=""/>
|
|
||||||
<functions visible="yes" title=""/>
|
|
||||||
<variables visible="yes" title=""/>
|
|
||||||
<properties visible="yes" title=""/>
|
|
||||||
</memberdef>
|
|
||||||
<authorsection/>
|
|
||||||
</file>
|
|
||||||
|
|
||||||
<!-- Layout definition for a group page -->
|
|
||||||
<group>
|
|
||||||
<briefdescription visible="yes"/>
|
|
||||||
<groupgraph visible="yes"/>
|
|
||||||
<memberdecl>
|
|
||||||
<nestedgroups visible="yes" title=""/>
|
|
||||||
<modules visible="yes" title=""/>
|
|
||||||
<dirs visible="yes" title=""/>
|
|
||||||
<files visible="yes" title=""/>
|
|
||||||
<namespaces visible="yes" title=""/>
|
|
||||||
<concepts visible="yes" title=""/>
|
|
||||||
<classes visible="yes" title=""/>
|
|
||||||
<defines visible="yes" title=""/>
|
|
||||||
<typedefs visible="yes" title=""/>
|
|
||||||
<sequences visible="yes" title=""/>
|
|
||||||
<dictionaries visible="yes" title=""/>
|
|
||||||
<enums visible="yes" title=""/>
|
|
||||||
<enumvalues visible="yes" title=""/>
|
|
||||||
<functions visible="yes" title=""/>
|
|
||||||
<variables visible="yes" title=""/>
|
|
||||||
<signals visible="yes" title=""/>
|
|
||||||
<publicslots visible="yes" title=""/>
|
|
||||||
<protectedslots visible="yes" title=""/>
|
|
||||||
<privateslots visible="yes" title=""/>
|
|
||||||
<events visible="yes" title=""/>
|
|
||||||
<properties visible="yes" title=""/>
|
|
||||||
<friends visible="yes" title=""/>
|
|
||||||
<membergroups visible="yes"/>
|
|
||||||
</memberdecl>
|
|
||||||
<detaileddescription visible="yes" title=""/>
|
|
||||||
<memberdef>
|
|
||||||
<pagedocs/>
|
|
||||||
<inlineclasses visible="yes" title=""/>
|
|
||||||
<defines visible="yes" title=""/>
|
|
||||||
<typedefs visible="yes" title=""/>
|
|
||||||
<sequences visible="yes" title=""/>
|
|
||||||
<dictionaries visible="yes" title=""/>
|
|
||||||
<enums visible="yes" title=""/>
|
|
||||||
<enumvalues visible="yes" title=""/>
|
|
||||||
<functions visible="yes" title=""/>
|
|
||||||
<variables visible="yes" title=""/>
|
|
||||||
<signals visible="yes" title=""/>
|
|
||||||
<publicslots visible="yes" title=""/>
|
|
||||||
<protectedslots visible="yes" title=""/>
|
|
||||||
<privateslots visible="yes" title=""/>
|
|
||||||
<events visible="yes" title=""/>
|
|
||||||
<properties visible="yes" title=""/>
|
|
||||||
<friends visible="yes" title=""/>
|
|
||||||
</memberdef>
|
|
||||||
<authorsection visible="yes"/>
|
|
||||||
</group>
|
|
||||||
|
|
||||||
<!-- Layout definition for a C++20 module page -->
|
|
||||||
<module>
|
|
||||||
<briefdescription visible="yes"/>
|
|
||||||
<exportedmodules visible="yes"/>
|
|
||||||
<memberdecl>
|
|
||||||
<concepts visible="yes" title=""/>
|
|
||||||
<classes visible="yes" title=""/>
|
|
||||||
<enums visible="yes" title=""/>
|
|
||||||
<typedefs visible="yes" title=""/>
|
|
||||||
<functions visible="yes" title=""/>
|
|
||||||
<variables visible="yes" title=""/>
|
|
||||||
<membergroups visible="yes" title=""/>
|
|
||||||
</memberdecl>
|
|
||||||
<detaileddescription visible="yes" title=""/>
|
|
||||||
<memberdecl>
|
|
||||||
<files visible="yes"/>
|
|
||||||
</memberdecl>
|
|
||||||
</module>
|
|
||||||
|
|
||||||
<!-- Layout definition for a directory page -->
|
|
||||||
<directory>
|
|
||||||
<briefdescription visible="yes"/>
|
|
||||||
<directorygraph visible="yes"/>
|
|
||||||
<memberdecl>
|
|
||||||
<dirs visible="yes"/>
|
|
||||||
<files visible="yes"/>
|
|
||||||
</memberdecl>
|
|
||||||
<detaileddescription visible="yes" title=""/>
|
|
||||||
</directory>
|
|
||||||
</doxygenlayout>
|
</doxygenlayout>
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -34,41 +34,37 @@ pub enum Meaning {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Lot {
|
impl Lot {
|
||||||
pub fn get_text1(&self) -> Option<Cow<'_, str>> {
|
pub fn get_text1(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
Self::Summary(summary) => match &summary.prefix {
|
Self::Summary(summary) => match &summary.prefix {
|
||||||
None => None,
|
None => None,
|
||||||
Some(SummaryPrefix::Draft(text)) => Some(Cow::Borrowed(text)),
|
Some(SummaryPrefix::Draft(text)) => Some(text),
|
||||||
Some(SummaryPrefix::Username(username)) => Some(Cow::Borrowed(username)),
|
Some(SummaryPrefix::Username(username)) => Some(username),
|
||||||
Some(SummaryPrefix::Me(text)) => Some(Cow::Borrowed(text)),
|
Some(SummaryPrefix::Me(text)) => Some(text),
|
||||||
},
|
},
|
||||||
Self::Qr(qr) => match qr {
|
Self::Qr(qr) => match qr {
|
||||||
Qr::AskVerifyContact { .. } => None,
|
Qr::AskVerifyContact { .. } => None,
|
||||||
Qr::AskVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
Qr::AskVerifyGroup { grpname, .. } => Some(grpname),
|
||||||
Qr::AskJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
|
||||||
Qr::FprOk { .. } => None,
|
Qr::FprOk { .. } => None,
|
||||||
Qr::FprMismatch { .. } => None,
|
Qr::FprMismatch { .. } => None,
|
||||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(Cow::Borrowed(fingerprint)),
|
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
|
||||||
Qr::Account { domain } => Some(Cow::Borrowed(domain)),
|
Qr::Account { domain } => Some(domain),
|
||||||
Qr::Backup2 { .. } => None,
|
Qr::Backup { .. } => None,
|
||||||
Qr::BackupTooNew { .. } => None,
|
Qr::WebrtcInstance { domain, .. } => Some(domain),
|
||||||
Qr::Proxy { host, port, .. } => Some(Cow::Owned(format!("{host}:{port}"))),
|
Qr::Addr { draft, .. } => draft.as_deref(),
|
||||||
Qr::Addr { draft, .. } => draft.as_deref().map(Cow::Borrowed),
|
Qr::Url { url } => Some(url),
|
||||||
Qr::Url { url } => Some(Cow::Borrowed(url)),
|
Qr::Text { text } => Some(text),
|
||||||
Qr::Text { text } => Some(Cow::Borrowed(text)),
|
|
||||||
Qr::WithdrawVerifyContact { .. } => None,
|
Qr::WithdrawVerifyContact { .. } => None,
|
||||||
Qr::WithdrawVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
Qr::WithdrawVerifyGroup { grpname, .. } => Some(grpname),
|
||||||
Qr::WithdrawJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
|
||||||
Qr::ReviveVerifyContact { .. } => None,
|
Qr::ReviveVerifyContact { .. } => None,
|
||||||
Qr::ReviveVerifyGroup { grpname, .. } => Some(Cow::Borrowed(grpname)),
|
Qr::ReviveVerifyGroup { grpname, .. } => Some(grpname),
|
||||||
Qr::ReviveJoinBroadcast { name, .. } => Some(Cow::Borrowed(name)),
|
Qr::Login { address, .. } => Some(address),
|
||||||
Qr::Login { address, .. } => Some(Cow::Borrowed(address)),
|
|
||||||
},
|
},
|
||||||
Self::Error(err) => Some(Cow::Borrowed(err)),
|
Self::Error(err) => Some(err),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_text2(&self) -> Option<Cow<'_, str>> {
|
pub fn get_text2(&self) -> Option<Cow<str>> {
|
||||||
match self {
|
match self {
|
||||||
Self::Summary(summary) => Some(summary.truncated_text(160)),
|
Self::Summary(summary) => Some(summary.truncated_text(160)),
|
||||||
Self::Qr(_) => None,
|
Self::Qr(_) => None,
|
||||||
@@ -101,23 +97,19 @@ impl Lot {
|
|||||||
Self::Qr(qr) => match qr {
|
Self::Qr(qr) => match qr {
|
||||||
Qr::AskVerifyContact { .. } => LotState::QrAskVerifyContact,
|
Qr::AskVerifyContact { .. } => LotState::QrAskVerifyContact,
|
||||||
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
|
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
|
||||||
Qr::AskJoinBroadcast { .. } => LotState::QrAskJoinBroadcast,
|
|
||||||
Qr::FprOk { .. } => LotState::QrFprOk,
|
Qr::FprOk { .. } => LotState::QrFprOk,
|
||||||
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
||||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
||||||
Qr::Account { .. } => LotState::QrAccount,
|
Qr::Account { .. } => LotState::QrAccount,
|
||||||
Qr::Backup2 { .. } => LotState::QrBackup2,
|
Qr::Backup { .. } => LotState::QrBackup,
|
||||||
Qr::BackupTooNew { .. } => LotState::QrBackupTooNew,
|
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
||||||
Qr::Proxy { .. } => LotState::QrProxy,
|
|
||||||
Qr::Addr { .. } => LotState::QrAddr,
|
Qr::Addr { .. } => LotState::QrAddr,
|
||||||
Qr::Url { .. } => LotState::QrUrl,
|
Qr::Url { .. } => LotState::QrUrl,
|
||||||
Qr::Text { .. } => LotState::QrText,
|
Qr::Text { .. } => LotState::QrText,
|
||||||
Qr::WithdrawVerifyContact { .. } => LotState::QrWithdrawVerifyContact,
|
Qr::WithdrawVerifyContact { .. } => LotState::QrWithdrawVerifyContact,
|
||||||
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
|
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
|
||||||
Qr::WithdrawJoinBroadcast { .. } => LotState::QrWithdrawJoinBroadcast,
|
|
||||||
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
|
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
|
||||||
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
|
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
|
||||||
Qr::ReviveJoinBroadcast { .. } => LotState::QrReviveJoinBroadcast,
|
|
||||||
Qr::Login { .. } => LotState::QrLogin,
|
Qr::Login { .. } => LotState::QrLogin,
|
||||||
},
|
},
|
||||||
Self::Error(_err) => LotState::QrError,
|
Self::Error(_err) => LotState::QrError,
|
||||||
@@ -130,23 +122,19 @@ impl Lot {
|
|||||||
Self::Qr(qr) => match qr {
|
Self::Qr(qr) => match qr {
|
||||||
Qr::AskVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
Qr::AskVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||||
Qr::AskVerifyGroup { .. } => Default::default(),
|
Qr::AskVerifyGroup { .. } => Default::default(),
|
||||||
Qr::AskJoinBroadcast { .. } => Default::default(),
|
|
||||||
Qr::FprOk { contact_id } => contact_id.to_u32(),
|
Qr::FprOk { contact_id } => contact_id.to_u32(),
|
||||||
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
|
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
|
||||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
Qr::FprWithoutAddr { .. } => Default::default(),
|
||||||
Qr::Account { .. } => Default::default(),
|
Qr::Account { .. } => Default::default(),
|
||||||
Qr::Backup2 { .. } => Default::default(),
|
Qr::Backup { .. } => Default::default(),
|
||||||
Qr::BackupTooNew { .. } => Default::default(),
|
Qr::WebrtcInstance { .. } => Default::default(),
|
||||||
Qr::Proxy { .. } => Default::default(),
|
|
||||||
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
Qr::Addr { contact_id, .. } => contact_id.to_u32(),
|
||||||
Qr::Url { .. } => Default::default(),
|
Qr::Url { .. } => Default::default(),
|
||||||
Qr::Text { .. } => Default::default(),
|
Qr::Text { .. } => Default::default(),
|
||||||
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||||
Qr::WithdrawVerifyGroup { .. } | Qr::WithdrawJoinBroadcast { .. } => {
|
Qr::WithdrawVerifyGroup { .. } => Default::default(),
|
||||||
Default::default()
|
|
||||||
}
|
|
||||||
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
||||||
Qr::ReviveVerifyGroup { .. } | Qr::ReviveJoinBroadcast { .. } => Default::default(),
|
Qr::ReviveVerifyGroup { .. } => Default::default(),
|
||||||
Qr::Login { .. } => Default::default(),
|
Qr::Login { .. } => Default::default(),
|
||||||
},
|
},
|
||||||
Self::Error(_) => Default::default(),
|
Self::Error(_) => Default::default(),
|
||||||
@@ -175,9 +163,6 @@ pub enum LotState {
|
|||||||
/// text1=groupname
|
/// text1=groupname
|
||||||
QrAskVerifyGroup = 202,
|
QrAskVerifyGroup = 202,
|
||||||
|
|
||||||
/// text1=broadcast_name
|
|
||||||
QrAskJoinBroadcast = 204,
|
|
||||||
|
|
||||||
/// id=contact
|
/// id=contact
|
||||||
QrFprOk = 210,
|
QrFprOk = 210,
|
||||||
|
|
||||||
@@ -190,12 +175,10 @@ pub enum LotState {
|
|||||||
/// text1=domain
|
/// text1=domain
|
||||||
QrAccount = 250,
|
QrAccount = 250,
|
||||||
|
|
||||||
QrBackup2 = 252,
|
QrBackup = 251,
|
||||||
|
|
||||||
QrBackupTooNew = 255,
|
/// text1=domain, text2=instance pattern
|
||||||
|
QrWebrtcInstance = 260,
|
||||||
/// text1=address, text2=protocol
|
|
||||||
QrProxy = 271,
|
|
||||||
|
|
||||||
/// id=contact
|
/// id=contact
|
||||||
QrAddr = 320,
|
QrAddr = 320,
|
||||||
@@ -213,15 +196,11 @@ pub enum LotState {
|
|||||||
|
|
||||||
/// text1=groupname
|
/// text1=groupname
|
||||||
QrWithdrawVerifyGroup = 502,
|
QrWithdrawVerifyGroup = 502,
|
||||||
/// text1=broadcast channel name
|
|
||||||
QrWithdrawJoinBroadcast = 504,
|
|
||||||
|
|
||||||
QrReviveVerifyContact = 510,
|
QrReviveVerifyContact = 510,
|
||||||
|
|
||||||
/// text1=groupname
|
/// text1=groupname
|
||||||
QrReviveVerifyGroup = 512,
|
QrReviveVerifyGroup = 512,
|
||||||
/// text1=groupname
|
|
||||||
QrReviveJoinBroadcast = 514,
|
|
||||||
|
|
||||||
/// text1=email_address
|
/// text1=email_address
|
||||||
QrLogin = 520,
|
QrLogin = 520,
|
||||||
|
|||||||
@@ -1,33 +1,44 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-jsonrpc"
|
name = "deltachat-jsonrpc"
|
||||||
version = "2.31.0"
|
version = "1.137.2"
|
||||||
description = "DeltaChat JSON-RPC API"
|
description = "DeltaChat JSON-RPC API"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
default-run = "deltachat-jsonrpc-server"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
repository = "https://github.com/chatmail/core"
|
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "deltachat-jsonrpc-server"
|
||||||
|
path = "src/webserver.rs"
|
||||||
|
required-features = ["webserver"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
anyhow = "1"
|
||||||
deltachat = { workspace = true }
|
deltachat = { path = ".." }
|
||||||
deltachat-contact-tools = { workspace = true }
|
num-traits = "0.2"
|
||||||
num-traits = { workspace = true }
|
schemars = "0.8.13"
|
||||||
schemars = "0.8.22"
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde = { workspace = true, features = ["derive"] }
|
tempfile = "3.10.1"
|
||||||
async-channel = { workspace = true }
|
log = "0.4"
|
||||||
serde_json = { workspace = true }
|
async-channel = { version = "2.0.0" }
|
||||||
yerpc = { workspace = true, features = ["anyhow_expose", "openrpc"] }
|
futures = { version = "0.3.30" }
|
||||||
typescript-type-def = { version = "0.5.13", features = ["json_value"] }
|
serde_json = "1"
|
||||||
tokio = { workspace = true }
|
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||||
sanitize-filename = { workspace = true }
|
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
|
||||||
|
tokio = { version = "1.37.0" }
|
||||||
|
sanitize-filename = "0.5"
|
||||||
walkdir = "2.5.0"
|
walkdir = "2.5.0"
|
||||||
base64 = { workspace = true }
|
base64 = "0.21"
|
||||||
|
|
||||||
|
# optional dependencies
|
||||||
|
axum = { version = "0.7", optional = true, features = ["ws"] }
|
||||||
|
env_logger = { version = "0.10.0", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { workspace = true, features = ["full", "rt-multi-thread"] }
|
tokio = { version = "1.37.0", features = ["full", "rt-multi-thread"] }
|
||||||
tempfile = { workspace = true }
|
|
||||||
futures = { workspace = true }
|
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
|
webserver = ["dep:env_logger", "dep:axum", "tokio/full", "yerpc/support-axum"]
|
||||||
vendored = ["deltachat/vendored"]
|
vendored = ["deltachat/vendored"]
|
||||||
|
|||||||
@@ -4,16 +4,46 @@ This crate provides a [JSON-RPC 2.0](https://www.jsonrpc.org/specification) inte
|
|||||||
|
|
||||||
The JSON-RPC API is exposed in two fashions:
|
The JSON-RPC API is exposed in two fashions:
|
||||||
|
|
||||||
* A executable `deltachat-rpc-server` that exposes the JSON-RPC API through stdio.
|
* A executable that exposes the JSON-RPC API through a [WebSocket](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API) server running on localhost.
|
||||||
* The JSON-RPC API can also be called through the [C FFI](../deltachat-ffi). It exposes the functions `dc_jsonrpc_init`, `dc_jsonrpc_request`, `dc_jsonrpc_next_response` and `dc_jsonrpc_unref`. See the docs in the [header file](../deltachat-ffi/deltachat.h) for details.
|
* The JSON-RPC API can also be called through the [C FFI](../deltachat-ffi). The C FFI needs to be built with the `jsonrpc` feature. It will then expose the functions `dc_jsonrpc_init`, `dc_jsonrpc_request`, `dc_jsonrpc_next_response` and `dc_jsonrpc_unref`. See the docs in the [header file](../deltachat-ffi/deltachat.h) for details.
|
||||||
|
|
||||||
We also include a JavaScript and TypeScript client for the JSON-RPC API. The source for this is in the [`typescript`](typescript) folder.
|
We also include a JavaScript and TypeScript client for the JSON-RPC API. The source for this is in the [`typescript`](typescript) folder. The client can easily be used with the WebSocket server to build DeltaChat apps for web browsers or Node.js. See the [examples](typescript/example) for details.
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
|
#### Running the WebSocket server
|
||||||
|
|
||||||
|
From within this folder, you can start the WebSocket server with the following command:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo run --features webserver
|
||||||
|
```
|
||||||
|
|
||||||
|
If you want to use the server in a production setup, first build it in release mode:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cargo build --features webserver --release
|
||||||
|
```
|
||||||
|
You will then find the `deltachat-jsonrpc-server` executable in your `target/release` folder.
|
||||||
|
|
||||||
|
The executable currently does not support any command-line arguments. By default, once started it will accept WebSocket connections on `ws://localhost:20808/ws`. It will store the persistent configuration and databases in a `./accounts` folder relative to the directory from where it is started.
|
||||||
|
|
||||||
|
The server can be configured with environment variables:
|
||||||
|
|
||||||
|
|variable|default|description|
|
||||||
|
|-|-|-|
|
||||||
|
|`DC_PORT`|`20808`|port to listen on|
|
||||||
|
|`DC_ACCOUNTS_PATH`|`./accounts`|path to storage directory|
|
||||||
|
|
||||||
|
If you are targeting other architectures (like KaiOS or Android), the webserver binary can be cross-compiled easily with [rust-cross](https://github.com/cross-rs/cross):
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cross build --features=webserver --target armv7-linux-androideabi --release
|
||||||
|
```
|
||||||
|
|
||||||
#### Using the TypeScript/JavaScript client
|
#### Using the TypeScript/JavaScript client
|
||||||
|
|
||||||
The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/chatmail/yerpc)). Find the source in the [`typescript`](typescript) folder.
|
The package includes a JavaScript/TypeScript client which is partially auto-generated through the JSON-RPC library used by this crate ([yerpc](https://github.com/Frando/yerpc/)). Find the source in the [`typescript`](typescript) folder.
|
||||||
|
|
||||||
To use it locally, first install the dependencies and compile the TypeScript code to JavaScript:
|
To use it locally, first install the dependencies and compile the TypeScript code to JavaScript:
|
||||||
```sh
|
```sh
|
||||||
@@ -22,7 +52,15 @@ npm install
|
|||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
The JavaScript client is [published on NPM](https://www.npmjs.com/package/@deltachat/jsonrpc-client).
|
The JavaScript client is not yet published on NPM (but will likely be soon). Currently, it is recommended to vendor the bundled build. After running `npm run build` as documented above, there will be a file `dist/deltachat.bundle.js`. This is an ESM module containing all dependencies. Copy this file to your project and import the DeltaChat class.
|
||||||
|
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
import { DeltaChat } from './deltachat.bundle.js'
|
||||||
|
const dc = new DeltaChat('ws://localhost:20808/ws')
|
||||||
|
const accounts = await dc.rpc.getAllAccounts()
|
||||||
|
console.log('accounts', accounts)
|
||||||
|
```
|
||||||
|
|
||||||
A script is included to build autogenerated documentation, which includes all RPC methods:
|
A script is included to build autogenerated documentation, which includes all RPC methods:
|
||||||
```sh
|
```sh
|
||||||
@@ -35,6 +73,18 @@ Then open the [`typescript/docs`](typescript/docs) folder in a web browser.
|
|||||||
|
|
||||||
#### Running the example app
|
#### Running the example app
|
||||||
|
|
||||||
|
We include a small demo web application that talks to the WebSocket server. It can be used for testing. Feel invited to expand this.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
cd typescript
|
||||||
|
npm run build
|
||||||
|
npm run example:build
|
||||||
|
npm run example:start
|
||||||
|
```
|
||||||
|
Then, open [`http://localhost:8080/example.html`](http://localhost:8080/example.html) in a web browser.
|
||||||
|
|
||||||
|
Run `npm run example:dev` to live-rebuild the example app when files changes.
|
||||||
|
|
||||||
### Testing
|
### Testing
|
||||||
|
|
||||||
The crate includes both a basic Rust smoke test and more featureful integration tests that use the TypeScript client.
|
The crate includes both a basic Rust smoke test and more featureful integration tests that use the TypeScript client.
|
||||||
@@ -54,12 +104,14 @@ cd typescript
|
|||||||
npm run test
|
npm run test
|
||||||
```
|
```
|
||||||
|
|
||||||
This will build the `deltachat-jsonrpc-server` binary and then run a test suite.
|
This will build the `deltachat-jsonrpc-server` binary and then run a test suite against the WebSocket server.
|
||||||
|
|
||||||
The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, set the `CHATMAIL_DOMAIN` environment variable to your testing email server domain.
|
The test suite includes some tests that need online connectivity and a way to create test email accounts. To run these tests, talk to DeltaChat developers to get a token for the `testrun.org` service, or use a local instance of [`mailadm`](https://github.com/deltachat/docker-mailadm).
|
||||||
|
|
||||||
|
Then, set the `CHATMAIL_DOMAIN` environment variable to your testing email server domain.
|
||||||
|
|
||||||
```
|
```
|
||||||
CHATMAIL_DOMAIN=ci-chatmail.testrun.org npm run test
|
CHATMAIL_DOMAIN=chat.example.org npm run test
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Test Coverage
|
#### Test Coverage
|
||||||
|
|||||||
28
deltachat-jsonrpc/TODO.md
Normal file
28
deltachat-jsonrpc/TODO.md
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
# TODO
|
||||||
|
|
||||||
|
- [ ] different test type to simulate two devices: to test autocrypt_initiate_key_transfer & autocrypt_continue_key_transfer
|
||||||
|
|
||||||
|
## MVP - Websocket server&client
|
||||||
|
|
||||||
|
For kaiOS and other experiments, like a deltachat "web" over network from an android phone.
|
||||||
|
|
||||||
|
- [ ] coverage for a majority of the API
|
||||||
|
- [ ] Blobs served
|
||||||
|
- [ ] Blob upload (for attachments, setting profile-picture, importing backup and so on)
|
||||||
|
- [ ] other way blobs can be addressed when using websocket vs. jsonrpc over dc-node
|
||||||
|
- [ ] Web push API? At least some kind of notification hook closure this lib can accept.
|
||||||
|
|
||||||
|
### Other Ideas for the Websocket server
|
||||||
|
|
||||||
|
- [ ] make sure there can only be one connection at a time to the ws
|
||||||
|
- why? , it could give problems if its commanded from multiple connections
|
||||||
|
- [ ] encrypted connection?
|
||||||
|
- [ ] authenticated connection?
|
||||||
|
- [ ] Look into unit-testing for the proc macros?
|
||||||
|
- [ ] proc macro taking over doc comments to generated typescript file
|
||||||
|
|
||||||
|
## Desktop Apis
|
||||||
|
|
||||||
|
Incomplete todo for desktop api porting, just some remainders for points that might need more work:
|
||||||
|
|
||||||
|
- [ ] manual start/stop io functions in the api for context and accounts, so "not syncing all accounts" can still be done in desktop -> webserver should then not do start io on all accounts by default
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -15,11 +15,8 @@ pub enum Account {
|
|||||||
display_name: Option<String>,
|
display_name: Option<String>,
|
||||||
addr: Option<String>,
|
addr: Option<String>,
|
||||||
// size: u32,
|
// size: u32,
|
||||||
profile_image: Option<String>,
|
profile_image: Option<String>, // TODO: This needs to be converted to work with blob http server.
|
||||||
color: String,
|
color: String,
|
||||||
/// Optional tag as "Work", "Family".
|
|
||||||
/// Meant to help profile owner to differ between profiles with similar names.
|
|
||||||
private_tag: Option<String>,
|
|
||||||
},
|
},
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
Unconfigured { id: u32 },
|
Unconfigured { id: u32 },
|
||||||
@@ -32,19 +29,14 @@ impl Account {
|
|||||||
let addr = ctx.get_config(Config::Addr).await?;
|
let addr = ctx.get_config(Config::Addr).await?;
|
||||||
let profile_image = ctx.get_config(Config::Selfavatar).await?;
|
let profile_image = ctx.get_config(Config::Selfavatar).await?;
|
||||||
let color = color_int_to_hex_string(
|
let color = color_int_to_hex_string(
|
||||||
Contact::get_by_id(ctx, ContactId::SELF)
|
Contact::get_by_id(ctx, ContactId::SELF).await?.get_color(),
|
||||||
.await?
|
|
||||||
.get_or_gen_color(ctx)
|
|
||||||
.await?,
|
|
||||||
);
|
);
|
||||||
let private_tag = ctx.get_config(Config::PrivateTag).await?;
|
|
||||||
Ok(Account::Configured {
|
Ok(Account::Configured {
|
||||||
id,
|
id,
|
||||||
display_name,
|
display_name,
|
||||||
addr,
|
addr,
|
||||||
profile_image,
|
profile_image,
|
||||||
color,
|
color,
|
||||||
private_tag,
|
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
Ok(Account::Unconfigured { id })
|
Ok(Account::Unconfigured { id })
|
||||||
|
|||||||
@@ -1,97 +0,0 @@
|
|||||||
use anyhow::{Context as _, Result};
|
|
||||||
|
|
||||||
use deltachat::calls::{call_state, sdp_has_video, CallState};
|
|
||||||
use deltachat::context::Context;
|
|
||||||
use deltachat::message::MsgId;
|
|
||||||
use serde::Serialize;
|
|
||||||
use typescript_type_def::TypeDef;
|
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
|
||||||
#[serde(rename = "CallInfo", rename_all = "camelCase")]
|
|
||||||
pub struct JsonrpcCallInfo {
|
|
||||||
/// SDP offer.
|
|
||||||
///
|
|
||||||
/// Can be used to manually answer the call
|
|
||||||
/// even if incoming call event was missed.
|
|
||||||
pub sdp_offer: String,
|
|
||||||
|
|
||||||
/// True if SDP offer has a video.
|
|
||||||
pub has_video: bool,
|
|
||||||
|
|
||||||
/// Call state.
|
|
||||||
///
|
|
||||||
/// For example, if the call is accepted, active, canceled, declined etc.
|
|
||||||
pub state: JsonrpcCallState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JsonrpcCallInfo {
|
|
||||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<JsonrpcCallInfo> {
|
|
||||||
let call_info = context.load_call_by_id(msg_id).await?.with_context(|| {
|
|
||||||
format!("Attempting to get call state of non-call message {msg_id}")
|
|
||||||
})?;
|
|
||||||
let sdp_offer = call_info.place_call_info.clone();
|
|
||||||
let has_video = sdp_has_video(&sdp_offer).unwrap_or_default();
|
|
||||||
let state = JsonrpcCallState::from_msg_id(context, msg_id).await?;
|
|
||||||
|
|
||||||
Ok(JsonrpcCallInfo {
|
|
||||||
sdp_offer,
|
|
||||||
has_video,
|
|
||||||
state,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
|
||||||
#[serde(rename = "CallState", tag = "kind")]
|
|
||||||
pub enum JsonrpcCallState {
|
|
||||||
/// Fresh incoming or outgoing call that is still ringing.
|
|
||||||
///
|
|
||||||
/// There is no separate state for outgoing call
|
|
||||||
/// that has been dialled but not ringing on the other side yet
|
|
||||||
/// as we don't know whether the other side received our call.
|
|
||||||
Alerting,
|
|
||||||
|
|
||||||
/// Active call.
|
|
||||||
Active,
|
|
||||||
|
|
||||||
/// Completed call that was once active
|
|
||||||
/// and then was terminated for any reason.
|
|
||||||
Completed {
|
|
||||||
/// Call duration in seconds.
|
|
||||||
duration: i64,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Incoming call that was not picked up within a timeout
|
|
||||||
/// or was explicitly ended by the caller before we picked up.
|
|
||||||
Missed,
|
|
||||||
|
|
||||||
/// Incoming call that was explicitly ended on our side
|
|
||||||
/// before picking up or outgoing call
|
|
||||||
/// that was declined before the timeout.
|
|
||||||
Declined,
|
|
||||||
|
|
||||||
/// Outgoing call that has been canceled on our side
|
|
||||||
/// before receiving a response.
|
|
||||||
///
|
|
||||||
/// Incoming calls cannot be canceled,
|
|
||||||
/// on the receiver side canceled calls
|
|
||||||
/// usually result in missed calls.
|
|
||||||
Canceled,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl JsonrpcCallState {
|
|
||||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<JsonrpcCallState> {
|
|
||||||
let call_state = call_state(context, msg_id).await?;
|
|
||||||
|
|
||||||
let jsonrpc_call_state = match call_state {
|
|
||||||
CallState::Alerting => JsonrpcCallState::Alerting,
|
|
||||||
CallState::Active => JsonrpcCallState::Active,
|
|
||||||
CallState::Completed { duration } => JsonrpcCallState::Completed { duration },
|
|
||||||
CallState::Missed => JsonrpcCallState::Missed,
|
|
||||||
CallState::Declined => JsonrpcCallState::Declined,
|
|
||||||
CallState::Canceled => JsonrpcCallState::Canceled,
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(jsonrpc_call_state)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,11 +1,12 @@
|
|||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use anyhow::{bail, Context as _, Result};
|
use anyhow::{bail, Context as _, Result};
|
||||||
use deltachat::chat::{self, get_chat_contacts, get_past_chat_contacts, ChatVisibility};
|
use deltachat::chat::{self, get_chat_contacts, ChatVisibility};
|
||||||
use deltachat::chat::{Chat, ChatId};
|
use deltachat::chat::{Chat, ChatId};
|
||||||
use deltachat::constants::Chattype;
|
use deltachat::constants::Chattype;
|
||||||
use deltachat::contact::{Contact, ContactId};
|
use deltachat::contact::{Contact, ContactId};
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
|
use num_traits::cast::ToPrimitive;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
@@ -18,58 +19,34 @@ pub struct FullChat {
|
|||||||
id: u32,
|
id: u32,
|
||||||
name: String,
|
name: String,
|
||||||
|
|
||||||
/// True if the chat is encrypted.
|
/// True if the chat is protected.
|
||||||
/// This means that all messages in the chat are encrypted,
|
|
||||||
/// and all contacts in the chat are "key-contacts",
|
|
||||||
/// i.e. identified by the PGP key fingerprint.
|
|
||||||
///
|
///
|
||||||
/// False if the chat is unencrypted.
|
/// UI should display a green checkmark
|
||||||
/// This means that all messages in the chat are unencrypted,
|
/// in the chat title,
|
||||||
/// and all contacts in the chat are "address-contacts",
|
/// in the chat profile title and
|
||||||
/// i.e. identified by the email address.
|
/// in the chatlist item
|
||||||
/// The UI should mark this chat e.g. with a mail-letter icon.
|
/// if chat protection is enabled.
|
||||||
///
|
/// UI should also display a green checkmark
|
||||||
/// Unencrypted groups are called "ad-hoc groups"
|
/// in the contact profile
|
||||||
/// and the user can't add/remove members,
|
/// if 1:1 chat with this contact exists and is protected.
|
||||||
/// create a QR invite code,
|
is_protected: bool,
|
||||||
/// or set an avatar.
|
|
||||||
/// These options should therefore be disabled in the UI.
|
|
||||||
///
|
|
||||||
/// Note that it can happen that an encrypted chat
|
|
||||||
/// contains unencrypted messages that were received in core <= v1.159.*
|
|
||||||
/// and vice versa.
|
|
||||||
///
|
|
||||||
/// See also `is_key_contact` on `Contact`.
|
|
||||||
is_encrypted: bool,
|
|
||||||
profile_image: Option<String>, //BLOBS ?
|
profile_image: Option<String>, //BLOBS ?
|
||||||
archived: bool,
|
archived: bool,
|
||||||
pinned: bool,
|
|
||||||
// subtitle - will be moved to frontend because it uses translation functions
|
// subtitle - will be moved to frontend because it uses translation functions
|
||||||
chat_type: JsonrpcChatType,
|
chat_type: u32,
|
||||||
is_unpromoted: bool,
|
is_unpromoted: bool,
|
||||||
is_self_talk: bool,
|
is_self_talk: bool,
|
||||||
contacts: Vec<ContactObject>,
|
contacts: Vec<ContactObject>,
|
||||||
contact_ids: Vec<u32>,
|
contact_ids: Vec<u32>,
|
||||||
|
|
||||||
/// Contact IDs of the past chat members.
|
|
||||||
past_contact_ids: Vec<u32>,
|
|
||||||
|
|
||||||
color: String,
|
color: String,
|
||||||
fresh_message_counter: usize,
|
fresh_message_counter: usize,
|
||||||
// is_group - please check over chat.type in frontend instead
|
// is_group - please check over chat.type in frontend instead
|
||||||
is_contact_request: bool,
|
is_contact_request: bool,
|
||||||
|
is_protection_broken: bool,
|
||||||
is_device_chat: bool,
|
is_device_chat: bool,
|
||||||
/// Note that this is different from
|
|
||||||
/// [`ChatListItem::is_self_in_group`](`crate::api::types::chat_list::ChatListItemFetchResult::ChatListItem::is_self_in_group`).
|
|
||||||
/// This property should only be accessed
|
|
||||||
/// when [`FullChat::chat_type`] is [`Chattype::Group`].
|
|
||||||
//
|
|
||||||
// We could utilize [`Chat::is_self_in_chat`],
|
|
||||||
// but that would be an extra DB query.
|
|
||||||
self_in_group: bool,
|
self_in_group: bool,
|
||||||
is_muted: bool,
|
is_muted: bool,
|
||||||
ephemeral_timer: u32,
|
ephemeral_timer: u32, //TODO look if there are more important properties in newer core versions
|
||||||
can_send: bool,
|
can_send: bool,
|
||||||
was_seen_recently: bool,
|
was_seen_recently: bool,
|
||||||
mailing_list_address: Option<String>,
|
mailing_list_address: Option<String>,
|
||||||
@@ -81,7 +58,6 @@ impl FullChat {
|
|||||||
let chat = Chat::load_from_db(context, rust_chat_id).await?;
|
let chat = Chat::load_from_db(context, rust_chat_id).await?;
|
||||||
|
|
||||||
let contact_ids = get_chat_contacts(context, rust_chat_id).await?;
|
let contact_ids = get_chat_contacts(context, rust_chat_id).await?;
|
||||||
let past_contact_ids = get_past_chat_contacts(context, rust_chat_id).await?;
|
|
||||||
|
|
||||||
let mut contacts = Vec::with_capacity(contact_ids.len());
|
let mut contacts = Vec::with_capacity(contact_ids.len());
|
||||||
|
|
||||||
@@ -125,19 +101,18 @@ impl FullChat {
|
|||||||
Ok(FullChat {
|
Ok(FullChat {
|
||||||
id: chat_id,
|
id: chat_id,
|
||||||
name: chat.name.clone(),
|
name: chat.name.clone(),
|
||||||
is_encrypted: chat.is_encrypted(context).await?,
|
is_protected: chat.is_protected(),
|
||||||
profile_image, //BLOBS ?
|
profile_image, //BLOBS ?
|
||||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||||
chat_type: chat.get_type().into(),
|
|
||||||
is_unpromoted: chat.is_unpromoted(),
|
is_unpromoted: chat.is_unpromoted(),
|
||||||
is_self_talk: chat.is_self_talk(),
|
is_self_talk: chat.is_self_talk(),
|
||||||
contacts,
|
contacts,
|
||||||
contact_ids: contact_ids.iter().map(|id| id.to_u32()).collect(),
|
contact_ids: contact_ids.iter().map(|id| id.to_u32()).collect(),
|
||||||
past_contact_ids: past_contact_ids.iter().map(|id| id.to_u32()).collect(),
|
|
||||||
color,
|
color,
|
||||||
fresh_message_counter,
|
fresh_message_counter,
|
||||||
is_contact_request: chat.is_contact_request(),
|
is_contact_request: chat.is_contact_request(),
|
||||||
|
is_protection_broken: chat.is_protection_broken(),
|
||||||
is_device_chat: chat.is_device_talk(),
|
is_device_chat: chat.is_device_talk(),
|
||||||
self_in_group: contact_ids.contains(&ContactId::SELF),
|
self_in_group: contact_ids.contains(&ContactId::SELF),
|
||||||
is_muted: chat.is_muted(),
|
is_muted: chat.is_muted(),
|
||||||
@@ -165,38 +140,25 @@ pub struct BasicChat {
|
|||||||
id: u32,
|
id: u32,
|
||||||
name: String,
|
name: String,
|
||||||
|
|
||||||
/// True if the chat is encrypted.
|
/// True if the chat is protected.
|
||||||
/// This means that all messages in the chat are encrypted,
|
|
||||||
/// and all contacts in the chat are "key-contacts",
|
|
||||||
/// i.e. identified by the PGP key fingerprint.
|
|
||||||
///
|
///
|
||||||
/// False if the chat is unencrypted.
|
/// UI should display a green checkmark
|
||||||
/// This means that all messages in the chat are unencrypted,
|
/// in the chat title,
|
||||||
/// and all contacts in the chat are "address-contacts",
|
/// in the chat profile title and
|
||||||
/// i.e. identified by the email address.
|
/// in the chatlist item
|
||||||
/// The UI should mark this chat e.g. with a mail-letter icon.
|
/// if chat protection is enabled.
|
||||||
///
|
/// UI should also display a green checkmark
|
||||||
/// Unencrypted groups are called "ad-hoc groups"
|
/// in the contact profile
|
||||||
/// and the user can't add/remove members,
|
/// if 1:1 chat with this contact exists and is protected.
|
||||||
/// create a QR invite code,
|
is_protected: bool,
|
||||||
/// or set an avatar.
|
|
||||||
/// These options should therefore be disabled in the UI.
|
|
||||||
///
|
|
||||||
/// Note that it can happen that an encrypted chat
|
|
||||||
/// contains unencrypted messages that were received in core <= v1.159.*
|
|
||||||
/// and vice versa.
|
|
||||||
///
|
|
||||||
/// See also `is_key_contact` on `Contact`.
|
|
||||||
is_encrypted: bool,
|
|
||||||
profile_image: Option<String>, //BLOBS ?
|
profile_image: Option<String>, //BLOBS ?
|
||||||
archived: bool,
|
archived: bool,
|
||||||
pinned: bool,
|
chat_type: u32,
|
||||||
chat_type: JsonrpcChatType,
|
|
||||||
is_unpromoted: bool,
|
is_unpromoted: bool,
|
||||||
is_self_talk: bool,
|
is_self_talk: bool,
|
||||||
color: String,
|
color: String,
|
||||||
is_contact_request: bool,
|
is_contact_request: bool,
|
||||||
|
is_protection_broken: bool,
|
||||||
is_device_chat: bool,
|
is_device_chat: bool,
|
||||||
is_muted: bool,
|
is_muted: bool,
|
||||||
}
|
}
|
||||||
@@ -215,15 +177,15 @@ impl BasicChat {
|
|||||||
Ok(BasicChat {
|
Ok(BasicChat {
|
||||||
id: chat_id,
|
id: chat_id,
|
||||||
name: chat.name.clone(),
|
name: chat.name.clone(),
|
||||||
is_encrypted: chat.is_encrypted(context).await?,
|
is_protected: chat.is_protected(),
|
||||||
profile_image, //BLOBS ?
|
profile_image, //BLOBS ?
|
||||||
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
archived: chat.get_visibility() == chat::ChatVisibility::Archived,
|
||||||
pinned: chat.get_visibility() == chat::ChatVisibility::Pinned,
|
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||||
chat_type: chat.get_type().into(),
|
|
||||||
is_unpromoted: chat.is_unpromoted(),
|
is_unpromoted: chat.is_unpromoted(),
|
||||||
is_self_talk: chat.is_self_talk(),
|
is_self_talk: chat.is_self_talk(),
|
||||||
color,
|
color,
|
||||||
is_contact_request: chat.is_contact_request(),
|
is_contact_request: chat.is_contact_request(),
|
||||||
|
is_protection_broken: chat.is_protection_broken(),
|
||||||
is_device_chat: chat.is_device_talk(),
|
is_device_chat: chat.is_device_talk(),
|
||||||
is_muted: chat.is_muted(),
|
is_muted: chat.is_muted(),
|
||||||
})
|
})
|
||||||
@@ -258,52 +220,18 @@ impl MuteDuration {
|
|||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "ChatVisibility")]
|
#[serde(rename = "ChatVisibility")]
|
||||||
pub enum JsonrpcChatVisibility {
|
pub enum JSONRPCChatVisibility {
|
||||||
Normal,
|
Normal,
|
||||||
Archived,
|
Archived,
|
||||||
Pinned,
|
Pinned,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JsonrpcChatVisibility {
|
impl JSONRPCChatVisibility {
|
||||||
pub fn into_core_type(self) -> ChatVisibility {
|
pub fn into_core_type(self) -> ChatVisibility {
|
||||||
match self {
|
match self {
|
||||||
JsonrpcChatVisibility::Normal => ChatVisibility::Normal,
|
JSONRPCChatVisibility::Normal => ChatVisibility::Normal,
|
||||||
JsonrpcChatVisibility::Archived => ChatVisibility::Archived,
|
JSONRPCChatVisibility::Archived => ChatVisibility::Archived,
|
||||||
JsonrpcChatVisibility::Pinned => ChatVisibility::Pinned,
|
JSONRPCChatVisibility::Pinned => ChatVisibility::Pinned,
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, PartialEq, TypeDef, schemars::JsonSchema)]
|
|
||||||
#[serde(rename = "ChatType")]
|
|
||||||
pub enum JsonrpcChatType {
|
|
||||||
Single,
|
|
||||||
Group,
|
|
||||||
Mailinglist,
|
|
||||||
OutBroadcast,
|
|
||||||
InBroadcast,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Chattype> for JsonrpcChatType {
|
|
||||||
fn from(chattype: Chattype) -> Self {
|
|
||||||
match chattype {
|
|
||||||
Chattype::Single => JsonrpcChatType::Single,
|
|
||||||
Chattype::Group => JsonrpcChatType::Group,
|
|
||||||
Chattype::Mailinglist => JsonrpcChatType::Mailinglist,
|
|
||||||
Chattype::OutBroadcast => JsonrpcChatType::OutBroadcast,
|
|
||||||
Chattype::InBroadcast => JsonrpcChatType::InBroadcast,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<JsonrpcChatType> for Chattype {
|
|
||||||
fn from(chattype: JsonrpcChatType) -> Self {
|
|
||||||
match chattype {
|
|
||||||
JsonrpcChatType::Single => Chattype::Single,
|
|
||||||
JsonrpcChatType::Group => Chattype::Group,
|
|
||||||
JsonrpcChatType::Mailinglist => Chattype::Mailinglist,
|
|
||||||
JsonrpcChatType::OutBroadcast => Chattype::OutBroadcast,
|
|
||||||
JsonrpcChatType::InBroadcast => Chattype::InBroadcast,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use anyhow::{Context, Result};
|
|||||||
use deltachat::chat::{Chat, ChatId};
|
use deltachat::chat::{Chat, ChatId};
|
||||||
use deltachat::chatlist::get_last_message_for_chat;
|
use deltachat::chatlist::get_last_message_for_chat;
|
||||||
use deltachat::constants::*;
|
use deltachat::constants::*;
|
||||||
use deltachat::contact::Contact;
|
use deltachat::contact::{Contact, ContactId};
|
||||||
use deltachat::{
|
use deltachat::{
|
||||||
chat::{get_chat_contacts, ChatVisibility},
|
chat::{get_chat_contacts, ChatVisibility},
|
||||||
chatlist::Chatlist,
|
chatlist::Chatlist,
|
||||||
@@ -11,7 +11,6 @@ use num_traits::cast::ToPrimitive;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
use super::chat::JsonrpcChatType;
|
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
use super::message::MessageViewtype;
|
use super::message::MessageViewtype;
|
||||||
|
|
||||||
@@ -24,38 +23,13 @@ pub enum ChatListItemFetchResult {
|
|||||||
name: String,
|
name: String,
|
||||||
avatar_path: Option<String>,
|
avatar_path: Option<String>,
|
||||||
color: String,
|
color: String,
|
||||||
chat_type: JsonrpcChatType,
|
|
||||||
last_updated: Option<i64>,
|
last_updated: Option<i64>,
|
||||||
summary_text1: String,
|
summary_text1: String,
|
||||||
summary_text2: String,
|
summary_text2: String,
|
||||||
summary_status: u32,
|
summary_status: u32,
|
||||||
/// showing preview if last chat message is image
|
/// showing preview if last chat message is image
|
||||||
summary_preview_image: Option<String>,
|
summary_preview_image: Option<String>,
|
||||||
|
is_protected: bool,
|
||||||
/// True if the chat is encrypted.
|
|
||||||
/// This means that all messages in the chat are encrypted,
|
|
||||||
/// and all contacts in the chat are "key-contacts",
|
|
||||||
/// i.e. identified by the PGP key fingerprint.
|
|
||||||
///
|
|
||||||
/// False if the chat is unencrypted.
|
|
||||||
/// This means that all messages in the chat are unencrypted,
|
|
||||||
/// and all contacts in the chat are "address-contacts",
|
|
||||||
/// i.e. identified by the email address.
|
|
||||||
/// The UI should mark this chat e.g. with a mail-letter icon.
|
|
||||||
///
|
|
||||||
/// Unencrypted groups are called "ad-hoc groups"
|
|
||||||
/// and the user can't add/remove members,
|
|
||||||
/// create a QR invite code,
|
|
||||||
/// or set an avatar.
|
|
||||||
/// These options should therefore be disabled in the UI.
|
|
||||||
///
|
|
||||||
/// Note that it can happen that an encrypted chat
|
|
||||||
/// contains unencrypted messages that were received in core <= v1.159.*
|
|
||||||
/// and vice versa.
|
|
||||||
///
|
|
||||||
/// See also `is_key_contact` on `Contact`.
|
|
||||||
is_encrypted: bool,
|
|
||||||
/// deprecated 2025-07, use chat_type instead
|
|
||||||
is_group: bool,
|
is_group: bool,
|
||||||
fresh_message_counter: usize,
|
fresh_message_counter: usize,
|
||||||
is_self_talk: bool,
|
is_self_talk: bool,
|
||||||
@@ -66,6 +40,8 @@ pub enum ChatListItemFetchResult {
|
|||||||
is_pinned: bool,
|
is_pinned: bool,
|
||||||
is_muted: bool,
|
is_muted: bool,
|
||||||
is_contact_request: bool,
|
is_contact_request: bool,
|
||||||
|
/// true when chat is a broadcastlist
|
||||||
|
is_broadcast: bool,
|
||||||
/// contact id if this is a dm chat (for view profile entry in context menu)
|
/// contact id if this is a dm chat (for view profile entry in context menu)
|
||||||
dm_chat_contact: Option<u32>,
|
dm_chat_contact: Option<u32>,
|
||||||
was_seen_recently: bool,
|
was_seen_recently: bool,
|
||||||
@@ -112,23 +88,20 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
|
|
||||||
let (last_updated, message_type) = match last_msgid {
|
let (last_updated, message_type) = match last_msgid {
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
if let Some(last_message) =
|
let last_message = deltachat::message::Message::load_from_db(ctx, id).await?;
|
||||||
deltachat::message::Message::load_from_db_optional(ctx, id).await?
|
(
|
||||||
{
|
Some(last_message.get_timestamp() * 1000),
|
||||||
(
|
Some(last_message.get_viewtype().into()),
|
||||||
Some(last_message.get_timestamp() * 1000),
|
)
|
||||||
Some(last_message.get_viewtype().into()),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
// Message may be deleted by the time we try to load it.
|
|
||||||
(None, None)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
None => (None, None),
|
None => (None, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
||||||
|
|
||||||
|
let self_in_group = chat_contacts.contains(&ContactId::SELF);
|
||||||
|
|
||||||
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
|
let (dm_chat_contact, was_seen_recently) = if chat.get_type() == Chattype::Single {
|
||||||
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
|
||||||
let contact = chat_contacts.first();
|
let contact = chat_contacts.first();
|
||||||
let was_seen_recently = match contact {
|
let was_seen_recently = match contact {
|
||||||
Some(contact) => Contact::get_by_id(ctx, *contact)
|
Some(contact) => Contact::get_by_id(ctx, *contact)
|
||||||
@@ -152,23 +125,23 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
name: chat.get_name().to_owned(),
|
name: chat.get_name().to_owned(),
|
||||||
avatar_path,
|
avatar_path,
|
||||||
color,
|
color,
|
||||||
chat_type: chat.get_type().into(),
|
|
||||||
last_updated,
|
last_updated,
|
||||||
summary_text1,
|
summary_text1,
|
||||||
summary_text2,
|
summary_text2,
|
||||||
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
|
summary_status: summary.state.to_u32().expect("impossible"), // idea and a function to transform the constant to strings? or return string enum
|
||||||
summary_preview_image,
|
summary_preview_image,
|
||||||
is_encrypted: chat.is_encrypted(ctx).await?,
|
is_protected: chat.is_protected(),
|
||||||
is_group: chat.get_type() == Chattype::Group,
|
is_group: chat.get_type() == Chattype::Group,
|
||||||
fresh_message_counter,
|
fresh_message_counter,
|
||||||
is_self_talk: chat.is_self_talk(),
|
is_self_talk: chat.is_self_talk(),
|
||||||
is_device_talk: chat.is_device_talk(),
|
is_device_talk: chat.is_device_talk(),
|
||||||
is_self_in_group: chat.is_self_in_chat(ctx).await?,
|
is_self_in_group: self_in_group,
|
||||||
is_sending_location: chat.is_sending_locations(),
|
is_sending_location: chat.is_sending_locations(),
|
||||||
is_archived: visibility == ChatVisibility::Archived,
|
is_archived: visibility == ChatVisibility::Archived,
|
||||||
is_pinned: visibility == ChatVisibility::Pinned,
|
is_pinned: visibility == ChatVisibility::Pinned,
|
||||||
is_muted: chat.is_muted(),
|
is_muted: chat.is_muted(),
|
||||||
is_contact_request: chat.is_contact_request(),
|
is_contact_request: chat.is_contact_request(),
|
||||||
|
is_broadcast: chat.get_type() == Chattype::Broadcast,
|
||||||
dm_chat_contact,
|
dm_chat_contact,
|
||||||
was_seen_recently,
|
was_seen_recently,
|
||||||
last_message_type: message_type,
|
last_message_type: message_type,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::key::{DcKey, SignedPublicKey};
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
@@ -20,47 +19,27 @@ pub struct ContactObject {
|
|||||||
name_and_addr: String,
|
name_and_addr: String,
|
||||||
is_blocked: bool,
|
is_blocked: bool,
|
||||||
|
|
||||||
/// Is the contact a key contact.
|
/// True if the contact can be added to verified groups.
|
||||||
is_key_contact: bool,
|
|
||||||
|
|
||||||
/// Is encryption available for this contact.
|
|
||||||
///
|
///
|
||||||
/// This can only be true for key-contacts.
|
/// If this is true
|
||||||
/// However, it is possible to have a key-contact
|
/// UI should display green checkmark after the contact name
|
||||||
/// for which encryption is not available because we don't have a key yet,
|
/// in contact list items,
|
||||||
/// e.g. if we just scanned the fingerprint from a QR code.
|
/// in chat member list items
|
||||||
e2ee_avail: bool,
|
/// and in profiles if no chat with the contact exist.
|
||||||
|
|
||||||
/// True if the contact
|
|
||||||
/// can be added to protected chats
|
|
||||||
/// because SELF and contact have verified their fingerprints in both directions.
|
|
||||||
///
|
|
||||||
/// See [`Self::verifier_id`]/`Contact.verifierId` for a guidance how to display these information.
|
|
||||||
is_verified: bool,
|
is_verified: bool,
|
||||||
|
|
||||||
/// The contact ID that verified a contact.
|
/// True if the contact profile title should have a green checkmark.
|
||||||
///
|
///
|
||||||
/// As verifier may be unknown,
|
/// This indicates whether 1:1 chat has a green checkmark
|
||||||
/// use [`Self::is_verified`]/`Contact.isVerified` to check if a contact can be added to a protected chat.
|
/// or will have a green checkmark if created.
|
||||||
|
is_profile_verified: bool,
|
||||||
|
|
||||||
|
/// The ID of the contact that verified this contact.
|
||||||
///
|
///
|
||||||
/// UI should display the information in the contact's profile as follows:
|
/// If this is present,
|
||||||
///
|
/// display a green checkmark and "Introduced by ..."
|
||||||
/// - If `verifierId` != 0,
|
/// string followed by the verifier contact name and address
|
||||||
/// display text "Introduced by ..."
|
/// in the contact profile.
|
||||||
/// with the name and address of the contact
|
|
||||||
/// formatted by `name_and_addr`/`nameAndAddr`.
|
|
||||||
/// Prefix the text by a green checkmark.
|
|
||||||
///
|
|
||||||
/// - If `verifierId` == 0 and `isVerified` != 0,
|
|
||||||
/// display "Introduced" prefixed by a green checkmark.
|
|
||||||
///
|
|
||||||
/// - if `verifierId` == 0 and `isVerified` == 0,
|
|
||||||
/// display nothing
|
|
||||||
///
|
|
||||||
/// This contains the contact ID of the verifier.
|
|
||||||
/// If it is `DC_CONTACT_ID_SELF`, we verified the contact ourself.
|
|
||||||
/// If it is None/Null, we don't have verifier information or
|
|
||||||
/// the contact is not verified.
|
|
||||||
verifier_id: Option<u32>,
|
verifier_id: Option<u32>,
|
||||||
|
|
||||||
/// the contact's last seen timestamp
|
/// the contact's last seen timestamp
|
||||||
@@ -81,11 +60,11 @@ impl ContactObject {
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
let is_verified = contact.is_verified(context).await?;
|
let is_verified = contact.is_verified(context).await?;
|
||||||
|
let is_profile_verified = contact.is_profile_verified(context).await?;
|
||||||
|
|
||||||
let verifier_id = contact
|
let verifier_id = contact
|
||||||
.get_verifier_id(context)
|
.get_verifier_id(context)
|
||||||
.await?
|
.await?
|
||||||
.flatten()
|
|
||||||
.map(|contact_id| contact_id.to_u32());
|
.map(|contact_id| contact_id.to_u32());
|
||||||
|
|
||||||
Ok(ContactObject {
|
Ok(ContactObject {
|
||||||
@@ -99,9 +78,8 @@ impl ContactObject {
|
|||||||
profile_image, //BLOBS
|
profile_image, //BLOBS
|
||||||
name_and_addr: contact.get_name_n_addr(),
|
name_and_addr: contact.get_name_n_addr(),
|
||||||
is_blocked: contact.is_blocked(),
|
is_blocked: contact.is_blocked(),
|
||||||
is_key_contact: contact.is_key_contact(),
|
|
||||||
e2ee_avail: contact.e2ee_avail(context).await?,
|
|
||||||
is_verified,
|
is_verified,
|
||||||
|
is_profile_verified,
|
||||||
verifier_id,
|
verifier_id,
|
||||||
last_seen: contact.last_seen(),
|
last_seen: contact.last_seen(),
|
||||||
was_seen_recently: contact.was_seen_recently(),
|
was_seen_recently: contact.was_seen_recently(),
|
||||||
@@ -109,41 +87,3 @@ impl ContactObject {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, TypeDef, schemars::JsonSchema)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct VcardContact {
|
|
||||||
/// Email address.
|
|
||||||
addr: String,
|
|
||||||
/// The contact's name, or the email address if no name was given.
|
|
||||||
display_name: String,
|
|
||||||
/// Public PGP key in Base64.
|
|
||||||
key: Option<String>,
|
|
||||||
/// Profile image in Base64.
|
|
||||||
profile_image: Option<String>,
|
|
||||||
/// Contact color as hex string.
|
|
||||||
color: String,
|
|
||||||
/// Last update timestamp.
|
|
||||||
timestamp: Option<i64>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<deltachat_contact_tools::VcardContact> for VcardContact {
|
|
||||||
fn from(vc: deltachat_contact_tools::VcardContact) -> Self {
|
|
||||||
let display_name = vc.display_name().to_string();
|
|
||||||
let is_self = false;
|
|
||||||
let fpr = vc.key.as_deref().and_then(|k| {
|
|
||||||
SignedPublicKey::from_base64(k)
|
|
||||||
.ok()
|
|
||||||
.map(|k| k.dc_fingerprint())
|
|
||||||
});
|
|
||||||
let color = deltachat::contact::get_color(is_self, &vc.addr, &fpr);
|
|
||||||
Self {
|
|
||||||
addr: vc.addr,
|
|
||||||
display_name,
|
|
||||||
key: vc.key,
|
|
||||||
profile_image: vc.profile_image,
|
|
||||||
color: color_int_to_hex_string(color),
|
|
||||||
timestamp: vc.timestamp.ok(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,8 +2,6 @@ use deltachat::{Event as CoreEvent, EventType as CoreEventType};
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
use super::chat::JsonrpcChatType;
|
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct Event {
|
pub struct Event {
|
||||||
@@ -71,7 +69,7 @@ pub enum EventType {
|
|||||||
/// or for functions that are expected to fail (eg. autocryptContinueKeyTransfer())
|
/// or for functions that are expected to fail (eg. autocryptContinueKeyTransfer())
|
||||||
/// it might be better to delay showing these events until the function has really
|
/// it might be better to delay showing these events until the function has really
|
||||||
/// failed (returned false). It should be sufficient to report only the *last* error
|
/// failed (returned false). It should be sufficient to report only the *last* error
|
||||||
/// in a message box then.
|
/// in a messasge box then.
|
||||||
Error { msg: String },
|
Error { msg: String },
|
||||||
|
|
||||||
/// An action cannot be performed because the user is not in the group.
|
/// An action cannot be performed because the user is not in the group.
|
||||||
@@ -86,78 +84,26 @@ pub enum EventType {
|
|||||||
/// - Messages sent, received or removed
|
/// - Messages sent, received or removed
|
||||||
/// - Chats created, deleted or archived
|
/// - Chats created, deleted or archived
|
||||||
/// - A draft has been set
|
/// - A draft has been set
|
||||||
|
///
|
||||||
|
/// `chatId` is set if only a single chat is affected by the changes, otherwise 0.
|
||||||
|
/// `msgId` is set if only a single message is affected by the changes, otherwise 0.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgsChanged {
|
MsgsChanged { chat_id: u32, msg_id: u32 },
|
||||||
/// Set if only a single chat is affected by the changes, otherwise 0.
|
|
||||||
chat_id: u32,
|
|
||||||
|
|
||||||
/// Set if only a single message is affected by the changes, otherwise 0.
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Reactions for the message changed.
|
/// Reactions for the message changed.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ReactionsChanged {
|
ReactionsChanged {
|
||||||
/// ID of the chat which the message belongs to.
|
|
||||||
chat_id: u32,
|
chat_id: u32,
|
||||||
|
|
||||||
/// ID of the message for which reactions were changed.
|
|
||||||
msg_id: u32,
|
msg_id: u32,
|
||||||
|
|
||||||
/// ID of the contact whose reaction set is changed.
|
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// A reaction to one's own sent message received.
|
/// There is a fresh message. Typically, the user will show an notification
|
||||||
/// Typically, the UI will show a notification for that.
|
|
||||||
///
|
|
||||||
/// In addition to this event, ReactionsChanged is emitted.
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
IncomingReaction {
|
|
||||||
/// ID of the chat which the message belongs to.
|
|
||||||
chat_id: u32,
|
|
||||||
|
|
||||||
/// ID of the contact whose reaction set is changed.
|
|
||||||
contact_id: u32,
|
|
||||||
|
|
||||||
/// ID of the message for which reactions were changed.
|
|
||||||
msg_id: u32,
|
|
||||||
|
|
||||||
/// The reaction.
|
|
||||||
reaction: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Incoming webxdc info or summary update, should be notified.
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
IncomingWebxdcNotify {
|
|
||||||
/// ID of the chat.
|
|
||||||
chat_id: u32,
|
|
||||||
|
|
||||||
/// ID of the contact sending.
|
|
||||||
contact_id: u32,
|
|
||||||
|
|
||||||
/// ID of the added info message or webxdc instance in case of summary change.
|
|
||||||
msg_id: u32,
|
|
||||||
|
|
||||||
/// Text to notify.
|
|
||||||
text: String,
|
|
||||||
|
|
||||||
/// Link assigned to this notification, if any.
|
|
||||||
href: Option<String>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// There is a fresh message. Typically, the user will show a notification
|
|
||||||
/// when receiving this message.
|
/// when receiving this message.
|
||||||
///
|
///
|
||||||
/// There is no extra #DC_EVENT_MSGS_CHANGED event sent together with this event.
|
/// There is no extra #DC_EVENT_MSGS_CHANGED event sent together with this event.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
IncomingMsg {
|
IncomingMsg { chat_id: u32, msg_id: u32 },
|
||||||
/// ID of the chat where the message is assigned.
|
|
||||||
chat_id: u32,
|
|
||||||
|
|
||||||
/// ID of the message.
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Downloading a bunch of messages just finished. This is an
|
/// Downloading a bunch of messages just finished. This is an
|
||||||
/// event to allow the UI to only show one notification per message bunch,
|
/// event to allow the UI to only show one notification per message bunch,
|
||||||
@@ -173,59 +119,24 @@ pub enum EventType {
|
|||||||
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
/// A single message is sent successfully. State changed from DC_STATE_OUT_PENDING to
|
||||||
/// DC_STATE_OUT_DELIVERED, see `Message.state`.
|
/// DC_STATE_OUT_DELIVERED, see `Message.state`.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgDelivered {
|
MsgDelivered { chat_id: u32, msg_id: u32 },
|
||||||
/// ID of the chat which the message belongs to.
|
|
||||||
chat_id: u32,
|
|
||||||
|
|
||||||
/// ID of the message that was successfully sent.
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
|
/// A single message could not be sent. State changed from DC_STATE_OUT_PENDING or DC_STATE_OUT_DELIVERED to
|
||||||
/// DC_STATE_OUT_FAILED, see `Message.state`.
|
/// DC_STATE_OUT_FAILED, see `Message.state`.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgFailed {
|
MsgFailed { chat_id: u32, msg_id: u32 },
|
||||||
/// ID of the chat which the message belongs to.
|
|
||||||
chat_id: u32,
|
|
||||||
|
|
||||||
/// ID of the message that could not be sent.
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
/// A single message is read by the receiver. State changed from DC_STATE_OUT_DELIVERED to
|
||||||
/// DC_STATE_OUT_MDN_RCVD, see `Message.state`.
|
/// DC_STATE_OUT_MDN_RCVD, see `Message.state`.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgRead {
|
MsgRead { chat_id: u32, msg_id: u32 },
|
||||||
/// ID of the chat which the message belongs to.
|
|
||||||
chat_id: u32,
|
|
||||||
|
|
||||||
/// ID of the message that was read.
|
/// A single message is deleted.
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A single message was deleted.
|
|
||||||
///
|
|
||||||
/// This event means that the message will no longer appear in the messagelist.
|
|
||||||
/// UI should remove the message from the messagelist
|
|
||||||
/// in response to this event if the message is currently displayed.
|
|
||||||
///
|
|
||||||
/// The message may have been explicitly deleted by the user or expired.
|
|
||||||
/// Internally the message may have been removed from the database,
|
|
||||||
/// moved to the trash chat or hidden.
|
|
||||||
///
|
|
||||||
/// This event does not indicate the message
|
|
||||||
/// deletion from the server.
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
MsgDeleted {
|
MsgDeleted { chat_id: u32, msg_id: u32 },
|
||||||
/// ID of the chat where the message was prior to deletion.
|
|
||||||
/// Never 0.
|
|
||||||
chat_id: u32,
|
|
||||||
|
|
||||||
/// ID of the deleted message. Never 0.
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
||||||
|
/// Or the verify state of a chat has changed.
|
||||||
/// See setChatName(), setChatProfileImage(), addContactToChat()
|
/// See setChatName(), setChatProfileImage(), addContactToChat()
|
||||||
/// and removeContactFromChat().
|
/// and removeContactFromChat().
|
||||||
///
|
///
|
||||||
@@ -236,35 +147,21 @@ pub enum EventType {
|
|||||||
|
|
||||||
/// Chat ephemeral timer changed.
|
/// Chat ephemeral timer changed.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ChatEphemeralTimerModified {
|
ChatEphemeralTimerModified { chat_id: u32, timer: u32 },
|
||||||
/// Chat ID.
|
|
||||||
chat_id: u32,
|
|
||||||
|
|
||||||
/// New ephemeral timer value.
|
|
||||||
timer: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Chat deleted.
|
|
||||||
ChatDeleted {
|
|
||||||
/// Chat ID.
|
|
||||||
chat_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Contact(s) created, renamed, blocked or deleted.
|
/// Contact(s) created, renamed, blocked or deleted.
|
||||||
|
///
|
||||||
|
/// @param data1 (int) If set, this is the contact_id of an added contact that should be selected.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ContactsChanged {
|
ContactsChanged { contact_id: Option<u32> },
|
||||||
/// If set, this is the contact_id of an added contact that should be selected.
|
|
||||||
contact_id: Option<u32>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Location of one or more contact has changed.
|
/// Location of one or more contact has changed.
|
||||||
|
///
|
||||||
|
/// @param data1 (u32) contact_id of the contact for which the location has changed.
|
||||||
|
/// If the locations of several contacts have been changed,
|
||||||
|
/// this parameter is set to `None`.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
LocationChanged {
|
LocationChanged { contact_id: Option<u32> },
|
||||||
/// contact_id of the contact for which the location has changed.
|
|
||||||
/// If the locations of several contacts have been changed,
|
|
||||||
/// this parameter is set to `None`.
|
|
||||||
contact_id: Option<u32>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Inform about the configuration progress started by configure().
|
/// Inform about the configuration progress started by configure().
|
||||||
ConfigureProgress {
|
ConfigureProgress {
|
||||||
@@ -279,11 +176,10 @@ pub enum EventType {
|
|||||||
|
|
||||||
/// Inform about the import/export progress started by imex().
|
/// Inform about the import/export progress started by imex().
|
||||||
///
|
///
|
||||||
|
/// @param data1 (usize) 0=error, 1-999=progress in permille, 1000=success and done
|
||||||
|
/// @param data2 0
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ImexProgress {
|
ImexProgress { progress: usize },
|
||||||
/// 0=error, 1-999=progress in permille, 1000=success and done
|
|
||||||
progress: usize,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// A file has been exported. A file has been written by imex().
|
/// A file has been exported. A file has been written by imex().
|
||||||
/// This event may be sent multiple times by a single call to imex().
|
/// This event may be sent multiple times by a single call to imex().
|
||||||
@@ -295,42 +191,31 @@ pub enum EventType {
|
|||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ImexFileWritten { path: String },
|
ImexFileWritten { path: String },
|
||||||
|
|
||||||
/// Progress event sent when SecureJoin protocol has finished
|
/// Progress information of a secure-join handshake from the view of the inviter
|
||||||
/// from the view of the inviter (Alice, the person who shows the QR code).
|
/// (Alice, the person who shows the QR code).
|
||||||
///
|
///
|
||||||
/// These events are typically sent after a joiner has scanned the QR code
|
/// These events are typically sent after a joiner has scanned the QR code
|
||||||
/// generated by getChatSecurejoinQrCodeSvg().
|
/// generated by getChatSecurejoinQrCodeSvg().
|
||||||
|
///
|
||||||
|
/// @param data1 (int) ID of the contact that wants to join.
|
||||||
|
/// @param data2 (int) Progress as:
|
||||||
|
/// 300=vg-/vc-request received, typically shown as "bob@addr joins".
|
||||||
|
/// 600=vg-/vc-request-with-auth received, vg-member-added/vc-contact-confirm sent, typically shown as "bob@addr verified".
|
||||||
|
/// 800=vg-member-added-received received, shown as "bob@addr securely joined GROUP", only sent for the verified-group-protocol.
|
||||||
|
/// 1000=Protocol finished for this contact.
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
SecurejoinInviterProgress {
|
SecurejoinInviterProgress { contact_id: u32, progress: usize },
|
||||||
/// ID of the contact that wants to join.
|
|
||||||
contact_id: u32,
|
|
||||||
|
|
||||||
/// The type of the joined chat.
|
|
||||||
/// This can take the same values
|
|
||||||
/// as `BasicChat.chatType` ([`crate::api::types::chat::BasicChat::chat_type`]).
|
|
||||||
chat_type: JsonrpcChatType,
|
|
||||||
/// ID of the chat in case of success.
|
|
||||||
chat_id: u32,
|
|
||||||
|
|
||||||
/// Progress, always 1000.
|
|
||||||
progress: usize,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Progress information of a secure-join handshake from the view of the joiner
|
/// Progress information of a secure-join handshake from the view of the joiner
|
||||||
/// (Bob, the person who scans the QR code).
|
/// (Bob, the person who scans the QR code).
|
||||||
/// The events are typically sent while secureJoin(), which
|
/// The events are typically sent while secureJoin(), which
|
||||||
/// may take some time, is executed.
|
/// may take some time, is executed.
|
||||||
|
/// @param data1 (int) ID of the inviting contact.
|
||||||
|
/// @param data2 (int) Progress as:
|
||||||
|
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
||||||
|
/// (Bob has verified alice and waits until Alice does the same for him)
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
SecurejoinJoinerProgress {
|
SecurejoinJoinerProgress { contact_id: u32, progress: usize },
|
||||||
/// ID of the inviting contact.
|
|
||||||
contact_id: u32,
|
|
||||||
|
|
||||||
/// Progress as:
|
|
||||||
/// 400=vg-/vc-request-with-auth sent, typically shown as "alice@addr verified, introducing myself."
|
|
||||||
/// (Bob has verified alice and waits until Alice does the same for him)
|
|
||||||
/// 1000=vg-member-added/vc-contact-confirm received
|
|
||||||
progress: usize,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// The connectivity to the server changed.
|
/// The connectivity to the server changed.
|
||||||
/// This means that you should refresh the connectivity view
|
/// This means that you should refresh the connectivity view
|
||||||
@@ -351,37 +236,13 @@ pub enum EventType {
|
|||||||
|
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
WebxdcStatusUpdate {
|
WebxdcStatusUpdate {
|
||||||
/// Message ID.
|
|
||||||
msg_id: u32,
|
msg_id: u32,
|
||||||
|
|
||||||
/// Status update ID.
|
|
||||||
status_update_serial: u32,
|
status_update_serial: u32,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Data received over an ephemeral peer channel.
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
WebxdcRealtimeData {
|
|
||||||
/// Message ID.
|
|
||||||
msg_id: u32,
|
|
||||||
|
|
||||||
/// Realtime data.
|
|
||||||
data: Vec<u8>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Advertisement received over an ephemeral peer channel.
|
|
||||||
/// This can be used by bots to initiate peer-to-peer communication from their side.
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
WebxdcRealtimeAdvertisementReceived {
|
|
||||||
/// Message ID of the webxdc instance.
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Inform that a message containing a webxdc instance has been deleted
|
/// Inform that a message containing a webxdc instance has been deleted
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
WebxdcInstanceDeleted {
|
WebxdcInstanceDeleted { msg_id: u32 },
|
||||||
/// ID of the deleted message.
|
|
||||||
msg_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Tells that the Background fetch was completed (or timed out).
|
/// Tells that the Background fetch was completed (or timed out).
|
||||||
/// This event acts as a marker, when you reach this event you can be sure
|
/// This event acts as a marker, when you reach this event you can be sure
|
||||||
@@ -389,86 +250,6 @@ pub enum EventType {
|
|||||||
///
|
///
|
||||||
/// This event is only emitted by the account manager
|
/// This event is only emitted by the account manager
|
||||||
AccountsBackgroundFetchDone,
|
AccountsBackgroundFetchDone,
|
||||||
/// Inform that set of chats or the order of the chats in the chatlist has changed.
|
|
||||||
///
|
|
||||||
/// Sometimes this is emitted together with `UIChatlistItemChanged`.
|
|
||||||
ChatlistChanged,
|
|
||||||
|
|
||||||
/// Inform that a single chat list item changed and needs to be rerendered.
|
|
||||||
/// If `chat_id` is set to None, then all currently visible chats need to be rerendered, and all not-visible items need to be cleared from cache if the UI has a cache.
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
ChatlistItemChanged {
|
|
||||||
/// ID of the changed chat
|
|
||||||
chat_id: Option<u32>,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Inform that the list of accounts has changed (an account removed or added or (not yet implemented) the account order changes)
|
|
||||||
///
|
|
||||||
/// This event is only emitted by the account manager
|
|
||||||
AccountsChanged,
|
|
||||||
|
|
||||||
/// Inform that an account property that might be shown in the account list changed, namely:
|
|
||||||
/// - is_configured (see is_configured())
|
|
||||||
/// - displayname
|
|
||||||
/// - selfavatar
|
|
||||||
/// - private_tag
|
|
||||||
///
|
|
||||||
/// This event is emitted from the account whose property changed.
|
|
||||||
AccountsItemChanged,
|
|
||||||
|
|
||||||
/// Inform than some events have been skipped due to event channel overflow.
|
|
||||||
EventChannelOverflow {
|
|
||||||
/// Number of events skipped.
|
|
||||||
n: u64,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Incoming call.
|
|
||||||
IncomingCall {
|
|
||||||
/// ID of the info message referring to the call.
|
|
||||||
msg_id: u32,
|
|
||||||
/// ID of the chat which the message belongs to.
|
|
||||||
chat_id: u32,
|
|
||||||
/// User-defined info as passed to place_outgoing_call()
|
|
||||||
place_call_info: String,
|
|
||||||
/// True if incoming call is a video call.
|
|
||||||
has_video: bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Incoming call accepted.
|
|
||||||
/// This is esp. interesting to stop ringing on other devices.
|
|
||||||
IncomingCallAccepted {
|
|
||||||
/// ID of the info message referring to the call.
|
|
||||||
msg_id: u32,
|
|
||||||
/// ID of the chat which the message belongs to.
|
|
||||||
chat_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Outgoing call accepted.
|
|
||||||
OutgoingCallAccepted {
|
|
||||||
/// ID of the info message referring to the call.
|
|
||||||
msg_id: u32,
|
|
||||||
/// ID of the chat which the message belongs to.
|
|
||||||
chat_id: u32,
|
|
||||||
/// User-defined info passed to dc_accept_incoming_call(
|
|
||||||
accept_call_info: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// Call ended.
|
|
||||||
CallEnded {
|
|
||||||
/// ID of the info message referring to the call.
|
|
||||||
msg_id: u32,
|
|
||||||
/// ID of the chat which the message belongs to.
|
|
||||||
chat_id: u32,
|
|
||||||
},
|
|
||||||
|
|
||||||
/// One or more transports has changed.
|
|
||||||
///
|
|
||||||
/// This event is used for tests to detect when transport
|
|
||||||
/// synchronization messages arrives.
|
|
||||||
/// UIs don't need to use it, it is unlikely
|
|
||||||
/// that user modifies transports on multiple
|
|
||||||
/// devices simultaneously.
|
|
||||||
TransportsModified,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<CoreEventType> for EventType {
|
impl From<CoreEventType> for EventType {
|
||||||
@@ -500,30 +281,6 @@ impl From<CoreEventType> for EventType {
|
|||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
contact_id: contact_id.to_u32(),
|
contact_id: contact_id.to_u32(),
|
||||||
},
|
},
|
||||||
CoreEventType::IncomingReaction {
|
|
||||||
chat_id,
|
|
||||||
contact_id,
|
|
||||||
msg_id,
|
|
||||||
reaction,
|
|
||||||
} => IncomingReaction {
|
|
||||||
chat_id: chat_id.to_u32(),
|
|
||||||
contact_id: contact_id.to_u32(),
|
|
||||||
msg_id: msg_id.to_u32(),
|
|
||||||
reaction: reaction.as_str().to_string(),
|
|
||||||
},
|
|
||||||
CoreEventType::IncomingWebxdcNotify {
|
|
||||||
chat_id,
|
|
||||||
contact_id,
|
|
||||||
msg_id,
|
|
||||||
text,
|
|
||||||
href,
|
|
||||||
} => IncomingWebxdcNotify {
|
|
||||||
chat_id: chat_id.to_u32(),
|
|
||||||
contact_id: contact_id.to_u32(),
|
|
||||||
msg_id: msg_id.to_u32(),
|
|
||||||
text,
|
|
||||||
href,
|
|
||||||
},
|
|
||||||
CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
|
CoreEventType::IncomingMsg { chat_id, msg_id } => IncomingMsg {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
@@ -557,9 +314,6 @@ impl From<CoreEventType> for EventType {
|
|||||||
timer: timer.to_u32(),
|
timer: timer.to_u32(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CoreEventType::ChatDeleted { chat_id } => ChatDeleted {
|
|
||||||
chat_id: chat_id.to_u32(),
|
|
||||||
},
|
|
||||||
CoreEventType::ContactsChanged(contact) => ContactsChanged {
|
CoreEventType::ContactsChanged(contact) => ContactsChanged {
|
||||||
contact_id: contact.map(|c| c.to_u32()),
|
contact_id: contact.map(|c| c.to_u32()),
|
||||||
},
|
},
|
||||||
@@ -575,13 +329,9 @@ impl From<CoreEventType> for EventType {
|
|||||||
},
|
},
|
||||||
CoreEventType::SecurejoinInviterProgress {
|
CoreEventType::SecurejoinInviterProgress {
|
||||||
contact_id,
|
contact_id,
|
||||||
chat_type,
|
|
||||||
chat_id,
|
|
||||||
progress,
|
progress,
|
||||||
} => SecurejoinInviterProgress {
|
} => SecurejoinInviterProgress {
|
||||||
contact_id: contact_id.to_u32(),
|
contact_id: contact_id.to_u32(),
|
||||||
chat_type: chat_type.into(),
|
|
||||||
chat_id: chat_id.to_u32(),
|
|
||||||
progress,
|
progress,
|
||||||
},
|
},
|
||||||
CoreEventType::SecurejoinJoinerProgress {
|
CoreEventType::SecurejoinJoinerProgress {
|
||||||
@@ -603,59 +353,10 @@ impl From<CoreEventType> for EventType {
|
|||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
status_update_serial: status_update_serial.to_u32(),
|
status_update_serial: status_update_serial.to_u32(),
|
||||||
},
|
},
|
||||||
CoreEventType::WebxdcRealtimeData { msg_id, data } => WebxdcRealtimeData {
|
|
||||||
msg_id: msg_id.to_u32(),
|
|
||||||
data,
|
|
||||||
},
|
|
||||||
CoreEventType::WebxdcRealtimeAdvertisementReceived { msg_id } => {
|
|
||||||
WebxdcRealtimeAdvertisementReceived {
|
|
||||||
msg_id: msg_id.to_u32(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
|
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
CoreEventType::AccountsBackgroundFetchDone => AccountsBackgroundFetchDone,
|
CoreEventType::AccountsBackgroundFetchDone => AccountsBackgroundFetchDone,
|
||||||
CoreEventType::ChatlistItemChanged { chat_id } => ChatlistItemChanged {
|
|
||||||
chat_id: chat_id.map(|id| id.to_u32()),
|
|
||||||
},
|
|
||||||
CoreEventType::ChatlistChanged => ChatlistChanged,
|
|
||||||
CoreEventType::EventChannelOverflow { n } => EventChannelOverflow { n },
|
|
||||||
CoreEventType::AccountsChanged => AccountsChanged,
|
|
||||||
CoreEventType::AccountsItemChanged => AccountsItemChanged,
|
|
||||||
CoreEventType::IncomingCall {
|
|
||||||
msg_id,
|
|
||||||
chat_id,
|
|
||||||
place_call_info,
|
|
||||||
has_video,
|
|
||||||
} => IncomingCall {
|
|
||||||
msg_id: msg_id.to_u32(),
|
|
||||||
chat_id: chat_id.to_u32(),
|
|
||||||
place_call_info,
|
|
||||||
has_video,
|
|
||||||
},
|
|
||||||
CoreEventType::IncomingCallAccepted { msg_id, chat_id } => IncomingCallAccepted {
|
|
||||||
msg_id: msg_id.to_u32(),
|
|
||||||
chat_id: chat_id.to_u32(),
|
|
||||||
},
|
|
||||||
CoreEventType::OutgoingCallAccepted {
|
|
||||||
msg_id,
|
|
||||||
chat_id,
|
|
||||||
accept_call_info,
|
|
||||||
} => OutgoingCallAccepted {
|
|
||||||
msg_id: msg_id.to_u32(),
|
|
||||||
chat_id: chat_id.to_u32(),
|
|
||||||
accept_call_info,
|
|
||||||
},
|
|
||||||
CoreEventType::CallEnded { msg_id, chat_id } => CallEnded {
|
|
||||||
msg_id: msg_id.to_u32(),
|
|
||||||
chat_id: chat_id.to_u32(),
|
|
||||||
},
|
|
||||||
CoreEventType::TransportsModified => TransportsModified,
|
|
||||||
|
|
||||||
#[allow(unreachable_patterns)]
|
|
||||||
#[cfg(test)]
|
|
||||||
_ => unreachable!("This is just to silence a rust_analyzer false-positive"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,203 +0,0 @@
|
|||||||
use anyhow::Result;
|
|
||||||
use deltachat::login_param as dc;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use serde::Serialize;
|
|
||||||
use yerpc::TypeDef;
|
|
||||||
|
|
||||||
/// Login parameters entered by the user.
|
|
||||||
///
|
|
||||||
/// Usually it will be enough to only set `addr` and `password`,
|
|
||||||
/// and all the other settings will be autoconfigured.
|
|
||||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub struct EnteredLoginParam {
|
|
||||||
/// Email address.
|
|
||||||
pub addr: String,
|
|
||||||
|
|
||||||
/// Password.
|
|
||||||
pub password: String,
|
|
||||||
|
|
||||||
/// Imap server hostname or IP address.
|
|
||||||
pub imap_server: Option<String>,
|
|
||||||
|
|
||||||
/// Imap server port.
|
|
||||||
pub imap_port: Option<u16>,
|
|
||||||
|
|
||||||
/// Imap socket security.
|
|
||||||
pub imap_security: Option<Socket>,
|
|
||||||
|
|
||||||
/// Imap username.
|
|
||||||
pub imap_user: Option<String>,
|
|
||||||
|
|
||||||
/// SMTP server hostname or IP address.
|
|
||||||
pub smtp_server: Option<String>,
|
|
||||||
|
|
||||||
/// SMTP server port.
|
|
||||||
pub smtp_port: Option<u16>,
|
|
||||||
|
|
||||||
/// SMTP socket security.
|
|
||||||
pub smtp_security: Option<Socket>,
|
|
||||||
|
|
||||||
/// SMTP username.
|
|
||||||
pub smtp_user: Option<String>,
|
|
||||||
|
|
||||||
/// SMTP Password.
|
|
||||||
///
|
|
||||||
/// Only needs to be specified if different than IMAP password.
|
|
||||||
pub smtp_password: Option<String>,
|
|
||||||
|
|
||||||
/// TLS options: whether to allow invalid certificates and/or
|
|
||||||
/// invalid hostnames.
|
|
||||||
/// Default: Automatic
|
|
||||||
pub certificate_checks: Option<EnteredCertificateChecks>,
|
|
||||||
|
|
||||||
/// If true, login via OAUTH2 (not recommended anymore).
|
|
||||||
/// Default: false
|
|
||||||
pub oauth2: Option<bool>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<dc::EnteredLoginParam> for EnteredLoginParam {
|
|
||||||
fn from(param: dc::EnteredLoginParam) -> Self {
|
|
||||||
let imap_security: Socket = param.imap.security.into();
|
|
||||||
let smtp_security: Socket = param.smtp.security.into();
|
|
||||||
let certificate_checks: EnteredCertificateChecks = param.certificate_checks.into();
|
|
||||||
Self {
|
|
||||||
addr: param.addr,
|
|
||||||
password: param.imap.password,
|
|
||||||
imap_server: param.imap.server.into_option(),
|
|
||||||
imap_port: param.imap.port.into_option(),
|
|
||||||
imap_security: imap_security.into_option(),
|
|
||||||
imap_user: param.imap.user.into_option(),
|
|
||||||
smtp_server: param.smtp.server.into_option(),
|
|
||||||
smtp_port: param.smtp.port.into_option(),
|
|
||||||
smtp_security: smtp_security.into_option(),
|
|
||||||
smtp_user: param.smtp.user.into_option(),
|
|
||||||
smtp_password: param.smtp.password.into_option(),
|
|
||||||
certificate_checks: certificate_checks.into_option(),
|
|
||||||
oauth2: param.oauth2.into_option(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<EnteredLoginParam> for dc::EnteredLoginParam {
|
|
||||||
type Error = anyhow::Error;
|
|
||||||
|
|
||||||
fn try_from(param: EnteredLoginParam) -> Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
addr: param.addr,
|
|
||||||
imap: dc::EnteredServerLoginParam {
|
|
||||||
server: param.imap_server.unwrap_or_default(),
|
|
||||||
port: param.imap_port.unwrap_or_default(),
|
|
||||||
security: param.imap_security.unwrap_or_default().into(),
|
|
||||||
user: param.imap_user.unwrap_or_default(),
|
|
||||||
password: param.password,
|
|
||||||
},
|
|
||||||
smtp: dc::EnteredServerLoginParam {
|
|
||||||
server: param.smtp_server.unwrap_or_default(),
|
|
||||||
port: param.smtp_port.unwrap_or_default(),
|
|
||||||
security: param.smtp_security.unwrap_or_default().into(),
|
|
||||||
user: param.smtp_user.unwrap_or_default(),
|
|
||||||
password: param.smtp_password.unwrap_or_default(),
|
|
||||||
},
|
|
||||||
certificate_checks: param.certificate_checks.unwrap_or_default().into(),
|
|
||||||
oauth2: param.oauth2.unwrap_or_default(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub enum Socket {
|
|
||||||
/// Unspecified socket security, select automatically.
|
|
||||||
#[default]
|
|
||||||
Automatic,
|
|
||||||
|
|
||||||
/// TLS connection.
|
|
||||||
Ssl,
|
|
||||||
|
|
||||||
/// STARTTLS connection.
|
|
||||||
Starttls,
|
|
||||||
|
|
||||||
/// No TLS, plaintext connection.
|
|
||||||
Plain,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<dc::Socket> for Socket {
|
|
||||||
fn from(value: dc::Socket) -> Self {
|
|
||||||
match value {
|
|
||||||
dc::Socket::Automatic => Self::Automatic,
|
|
||||||
dc::Socket::Ssl => Self::Ssl,
|
|
||||||
dc::Socket::Starttls => Self::Starttls,
|
|
||||||
dc::Socket::Plain => Self::Plain,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Socket> for dc::Socket {
|
|
||||||
fn from(value: Socket) -> Self {
|
|
||||||
match value {
|
|
||||||
Socket::Automatic => Self::Automatic,
|
|
||||||
Socket::Ssl => Self::Ssl,
|
|
||||||
Socket::Starttls => Self::Starttls,
|
|
||||||
Socket::Plain => Self::Plain,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema, Default, PartialEq)]
|
|
||||||
#[serde(rename_all = "camelCase")]
|
|
||||||
pub enum EnteredCertificateChecks {
|
|
||||||
/// `Automatic` means that provider database setting should be taken.
|
|
||||||
/// If there is no provider database setting for certificate checks,
|
|
||||||
/// check certificates strictly.
|
|
||||||
#[default]
|
|
||||||
Automatic,
|
|
||||||
|
|
||||||
/// Ensure that TLS certificate is valid for the server hostname.
|
|
||||||
Strict,
|
|
||||||
|
|
||||||
/// Accept certificates that are expired, self-signed
|
|
||||||
/// or otherwise not valid for the server hostname.
|
|
||||||
AcceptInvalidCertificates,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<dc::EnteredCertificateChecks> for EnteredCertificateChecks {
|
|
||||||
fn from(value: dc::EnteredCertificateChecks) -> Self {
|
|
||||||
match value {
|
|
||||||
dc::EnteredCertificateChecks::Automatic => Self::Automatic,
|
|
||||||
dc::EnteredCertificateChecks::Strict => Self::Strict,
|
|
||||||
dc::EnteredCertificateChecks::AcceptInvalidCertificates => {
|
|
||||||
Self::AcceptInvalidCertificates
|
|
||||||
}
|
|
||||||
dc::EnteredCertificateChecks::AcceptInvalidCertificates2 => {
|
|
||||||
Self::AcceptInvalidCertificates
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<EnteredCertificateChecks> for dc::EnteredCertificateChecks {
|
|
||||||
fn from(value: EnteredCertificateChecks) -> Self {
|
|
||||||
match value {
|
|
||||||
EnteredCertificateChecks::Automatic => Self::Automatic,
|
|
||||||
EnteredCertificateChecks::Strict => Self::Strict,
|
|
||||||
EnteredCertificateChecks::AcceptInvalidCertificates => Self::AcceptInvalidCertificates,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trait IntoOption<T> {
|
|
||||||
fn into_option(self) -> Option<T>;
|
|
||||||
}
|
|
||||||
impl<T> IntoOption<T> for T
|
|
||||||
where
|
|
||||||
T: Default + std::cmp::PartialEq,
|
|
||||||
{
|
|
||||||
fn into_option(self) -> Option<T> {
|
|
||||||
if self == T::default() {
|
|
||||||
None
|
|
||||||
} else {
|
|
||||||
Some(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,3 @@
|
|||||||
use std::path::Path;
|
|
||||||
|
|
||||||
use crate::api::VcardContact;
|
|
||||||
use anyhow::{Context as _, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use deltachat::chat::Chat;
|
use deltachat::chat::Chat;
|
||||||
use deltachat::chat::ChatItem;
|
use deltachat::chat::ChatItem;
|
||||||
@@ -16,14 +13,13 @@ use num_traits::cast::ToPrimitive;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
use super::chat::JsonrpcChatType;
|
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
use super::contact::ContactObject;
|
use super::contact::ContactObject;
|
||||||
use super::reactions::JsonrpcReactions;
|
use super::reactions::JSONRPCReactions;
|
||||||
|
use super::webxdc::WebxdcMessageInfo;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||||
#[expect(clippy::large_enum_variant)]
|
|
||||||
pub enum MessageLoadResult {
|
pub enum MessageLoadResult {
|
||||||
Message(MessageObject),
|
Message(MessageObject),
|
||||||
LoadingError { error: String },
|
LoadingError { error: String },
|
||||||
@@ -39,12 +35,6 @@ pub struct MessageObject {
|
|||||||
parent_id: Option<u32>,
|
parent_id: Option<u32>,
|
||||||
|
|
||||||
text: String,
|
text: String,
|
||||||
|
|
||||||
is_edited: bool,
|
|
||||||
|
|
||||||
/// Check if a message has a POI location bound to it.
|
|
||||||
/// These locations are also returned by `get_locations` method.
|
|
||||||
/// The UI may decide to display a special icon beside such messages.
|
|
||||||
has_location: bool,
|
has_location: bool,
|
||||||
has_html: bool,
|
has_html: bool,
|
||||||
view_type: MessageViewtype,
|
view_type: MessageViewtype,
|
||||||
@@ -60,13 +50,6 @@ pub struct MessageObject {
|
|||||||
|
|
||||||
// summary - use/create another function if you need it
|
// summary - use/create another function if you need it
|
||||||
subject: String,
|
subject: String,
|
||||||
|
|
||||||
/// True if the message was correctly encrypted&signed, false otherwise.
|
|
||||||
/// Historically, UIs showed a small padlock on the message then.
|
|
||||||
///
|
|
||||||
/// Today, the UIs should instead show a small email-icon on the message
|
|
||||||
/// if `show_padlock` is `false`,
|
|
||||||
/// and nothing if it is `true`.
|
|
||||||
show_padlock: bool,
|
show_padlock: bool,
|
||||||
is_setupmessage: bool,
|
is_setupmessage: bool,
|
||||||
is_info: bool,
|
is_info: bool,
|
||||||
@@ -78,13 +61,13 @@ pub struct MessageObject {
|
|||||||
/// when is_info is true this describes what type of system message it is
|
/// when is_info is true this describes what type of system message it is
|
||||||
system_message_type: SystemMessageType,
|
system_message_type: SystemMessageType,
|
||||||
|
|
||||||
/// if is_info is set, this refers to the contact profile that should be opened when the info message is tapped.
|
|
||||||
info_contact_id: Option<u32>,
|
|
||||||
|
|
||||||
duration: i32,
|
duration: i32,
|
||||||
dimensions_height: i32,
|
dimensions_height: i32,
|
||||||
dimensions_width: i32,
|
dimensions_width: i32,
|
||||||
|
|
||||||
|
videochat_type: Option<u32>,
|
||||||
|
videochat_url: Option<String>,
|
||||||
|
|
||||||
override_sender_name: Option<String>,
|
override_sender_name: Option<String>,
|
||||||
sender: ContactObject,
|
sender: ContactObject,
|
||||||
|
|
||||||
@@ -95,17 +78,11 @@ pub struct MessageObject {
|
|||||||
file_bytes: u64,
|
file_bytes: u64,
|
||||||
file_name: Option<String>,
|
file_name: Option<String>,
|
||||||
|
|
||||||
webxdc_href: Option<String>,
|
webxdc_info: Option<WebxdcMessageInfo>,
|
||||||
|
|
||||||
download_state: DownloadState,
|
download_state: DownloadState,
|
||||||
|
|
||||||
original_msg_id: Option<u32>,
|
reactions: Option<JSONRPCReactions>,
|
||||||
|
|
||||||
saved_message_id: Option<u32>,
|
|
||||||
|
|
||||||
reactions: Option<JsonrpcReactions>,
|
|
||||||
|
|
||||||
vcard_contact: Option<VcardContact>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
@@ -118,9 +95,6 @@ enum MessageQuote {
|
|||||||
WithMessage {
|
WithMessage {
|
||||||
text: String,
|
text: String,
|
||||||
message_id: u32,
|
message_id: u32,
|
||||||
/// The quoted message does not always belong
|
|
||||||
/// to the same chat, e.g. when "Reply Privately" is used.
|
|
||||||
chat_id: u32,
|
|
||||||
author_display_name: String,
|
author_display_name: String,
|
||||||
author_display_color: String,
|
author_display_color: String,
|
||||||
override_sender_name: Option<String>,
|
override_sender_name: Option<String>,
|
||||||
@@ -131,10 +105,8 @@ enum MessageQuote {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl MessageObject {
|
impl MessageObject {
|
||||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Option<Self>> {
|
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||||
let Some(message) = Message::load_from_db_optional(context, msg_id).await? else {
|
let message = Message::load_from_db(context, msg_id).await?;
|
||||||
return Ok(None);
|
|
||||||
};
|
|
||||||
|
|
||||||
let sender_contact = Contact::get_by_id(context, message.get_from_id())
|
let sender_contact = Contact::get_by_id(context, message.get_from_id())
|
||||||
.await
|
.await
|
||||||
@@ -145,6 +117,12 @@ impl MessageObject {
|
|||||||
let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
|
let file_bytes = message.get_filebytes(context).await?.unwrap_or_default();
|
||||||
let override_sender_name = message.get_override_sender_name();
|
let override_sender_name = message.get_override_sender_name();
|
||||||
|
|
||||||
|
let webxdc_info = if message.get_viewtype() == Viewtype::Webxdc {
|
||||||
|
Some(WebxdcMessageInfo::get_for_message(context, msg_id).await?)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
let parent_id = message.parent(context).await?.map(|m| m.get_id().to_u32());
|
let parent_id = message.parent(context).await?.map(|m| m.get_id().to_u32());
|
||||||
|
|
||||||
let download_state = message.download_state().into();
|
let download_state = message.download_state().into();
|
||||||
@@ -158,7 +136,6 @@ impl MessageObject {
|
|||||||
Some(MessageQuote::WithMessage {
|
Some(MessageQuote::WithMessage {
|
||||||
text: quoted_text,
|
text: quoted_text,
|
||||||
message_id: quote.get_id().to_u32(),
|
message_id: quote.get_id().to_u32(),
|
||||||
chat_id: quote.get_chat_id().to_u32(),
|
|
||||||
author_display_name: quote_author.get_display_name().to_owned(),
|
author_display_name: quote_author.get_display_name().to_owned(),
|
||||||
author_display_color: color_int_to_hex_string(quote_author.get_color()),
|
author_display_color: color_int_to_hex_string(quote_author.get_color()),
|
||||||
override_sender_name: quote.get_override_sender_name(),
|
override_sender_name: quote.get_override_sender_name(),
|
||||||
@@ -192,21 +169,13 @@ impl MessageObject {
|
|||||||
Some(reactions.into())
|
Some(reactions.into())
|
||||||
};
|
};
|
||||||
|
|
||||||
let vcard_contacts: Vec<VcardContact> = message
|
Ok(MessageObject {
|
||||||
.vcard_contacts(context)
|
|
||||||
.await?
|
|
||||||
.into_iter()
|
|
||||||
.map(Into::into)
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let message_object = MessageObject {
|
|
||||||
id: msg_id.to_u32(),
|
id: msg_id.to_u32(),
|
||||||
chat_id: message.get_chat_id().to_u32(),
|
chat_id: message.get_chat_id().to_u32(),
|
||||||
from_id: message.get_from_id().to_u32(),
|
from_id: message.get_from_id().to_u32(),
|
||||||
quote,
|
quote,
|
||||||
parent_id,
|
parent_id,
|
||||||
text: message.get_text(),
|
text: message.get_text(),
|
||||||
is_edited: message.is_edited(),
|
|
||||||
has_location: message.has_location(),
|
has_location: message.has_location(),
|
||||||
has_html: message.has_html(),
|
has_html: message.has_html(),
|
||||||
view_type: message.get_viewtype().into(),
|
view_type: message.get_viewtype().into(),
|
||||||
@@ -228,15 +197,20 @@ impl MessageObject {
|
|||||||
is_forwarded: message.is_forwarded(),
|
is_forwarded: message.is_forwarded(),
|
||||||
is_bot: message.is_bot(),
|
is_bot: message.is_bot(),
|
||||||
system_message_type: message.get_info_type().into(),
|
system_message_type: message.get_info_type().into(),
|
||||||
info_contact_id: message
|
|
||||||
.get_info_contact_id(context)
|
|
||||||
.await?
|
|
||||||
.map(|id| id.to_u32()),
|
|
||||||
|
|
||||||
duration: message.get_duration(),
|
duration: message.get_duration(),
|
||||||
dimensions_height: message.get_height(),
|
dimensions_height: message.get_height(),
|
||||||
dimensions_width: message.get_width(),
|
dimensions_width: message.get_width(),
|
||||||
|
|
||||||
|
videochat_type: match message.get_videochat_type() {
|
||||||
|
Some(vct) => Some(
|
||||||
|
vct.to_u32()
|
||||||
|
.context("videochat type conversion to number failed")?,
|
||||||
|
),
|
||||||
|
None => None,
|
||||||
|
},
|
||||||
|
videochat_url: message.get_videochat_url(),
|
||||||
|
|
||||||
override_sender_name,
|
override_sender_name,
|
||||||
sender,
|
sender,
|
||||||
|
|
||||||
@@ -249,28 +223,12 @@ impl MessageObject {
|
|||||||
file_mime: message.get_filemime(),
|
file_mime: message.get_filemime(),
|
||||||
file_bytes,
|
file_bytes,
|
||||||
file_name: message.get_filename(),
|
file_name: message.get_filename(),
|
||||||
|
webxdc_info,
|
||||||
// On a WebxdcInfoMessage this might include a hash holding
|
|
||||||
// information about a specific position or state in a webxdc app
|
|
||||||
webxdc_href: message.get_webxdc_href(),
|
|
||||||
|
|
||||||
download_state,
|
download_state,
|
||||||
|
|
||||||
original_msg_id: message
|
|
||||||
.get_original_msg_id(context)
|
|
||||||
.await?
|
|
||||||
.map(|id| id.to_u32()),
|
|
||||||
|
|
||||||
saved_message_id: message
|
|
||||||
.get_saved_msg_id(context)
|
|
||||||
.await?
|
|
||||||
.map(|id| id.to_u32()),
|
|
||||||
|
|
||||||
reactions,
|
reactions,
|
||||||
|
})
|
||||||
vcard_contact: vcard_contacts.first().cloned(),
|
|
||||||
};
|
|
||||||
Ok(Some(message_object))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -290,9 +248,6 @@ pub enum MessageViewtype {
|
|||||||
Gif,
|
Gif,
|
||||||
|
|
||||||
/// Message containing a sticker, similar to image.
|
/// Message containing a sticker, similar to image.
|
||||||
/// NB: When sending, the message viewtype may be changed to `Image` by some heuristics like
|
|
||||||
/// checking for transparent pixels. Use `Message::force_sticker()` to disable them.
|
|
||||||
///
|
|
||||||
/// If possible, the ui should display the image without borders in a transparent way.
|
/// If possible, the ui should display the image without borders in a transparent way.
|
||||||
/// A click on a sticker will offer to install the sticker set in some future.
|
/// A click on a sticker will offer to install the sticker set in some future.
|
||||||
Sticker,
|
Sticker,
|
||||||
@@ -310,16 +265,11 @@ pub enum MessageViewtype {
|
|||||||
/// Message containing any file, eg. a PDF.
|
/// Message containing any file, eg. a PDF.
|
||||||
File,
|
File,
|
||||||
|
|
||||||
/// Message is a call.
|
/// Message is an invitation to a videochat.
|
||||||
Call,
|
VideochatInvitation,
|
||||||
|
|
||||||
/// Message is an webxdc instance.
|
/// Message is an webxdc instance.
|
||||||
Webxdc,
|
Webxdc,
|
||||||
|
|
||||||
/// Message containing shared contacts represented as a vCard (virtual contact file)
|
|
||||||
/// with email addresses and possibly other fields.
|
|
||||||
/// Use `parse_vcard()` to retrieve them.
|
|
||||||
Vcard,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Viewtype> for MessageViewtype {
|
impl From<Viewtype> for MessageViewtype {
|
||||||
@@ -334,9 +284,8 @@ impl From<Viewtype> for MessageViewtype {
|
|||||||
Viewtype::Voice => MessageViewtype::Voice,
|
Viewtype::Voice => MessageViewtype::Voice,
|
||||||
Viewtype::Video => MessageViewtype::Video,
|
Viewtype::Video => MessageViewtype::Video,
|
||||||
Viewtype::File => MessageViewtype::File,
|
Viewtype::File => MessageViewtype::File,
|
||||||
Viewtype::Call => MessageViewtype::Call,
|
Viewtype::VideochatInvitation => MessageViewtype::VideochatInvitation,
|
||||||
Viewtype::Webxdc => MessageViewtype::Webxdc,
|
Viewtype::Webxdc => MessageViewtype::Webxdc,
|
||||||
Viewtype::Vcard => MessageViewtype::Vcard,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -353,9 +302,8 @@ impl From<MessageViewtype> for Viewtype {
|
|||||||
MessageViewtype::Voice => Viewtype::Voice,
|
MessageViewtype::Voice => Viewtype::Voice,
|
||||||
MessageViewtype::Video => Viewtype::Video,
|
MessageViewtype::Video => Viewtype::Video,
|
||||||
MessageViewtype::File => Viewtype::File,
|
MessageViewtype::File => Viewtype::File,
|
||||||
MessageViewtype::Call => Viewtype::Call,
|
MessageViewtype::VideochatInvitation => Viewtype::VideochatInvitation,
|
||||||
MessageViewtype::Webxdc => Viewtype::Webxdc,
|
MessageViewtype::Webxdc => Viewtype::Webxdc,
|
||||||
MessageViewtype::Vcard => Viewtype::Vcard,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -394,20 +342,9 @@ pub enum SystemMessageType {
|
|||||||
LocationOnly,
|
LocationOnly,
|
||||||
InvalidUnencryptedMail,
|
InvalidUnencryptedMail,
|
||||||
|
|
||||||
/// 1:1 chats info message telling that SecureJoin has started and the user should wait for it
|
|
||||||
/// to complete.
|
|
||||||
SecurejoinWait,
|
|
||||||
|
|
||||||
/// 1:1 chats info message telling that SecureJoin is still running, but the user may already
|
|
||||||
/// send messages.
|
|
||||||
SecurejoinWaitTimeout,
|
|
||||||
|
|
||||||
/// Chat ephemeral message timer is changed.
|
/// Chat ephemeral message timer is changed.
|
||||||
EphemeralTimerChanged,
|
EphemeralTimerChanged,
|
||||||
|
|
||||||
// Chat is e2ee
|
|
||||||
ChatE2ee,
|
|
||||||
|
|
||||||
// Chat protection state changed
|
// Chat protection state changed
|
||||||
ChatProtectionEnabled,
|
ChatProtectionEnabled,
|
||||||
ChatProtectionDisabled,
|
ChatProtectionDisabled,
|
||||||
@@ -423,12 +360,6 @@ pub enum SystemMessageType {
|
|||||||
|
|
||||||
/// Webxdc info added with `info` set in `send_webxdc_status_update()`.
|
/// Webxdc info added with `info` set in `send_webxdc_status_update()`.
|
||||||
WebxdcInfoMessage,
|
WebxdcInfoMessage,
|
||||||
|
|
||||||
/// This message contains a users iroh node address.
|
|
||||||
IrohNodeAddr,
|
|
||||||
|
|
||||||
CallAccepted,
|
|
||||||
CallEnded,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
||||||
@@ -445,18 +376,12 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
|||||||
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
|
SystemMessage::LocationStreamingEnabled => SystemMessageType::LocationStreamingEnabled,
|
||||||
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
|
SystemMessage::LocationOnly => SystemMessageType::LocationOnly,
|
||||||
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
|
SystemMessage::EphemeralTimerChanged => SystemMessageType::EphemeralTimerChanged,
|
||||||
SystemMessage::ChatE2ee => SystemMessageType::ChatE2ee,
|
|
||||||
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
|
SystemMessage::ChatProtectionEnabled => SystemMessageType::ChatProtectionEnabled,
|
||||||
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
|
SystemMessage::ChatProtectionDisabled => SystemMessageType::ChatProtectionDisabled,
|
||||||
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
SystemMessage::MultiDeviceSync => SystemMessageType::MultiDeviceSync,
|
||||||
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
|
SystemMessage::WebxdcStatusUpdate => SystemMessageType::WebxdcStatusUpdate,
|
||||||
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
|
SystemMessage::WebxdcInfoMessage => SystemMessageType::WebxdcInfoMessage,
|
||||||
SystemMessage::InvalidUnencryptedMail => SystemMessageType::InvalidUnencryptedMail,
|
SystemMessage::InvalidUnencryptedMail => SystemMessageType::InvalidUnencryptedMail,
|
||||||
SystemMessage::IrohNodeAddr => SystemMessageType::IrohNodeAddr,
|
|
||||||
SystemMessage::SecurejoinWait => SystemMessageType::SecurejoinWait,
|
|
||||||
SystemMessage::SecurejoinWaitTimeout => SystemMessageType::SecurejoinWaitTimeout,
|
|
||||||
SystemMessage::CallAccepted => SystemMessageType::CallAccepted,
|
|
||||||
SystemMessage::CallEnded => SystemMessageType::CallEnded,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -528,11 +453,11 @@ pub struct MessageSearchResult {
|
|||||||
author_name: String,
|
author_name: String,
|
||||||
author_color: String,
|
author_color: String,
|
||||||
author_id: u32,
|
author_id: u32,
|
||||||
chat_id: u32,
|
|
||||||
chat_profile_image: Option<String>,
|
chat_profile_image: Option<String>,
|
||||||
chat_color: String,
|
chat_color: String,
|
||||||
chat_name: String,
|
chat_name: String,
|
||||||
chat_type: JsonrpcChatType,
|
chat_type: u32,
|
||||||
|
is_chat_protected: bool,
|
||||||
is_chat_contact_request: bool,
|
is_chat_contact_request: bool,
|
||||||
is_chat_archived: bool,
|
is_chat_archived: bool,
|
||||||
message: String,
|
message: String,
|
||||||
@@ -567,11 +492,11 @@ impl MessageSearchResult {
|
|||||||
author_name,
|
author_name,
|
||||||
author_color: color_int_to_hex_string(sender.get_color()),
|
author_color: color_int_to_hex_string(sender.get_color()),
|
||||||
author_id: sender.id.to_u32(),
|
author_id: sender.id.to_u32(),
|
||||||
chat_id: chat.id.to_u32(),
|
|
||||||
chat_name: chat.get_name().to_owned(),
|
chat_name: chat.get_name().to_owned(),
|
||||||
chat_color,
|
chat_color,
|
||||||
chat_type: chat.get_type().into(),
|
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||||
chat_profile_image,
|
chat_profile_image,
|
||||||
|
is_chat_protected: chat.is_protected(),
|
||||||
is_chat_contact_request: chat.is_contact_request(),
|
is_chat_contact_request: chat.is_contact_request(),
|
||||||
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
|
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
|
||||||
message: message.get_text(),
|
message: message.get_text(),
|
||||||
@@ -582,7 +507,7 @@ impl MessageSearchResult {
|
|||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")]
|
#[serde(rename_all = "camelCase", rename = "MessageListItem", tag = "kind")]
|
||||||
pub enum JsonrpcMessageListItem {
|
pub enum JSONRPCMessageListItem {
|
||||||
Message {
|
Message {
|
||||||
msg_id: u32,
|
msg_id: u32,
|
||||||
},
|
},
|
||||||
@@ -595,13 +520,13 @@ pub enum JsonrpcMessageListItem {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ChatItem> for JsonrpcMessageListItem {
|
impl From<ChatItem> for JSONRPCMessageListItem {
|
||||||
fn from(item: ChatItem) -> Self {
|
fn from(item: ChatItem) -> Self {
|
||||||
match item {
|
match item {
|
||||||
ChatItem::Message { msg_id } => JsonrpcMessageListItem::Message {
|
ChatItem::Message { msg_id } => JSONRPCMessageListItem::Message {
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
ChatItem::DayMarker { timestamp } => JsonrpcMessageListItem::DayMarker { timestamp },
|
ChatItem::DayMarker { timestamp } => JSONRPCMessageListItem::DayMarker { timestamp },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -613,12 +538,9 @@ pub struct MessageData {
|
|||||||
pub html: Option<String>,
|
pub html: Option<String>,
|
||||||
pub viewtype: Option<MessageViewtype>,
|
pub viewtype: Option<MessageViewtype>,
|
||||||
pub file: Option<String>,
|
pub file: Option<String>,
|
||||||
pub filename: Option<String>,
|
|
||||||
pub location: Option<(f64, f64)>,
|
pub location: Option<(f64, f64)>,
|
||||||
pub override_sender_name: Option<String>,
|
pub override_sender_name: Option<String>,
|
||||||
/// Quoted message id. Takes preference over `quoted_text` (see below).
|
|
||||||
pub quoted_message_id: Option<u32>,
|
pub quoted_message_id: Option<u32>,
|
||||||
pub quoted_text: Option<String>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageData {
|
impl MessageData {
|
||||||
@@ -638,27 +560,22 @@ impl MessageData {
|
|||||||
message.set_override_sender_name(self.override_sender_name);
|
message.set_override_sender_name(self.override_sender_name);
|
||||||
}
|
}
|
||||||
if let Some(file) = self.file {
|
if let Some(file) = self.file {
|
||||||
message.set_file_and_deduplicate(
|
message.set_file(file, None);
|
||||||
context,
|
|
||||||
Path::new(&file),
|
|
||||||
self.filename.as_deref(),
|
|
||||||
None,
|
|
||||||
)?;
|
|
||||||
}
|
}
|
||||||
if let Some((latitude, longitude)) = self.location {
|
if let Some((latitude, longitude)) = self.location {
|
||||||
message.set_location(latitude, longitude);
|
message.set_location(latitude, longitude);
|
||||||
}
|
}
|
||||||
if let Some(id) = self.quoted_message_id {
|
if let Some(id) = self.quoted_message_id {
|
||||||
let quoted_message = Message::load_from_db(context, MsgId::new(id))
|
|
||||||
.await
|
|
||||||
.context("Failed to load quoted message")?;
|
|
||||||
message
|
message
|
||||||
.set_quote(context, Some("ed_message))
|
.set_quote(
|
||||||
.await
|
context,
|
||||||
.context("Failed to set quote")?;
|
Some(
|
||||||
} else if let Some(text) = self.quoted_text {
|
&Message::load_from_db(context, MsgId::new(id))
|
||||||
let protect = false;
|
.await
|
||||||
message.set_quote_text(Some((text, protect)));
|
.context("message to quote could not be loaded")?,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
}
|
}
|
||||||
Ok(message)
|
Ok(message)
|
||||||
}
|
}
|
||||||
@@ -674,18 +591,20 @@ pub struct MessageReadReceipt {
|
|||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct MessageInfo {
|
pub struct MessageInfo {
|
||||||
|
rawtext: String,
|
||||||
ephemeral_timer: EphemeralTimer,
|
ephemeral_timer: EphemeralTimer,
|
||||||
/// When message is ephemeral this contains the timestamp of the message expiry
|
/// When message is ephemeral this contains the timestamp of the message expiry
|
||||||
ephemeral_timestamp: Option<i64>,
|
ephemeral_timestamp: Option<i64>,
|
||||||
error: Option<String>,
|
error: Option<String>,
|
||||||
rfc724_mid: String,
|
rfc724_mid: String,
|
||||||
server_urls: Vec<String>,
|
server_urls: Vec<String>,
|
||||||
hop_info: String,
|
hop_info: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageInfo {
|
impl MessageInfo {
|
||||||
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
pub async fn from_msg_id(context: &Context, msg_id: MsgId) -> Result<Self> {
|
||||||
let message = Message::load_from_db(context, msg_id).await?;
|
let message = Message::load_from_db(context, msg_id).await?;
|
||||||
|
let rawtext = msg_id.rawtext(context).await?;
|
||||||
let ephemeral_timer = message.get_ephemeral_timer().into();
|
let ephemeral_timer = message.get_ephemeral_timer().into();
|
||||||
let ephemeral_timestamp = match message.get_ephemeral_timer() {
|
let ephemeral_timestamp = match message.get_ephemeral_timer() {
|
||||||
deltachat::ephemeral::Timer::Disabled => None,
|
deltachat::ephemeral::Timer::Disabled => None,
|
||||||
@@ -698,6 +617,7 @@ impl MessageInfo {
|
|||||||
let hop_info = msg_id.hop_info(context).await?;
|
let hop_info = msg_id.hop_info(context).await?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
|
rawtext,
|
||||||
ephemeral_timer,
|
ephemeral_timer,
|
||||||
ephemeral_timestamp,
|
ephemeral_timestamp,
|
||||||
error: message.error(),
|
error: message.error(),
|
||||||
@@ -711,7 +631,7 @@ impl MessageInfo {
|
|||||||
#[derive(
|
#[derive(
|
||||||
Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema,
|
Debug, PartialEq, Eq, Copy, Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema,
|
||||||
)]
|
)]
|
||||||
#[serde(rename_all = "camelCase", tag = "kind")]
|
#[serde(rename_all = "camelCase", tag = "variant")]
|
||||||
pub enum EphemeralTimer {
|
pub enum EphemeralTimer {
|
||||||
/// Timer is disabled.
|
/// Timer is disabled.
|
||||||
Disabled,
|
Disabled,
|
||||||
|
|||||||
@@ -1,14 +1,11 @@
|
|||||||
pub mod account;
|
pub mod account;
|
||||||
pub mod calls;
|
|
||||||
pub mod chat;
|
pub mod chat;
|
||||||
pub mod chat_list;
|
pub mod chat_list;
|
||||||
pub mod contact;
|
pub mod contact;
|
||||||
pub mod events;
|
pub mod events;
|
||||||
pub mod http;
|
pub mod http;
|
||||||
pub mod location;
|
pub mod location;
|
||||||
pub mod login_param;
|
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod notify_state;
|
|
||||||
pub mod provider_info;
|
pub mod provider_info;
|
||||||
pub mod qr;
|
pub mod qr;
|
||||||
pub mod reactions;
|
pub mod reactions;
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
use deltachat::push::NotifyState;
|
|
||||||
use serde::Serialize;
|
|
||||||
use typescript_type_def::TypeDef;
|
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
|
||||||
#[serde(rename = "NotifyState")]
|
|
||||||
pub enum JsonrpcNotifyState {
|
|
||||||
/// Not subscribed to push notifications.
|
|
||||||
NotConnected,
|
|
||||||
|
|
||||||
/// Subscribed to heartbeat push notifications.
|
|
||||||
Heartbeat,
|
|
||||||
|
|
||||||
/// Subscribed to push notifications for new messages.
|
|
||||||
Connected,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<NotifyState> for JsonrpcNotifyState {
|
|
||||||
fn from(state: NotifyState) -> Self {
|
|
||||||
match state {
|
|
||||||
NotifyState::NotConnected => Self::NotConnected,
|
|
||||||
NotifyState::Heartbeat => Self::Heartbeat,
|
|
||||||
NotifyState::Connected => Self::Connected,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,4 @@
|
|||||||
use deltachat::qr::Qr;
|
use deltachat::qr::Qr;
|
||||||
use serde::Deserialize;
|
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
@@ -7,215 +6,77 @@ use typescript_type_def::TypeDef;
|
|||||||
#[serde(rename = "Qr", rename_all = "camelCase")]
|
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||||
#[serde(tag = "kind")]
|
#[serde(tag = "kind")]
|
||||||
pub enum QrObject {
|
pub enum QrObject {
|
||||||
/// Ask the user whether to verify the contact.
|
|
||||||
///
|
|
||||||
/// If the user agrees, pass this QR code to [`crate::securejoin::join_securejoin`].
|
|
||||||
AskVerifyContact {
|
AskVerifyContact {
|
||||||
/// ID of the contact.
|
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
/// Fingerprint of the contact key as scanned from the QR code.
|
|
||||||
fingerprint: String,
|
fingerprint: String,
|
||||||
/// Invite number.
|
|
||||||
invitenumber: String,
|
invitenumber: String,
|
||||||
/// Authentication code.
|
|
||||||
authcode: String,
|
authcode: String,
|
||||||
},
|
},
|
||||||
/// Ask the user whether to join the group.
|
|
||||||
AskVerifyGroup {
|
AskVerifyGroup {
|
||||||
/// Group name.
|
|
||||||
grpname: String,
|
grpname: String,
|
||||||
/// Group ID.
|
|
||||||
grpid: String,
|
grpid: String,
|
||||||
/// ID of the contact.
|
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
/// Fingerprint of the contact key as scanned from the QR code.
|
|
||||||
fingerprint: String,
|
fingerprint: String,
|
||||||
/// Invite number.
|
|
||||||
invitenumber: String,
|
invitenumber: String,
|
||||||
/// Authentication code.
|
|
||||||
authcode: String,
|
authcode: String,
|
||||||
},
|
},
|
||||||
/// Ask the user whether to join the broadcast channel.
|
|
||||||
AskJoinBroadcast {
|
|
||||||
/// The user-visible name of this broadcast channel
|
|
||||||
name: String,
|
|
||||||
/// A string of random characters,
|
|
||||||
/// uniquely identifying this broadcast channel across all databases/clients.
|
|
||||||
/// Called `grpid` for historic reasons:
|
|
||||||
/// The id of multi-user chats is always called `grpid` in the database
|
|
||||||
/// because groups were once the only multi-user chats.
|
|
||||||
grpid: String,
|
|
||||||
/// ID of the contact who owns the broadcast channel and created the QR code.
|
|
||||||
contact_id: u32,
|
|
||||||
/// Fingerprint of the broadcast channel owner's key as scanned from the QR code.
|
|
||||||
fingerprint: String,
|
|
||||||
|
|
||||||
/// Invite number.
|
|
||||||
invitenumber: String,
|
|
||||||
/// Authentication code.
|
|
||||||
authcode: String,
|
|
||||||
},
|
|
||||||
/// Contact fingerprint is verified.
|
|
||||||
///
|
|
||||||
/// Ask the user if they want to start chatting.
|
|
||||||
FprOk {
|
FprOk {
|
||||||
/// Contact ID.
|
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
},
|
},
|
||||||
/// Scanned fingerprint does not match the last seen fingerprint.
|
|
||||||
FprMismatch {
|
FprMismatch {
|
||||||
/// Contact ID.
|
|
||||||
contact_id: Option<u32>,
|
contact_id: Option<u32>,
|
||||||
},
|
},
|
||||||
/// The scanned QR code contains a fingerprint but no e-mail address.
|
|
||||||
FprWithoutAddr {
|
FprWithoutAddr {
|
||||||
/// Key fingerprint.
|
|
||||||
fingerprint: String,
|
fingerprint: String,
|
||||||
},
|
},
|
||||||
/// Ask the user if they want to create an account on the given domain.
|
|
||||||
Account {
|
Account {
|
||||||
/// Server domain name.
|
|
||||||
domain: String,
|
domain: String,
|
||||||
},
|
},
|
||||||
/// Provides a backup that can be retrieved using iroh-net based backup transfer protocol.
|
Backup {
|
||||||
Backup2 {
|
ticket: String,
|
||||||
/// Authentication token.
|
|
||||||
auth_token: String,
|
|
||||||
/// Iroh node address.
|
|
||||||
node_addr: String,
|
|
||||||
},
|
},
|
||||||
BackupTooNew {},
|
|
||||||
/// Ask the user if they want to use the given service for video chats.
|
|
||||||
WebrtcInstance {
|
WebrtcInstance {
|
||||||
domain: String,
|
domain: String,
|
||||||
instance_pattern: String,
|
instance_pattern: String,
|
||||||
},
|
},
|
||||||
/// Ask the user if they want to use the given proxy.
|
|
||||||
///
|
|
||||||
/// Note that HTTP(S) URLs without a path
|
|
||||||
/// and query parameters are treated as HTTP(S) proxy URL.
|
|
||||||
/// UI may want to still offer to open the URL
|
|
||||||
/// in the browser if QR code contents
|
|
||||||
/// starts with `http://` or `https://`
|
|
||||||
/// and the QR code was not scanned from
|
|
||||||
/// the proxy configuration screen.
|
|
||||||
Proxy {
|
|
||||||
/// Proxy URL.
|
|
||||||
///
|
|
||||||
/// This is the URL that is going to be added.
|
|
||||||
url: String,
|
|
||||||
/// Host extracted from the URL to display in the UI.
|
|
||||||
host: String,
|
|
||||||
/// Port extracted from the URL to display in the UI.
|
|
||||||
port: u16,
|
|
||||||
},
|
|
||||||
/// Contact address is scanned.
|
|
||||||
///
|
|
||||||
/// Optionally, a draft message could be provided.
|
|
||||||
/// Ask the user if they want to start chatting.
|
|
||||||
Addr {
|
Addr {
|
||||||
/// Contact ID.
|
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
/// Draft message.
|
|
||||||
draft: Option<String>,
|
draft: Option<String>,
|
||||||
},
|
},
|
||||||
/// URL scanned.
|
|
||||||
///
|
|
||||||
/// Ask the user if they want to open a browser or copy the URL to clipboard.
|
|
||||||
Url {
|
Url {
|
||||||
url: String,
|
url: String,
|
||||||
},
|
},
|
||||||
/// Text scanned.
|
|
||||||
///
|
|
||||||
/// Ask the user if they want to copy the text to clipboard.
|
|
||||||
Text {
|
Text {
|
||||||
text: String,
|
text: String,
|
||||||
},
|
},
|
||||||
/// Ask the user if they want to withdraw their own QR code.
|
|
||||||
WithdrawVerifyContact {
|
WithdrawVerifyContact {
|
||||||
/// Contact ID.
|
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
/// Fingerprint of the contact key as scanned from the QR code.
|
|
||||||
fingerprint: String,
|
fingerprint: String,
|
||||||
/// Invite number.
|
|
||||||
invitenumber: String,
|
invitenumber: String,
|
||||||
/// Authentication code.
|
|
||||||
authcode: String,
|
authcode: String,
|
||||||
},
|
},
|
||||||
/// Ask the user if they want to withdraw their own group invite QR code.
|
|
||||||
WithdrawVerifyGroup {
|
WithdrawVerifyGroup {
|
||||||
/// Group name.
|
|
||||||
grpname: String,
|
grpname: String,
|
||||||
/// Group ID.
|
|
||||||
grpid: String,
|
grpid: String,
|
||||||
/// Contact ID.
|
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
/// Fingerprint of the contact key as scanned from the QR code.
|
|
||||||
fingerprint: String,
|
fingerprint: String,
|
||||||
/// Invite number.
|
|
||||||
invitenumber: String,
|
invitenumber: String,
|
||||||
/// Authentication code.
|
|
||||||
authcode: String,
|
authcode: String,
|
||||||
},
|
},
|
||||||
/// Ask the user if they want to withdraw their own broadcast channel invite QR code.
|
|
||||||
WithdrawJoinBroadcast {
|
|
||||||
/// Broadcast name.
|
|
||||||
name: String,
|
|
||||||
/// ID, uniquely identifying this chat. Called grpid for historic reasons.
|
|
||||||
grpid: String,
|
|
||||||
/// Contact ID. Always `ContactId::SELF`.
|
|
||||||
contact_id: u32,
|
|
||||||
/// Fingerprint of the contact key as scanned from the QR code.
|
|
||||||
fingerprint: String,
|
|
||||||
/// Invite number.
|
|
||||||
invitenumber: String,
|
|
||||||
/// Authentication code.
|
|
||||||
authcode: String,
|
|
||||||
},
|
|
||||||
/// Ask the user if they want to revive their own QR code.
|
|
||||||
ReviveVerifyContact {
|
ReviveVerifyContact {
|
||||||
/// Contact ID.
|
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
/// Fingerprint of the contact key as scanned from the QR code.
|
|
||||||
fingerprint: String,
|
fingerprint: String,
|
||||||
/// Invite number.
|
|
||||||
invitenumber: String,
|
invitenumber: String,
|
||||||
/// Authentication code.
|
|
||||||
authcode: String,
|
authcode: String,
|
||||||
},
|
},
|
||||||
/// Ask the user if they want to revive their own group invite QR code.
|
|
||||||
ReviveVerifyGroup {
|
ReviveVerifyGroup {
|
||||||
/// Contact ID.
|
|
||||||
grpname: String,
|
grpname: String,
|
||||||
/// Group ID.
|
|
||||||
grpid: String,
|
grpid: String,
|
||||||
/// Contact ID.
|
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
/// Fingerprint of the contact key as scanned from the QR code.
|
|
||||||
fingerprint: String,
|
fingerprint: String,
|
||||||
/// Invite number.
|
|
||||||
invitenumber: String,
|
invitenumber: String,
|
||||||
/// Authentication code.
|
|
||||||
authcode: String,
|
authcode: String,
|
||||||
},
|
},
|
||||||
/// Ask the user if they want to revive their own broadcast channel invite QR code.
|
|
||||||
ReviveJoinBroadcast {
|
|
||||||
/// Broadcast name.
|
|
||||||
name: String,
|
|
||||||
/// Globally unique chat ID. Called grpid for historic reasons.
|
|
||||||
grpid: String,
|
|
||||||
/// Contact ID. Always `ContactId::SELF`.
|
|
||||||
contact_id: u32,
|
|
||||||
/// Fingerprint of the contact key as scanned from the QR code.
|
|
||||||
fingerprint: String,
|
|
||||||
/// Invite number.
|
|
||||||
invitenumber: String,
|
|
||||||
/// Authentication code.
|
|
||||||
authcode: String,
|
|
||||||
},
|
|
||||||
/// `dclogin:` scheme parameters.
|
|
||||||
///
|
|
||||||
/// Ask the user if they want to login with the email address.
|
|
||||||
Login {
|
Login {
|
||||||
address: String,
|
address: String,
|
||||||
},
|
},
|
||||||
@@ -258,25 +119,6 @@ impl From<Qr> for QrObject {
|
|||||||
authcode,
|
authcode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Qr::AskJoinBroadcast {
|
|
||||||
name,
|
|
||||||
grpid,
|
|
||||||
contact_id,
|
|
||||||
fingerprint,
|
|
||||||
authcode,
|
|
||||||
invitenumber,
|
|
||||||
} => {
|
|
||||||
let contact_id = contact_id.to_u32();
|
|
||||||
let fingerprint = fingerprint.to_string();
|
|
||||||
QrObject::AskJoinBroadcast {
|
|
||||||
name,
|
|
||||||
grpid,
|
|
||||||
contact_id,
|
|
||||||
fingerprint,
|
|
||||||
authcode,
|
|
||||||
invitenumber,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Qr::FprOk { contact_id } => {
|
Qr::FprOk { contact_id } => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
QrObject::FprOk { contact_id }
|
QrObject::FprOk { contact_id }
|
||||||
@@ -287,15 +129,16 @@ impl From<Qr> for QrObject {
|
|||||||
}
|
}
|
||||||
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
|
Qr::FprWithoutAddr { fingerprint } => QrObject::FprWithoutAddr { fingerprint },
|
||||||
Qr::Account { domain } => QrObject::Account { domain },
|
Qr::Account { domain } => QrObject::Account { domain },
|
||||||
Qr::Backup2 {
|
Qr::Backup { ticket } => QrObject::Backup {
|
||||||
ref node_addr,
|
ticket: ticket.to_string(),
|
||||||
auth_token,
|
},
|
||||||
} => QrObject::Backup2 {
|
Qr::WebrtcInstance {
|
||||||
node_addr: serde_json::to_string(node_addr).unwrap_or_default(),
|
domain,
|
||||||
auth_token,
|
instance_pattern,
|
||||||
|
} => QrObject::WebrtcInstance {
|
||||||
|
domain,
|
||||||
|
instance_pattern,
|
||||||
},
|
},
|
||||||
Qr::BackupTooNew {} => QrObject::BackupTooNew {},
|
|
||||||
Qr::Proxy { url, host, port } => QrObject::Proxy { url, host, port },
|
|
||||||
Qr::Addr { contact_id, draft } => {
|
Qr::Addr { contact_id, draft } => {
|
||||||
let contact_id = contact_id.to_u32();
|
let contact_id = contact_id.to_u32();
|
||||||
QrObject::Addr { contact_id, draft }
|
QrObject::Addr { contact_id, draft }
|
||||||
@@ -336,25 +179,6 @@ impl From<Qr> for QrObject {
|
|||||||
authcode,
|
authcode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Qr::WithdrawJoinBroadcast {
|
|
||||||
name,
|
|
||||||
grpid,
|
|
||||||
contact_id,
|
|
||||||
fingerprint,
|
|
||||||
invitenumber,
|
|
||||||
authcode,
|
|
||||||
} => {
|
|
||||||
let contact_id = contact_id.to_u32();
|
|
||||||
let fingerprint = fingerprint.to_string();
|
|
||||||
QrObject::WithdrawJoinBroadcast {
|
|
||||||
name,
|
|
||||||
grpid,
|
|
||||||
contact_id,
|
|
||||||
fingerprint,
|
|
||||||
invitenumber,
|
|
||||||
authcode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Qr::ReviveVerifyContact {
|
Qr::ReviveVerifyContact {
|
||||||
contact_id,
|
contact_id,
|
||||||
fingerprint,
|
fingerprint,
|
||||||
@@ -389,76 +213,7 @@ impl From<Qr> for QrObject {
|
|||||||
authcode,
|
authcode,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Qr::ReviveJoinBroadcast {
|
|
||||||
name,
|
|
||||||
grpid,
|
|
||||||
contact_id,
|
|
||||||
fingerprint,
|
|
||||||
invitenumber,
|
|
||||||
authcode,
|
|
||||||
} => {
|
|
||||||
let contact_id = contact_id.to_u32();
|
|
||||||
let fingerprint = fingerprint.to_string();
|
|
||||||
QrObject::ReviveJoinBroadcast {
|
|
||||||
name,
|
|
||||||
grpid,
|
|
||||||
contact_id,
|
|
||||||
fingerprint,
|
|
||||||
invitenumber,
|
|
||||||
authcode,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Qr::Login { address, .. } => QrObject::Login { address },
|
Qr::Login { address, .. } => QrObject::Login { address },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, TypeDef, schemars::JsonSchema)]
|
|
||||||
pub enum SecurejoinSource {
|
|
||||||
/// Because of some problem, it is unknown where the QR code came from.
|
|
||||||
Unknown,
|
|
||||||
/// The user opened a link somewhere outside Delta Chat
|
|
||||||
ExternalLink,
|
|
||||||
/// The user clicked on a link in a message inside Delta Chat
|
|
||||||
InternalLink,
|
|
||||||
/// The user clicked "Paste from Clipboard" in the QR scan activity
|
|
||||||
Clipboard,
|
|
||||||
/// The user clicked "Load QR code as image" in the QR scan activity
|
|
||||||
ImageLoaded,
|
|
||||||
/// The user scanned a QR code
|
|
||||||
Scan,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Deserialize, TypeDef, schemars::JsonSchema)]
|
|
||||||
pub enum SecurejoinUiPath {
|
|
||||||
/// The UI path is unknown, or the user didn't open the QR code screen at all.
|
|
||||||
Unknown,
|
|
||||||
/// The user directly clicked on the QR icon in the main screen
|
|
||||||
QrIcon,
|
|
||||||
/// The user first clicked on the `+` button in the main screen,
|
|
||||||
/// and then on "New Contact"
|
|
||||||
NewContact,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SecurejoinSource> for deltachat::SecurejoinSource {
|
|
||||||
fn from(value: SecurejoinSource) -> Self {
|
|
||||||
match value {
|
|
||||||
SecurejoinSource::Unknown => deltachat::SecurejoinSource::Unknown,
|
|
||||||
SecurejoinSource::ExternalLink => deltachat::SecurejoinSource::ExternalLink,
|
|
||||||
SecurejoinSource::InternalLink => deltachat::SecurejoinSource::InternalLink,
|
|
||||||
SecurejoinSource::Clipboard => deltachat::SecurejoinSource::Clipboard,
|
|
||||||
SecurejoinSource::ImageLoaded => deltachat::SecurejoinSource::ImageLoaded,
|
|
||||||
SecurejoinSource::Scan => deltachat::SecurejoinSource::Scan,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<SecurejoinUiPath> for deltachat::SecurejoinUiPath {
|
|
||||||
fn from(value: SecurejoinUiPath) -> Self {
|
|
||||||
match value {
|
|
||||||
SecurejoinUiPath::Unknown => deltachat::SecurejoinUiPath::Unknown,
|
|
||||||
SecurejoinUiPath::QrIcon => deltachat::SecurejoinUiPath::QrIcon,
|
|
||||||
SecurejoinUiPath::NewContact => deltachat::SecurejoinUiPath::NewContact,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use typescript_type_def::TypeDef;
|
|||||||
/// A single reaction emoji.
|
/// A single reaction emoji.
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "Reaction", rename_all = "camelCase")]
|
#[serde(rename = "Reaction", rename_all = "camelCase")]
|
||||||
pub struct JsonrpcReaction {
|
pub struct JSONRPCReaction {
|
||||||
/// Emoji.
|
/// Emoji.
|
||||||
emoji: String,
|
emoji: String,
|
||||||
|
|
||||||
@@ -22,14 +22,14 @@ pub struct JsonrpcReaction {
|
|||||||
/// Structure representing all reactions to a particular message.
|
/// Structure representing all reactions to a particular message.
|
||||||
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "Reactions", rename_all = "camelCase")]
|
#[serde(rename = "Reactions", rename_all = "camelCase")]
|
||||||
pub struct JsonrpcReactions {
|
pub struct JSONRPCReactions {
|
||||||
/// Map from a contact to it's reaction to message.
|
/// Map from a contact to it's reaction to message.
|
||||||
reactions_by_contact: BTreeMap<u32, Vec<String>>,
|
reactions_by_contact: BTreeMap<u32, Vec<String>>,
|
||||||
/// Unique reactions and their count, sorted in descending order.
|
/// Unique reactions and their count, sorted in descending order.
|
||||||
reactions: Vec<JsonrpcReaction>,
|
reactions: Vec<JSONRPCReaction>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Reactions> for JsonrpcReactions {
|
impl From<Reactions> for JSONRPCReactions {
|
||||||
fn from(reactions: Reactions) -> Self {
|
fn from(reactions: Reactions) -> Self {
|
||||||
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
|
let mut reactions_by_contact: BTreeMap<u32, Vec<String>> = BTreeMap::new();
|
||||||
|
|
||||||
@@ -56,7 +56,7 @@ impl From<Reactions> for JsonrpcReactions {
|
|||||||
false
|
false
|
||||||
};
|
};
|
||||||
|
|
||||||
let reaction = JsonrpcReaction {
|
let reaction = JSONRPCReaction {
|
||||||
emoji,
|
emoji,
|
||||||
count,
|
count,
|
||||||
is_from_self,
|
is_from_self,
|
||||||
@@ -64,7 +64,7 @@ impl From<Reactions> for JsonrpcReactions {
|
|||||||
reactions_v.push(reaction)
|
reactions_v.push(reaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
JsonrpcReactions {
|
JSONRPCReactions {
|
||||||
reactions_by_contact,
|
reactions_by_contact,
|
||||||
reactions: reactions_v,
|
reactions: reactions_v,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,14 +35,6 @@ pub struct WebxdcMessageInfo {
|
|||||||
source_code_url: Option<String>,
|
source_code_url: Option<String>,
|
||||||
/// True if full internet access should be granted to the app.
|
/// True if full internet access should be granted to the app.
|
||||||
internet_access: bool,
|
internet_access: bool,
|
||||||
/// Address to be used for `window.webxdc.selfAddr` in JS land.
|
|
||||||
self_addr: String,
|
|
||||||
/// Milliseconds to wait before calling `sendUpdate()` again since the last call.
|
|
||||||
/// Should be exposed to `window.sendUpdateInterval` in JS land.
|
|
||||||
send_update_interval: usize,
|
|
||||||
/// Maximum number of bytes accepted for a serialized update object.
|
|
||||||
/// Should be exposed to `window.sendUpdateMaxSize` in JS land.
|
|
||||||
send_update_max_size: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WebxdcMessageInfo {
|
impl WebxdcMessageInfo {
|
||||||
@@ -57,11 +49,7 @@ impl WebxdcMessageInfo {
|
|||||||
document,
|
document,
|
||||||
summary,
|
summary,
|
||||||
source_code_url,
|
source_code_url,
|
||||||
request_integration: _,
|
|
||||||
internet_access,
|
internet_access,
|
||||||
self_addr,
|
|
||||||
send_update_interval,
|
|
||||||
send_update_max_size,
|
|
||||||
} = message.get_webxdc_info(context).await?;
|
} = message.get_webxdc_info(context).await?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
@@ -71,9 +59,6 @@ impl WebxdcMessageInfo {
|
|||||||
summary: maybe_empty_string_to_option(summary),
|
summary: maybe_empty_string_to_option(summary),
|
||||||
source_code_url: maybe_empty_string_to_option(source_code_url),
|
source_code_url: maybe_empty_string_to_option(source_code_url),
|
||||||
internet_access,
|
internet_access,
|
||||||
self_addr,
|
|
||||||
send_update_interval,
|
|
||||||
send_update_max_size,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
#![recursion_limit = "256"]
|
|
||||||
#![cfg_attr(not(test), forbid(clippy::indexing_slicing))]
|
|
||||||
#![cfg_attr(not(test), forbid(clippy::string_slice))]
|
|
||||||
pub mod api;
|
pub mod api;
|
||||||
pub use yerpc;
|
pub use yerpc;
|
||||||
|
|
||||||
@@ -85,7 +82,7 @@ mod tests {
|
|||||||
assert_eq!(result, response.to_owned());
|
assert_eq!(result, response.to_owned());
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":""}]}"#;
|
let request = r#"{"jsonrpc":"2.0","method":"batch_set_config","id":2,"params":[1,{"addr":"","mail_user":"","mail_pw":"","mail_server":"","mail_port":"","mail_security":"","imap_certificate_checks":"","send_user":"","send_pw":"","send_server":"","send_port":"","send_security":"","smtp_certificate_checks":"","socks5_enabled":"0","socks5_host":"","socks5_port":"","socks5_user":"","socks5_password":""}]}"#;
|
||||||
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
|
let response = r#"{"jsonrpc":"2.0","id":2,"result":null}"#;
|
||||||
session.handle_incoming(request).await;
|
session.handle_incoming(request).await;
|
||||||
let result = receiver.recv().await?;
|
let result = receiver.recv().await?;
|
||||||
|
|||||||
46
deltachat-jsonrpc/src/webserver.rs
Normal file
46
deltachat-jsonrpc/src/webserver.rs
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
use std::net::SocketAddr;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use axum::{extract::ws::WebSocketUpgrade, response::Response, routing::get, Extension, Router};
|
||||||
|
use yerpc::axum::handle_ws_rpc;
|
||||||
|
use yerpc::{RpcClient, RpcSession};
|
||||||
|
|
||||||
|
mod api;
|
||||||
|
use api::{Accounts, CommandApi};
|
||||||
|
|
||||||
|
const DEFAULT_PORT: u16 = 20808;
|
||||||
|
|
||||||
|
#[tokio::main(flavor = "multi_thread")]
|
||||||
|
async fn main() -> Result<(), std::io::Error> {
|
||||||
|
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
||||||
|
|
||||||
|
let path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "./accounts".to_string());
|
||||||
|
let port = std::env::var("DC_PORT")
|
||||||
|
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
|
||||||
|
.unwrap_or(DEFAULT_PORT);
|
||||||
|
log::info!("Starting with accounts directory `{path}`.");
|
||||||
|
let writable = true;
|
||||||
|
let accounts = Accounts::new(PathBuf::from(&path), writable).await.unwrap();
|
||||||
|
let state = CommandApi::new(accounts);
|
||||||
|
|
||||||
|
let app = Router::new()
|
||||||
|
.route("/ws", get(handler))
|
||||||
|
.layer(Extension(state.clone()));
|
||||||
|
|
||||||
|
tokio::spawn(async move {
|
||||||
|
state.accounts.write().await.start_io().await;
|
||||||
|
});
|
||||||
|
|
||||||
|
let addr = SocketAddr::from(([127, 0, 0, 1], port));
|
||||||
|
log::info!("JSON-RPC WebSocket server listening on {}", addr);
|
||||||
|
let listener = tokio::net::TcpListener::bind(addr).await.unwrap();
|
||||||
|
axum::serve(listener, app).await.unwrap();
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response {
|
||||||
|
let (client, out_receiver) = RpcClient::new();
|
||||||
|
let session = RpcSession::new(client.clone(), api.clone());
|
||||||
|
handle_ws_rpc(ws, out_receiver, session).await
|
||||||
|
}
|
||||||
56
deltachat-jsonrpc/typescript/example.html
Normal file
56
deltachat-jsonrpc/typescript/example.html
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>DeltaChat JSON-RPC example</title>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: monospace;
|
||||||
|
background: black;
|
||||||
|
color: grey;
|
||||||
|
}
|
||||||
|
.grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 3fr 1fr;
|
||||||
|
grid-template-areas: "a a" "b c";
|
||||||
|
}
|
||||||
|
.message {
|
||||||
|
color: red;
|
||||||
|
}
|
||||||
|
#header {
|
||||||
|
grid-area: a;
|
||||||
|
color: white;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
#header a {
|
||||||
|
color: white;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
#main {
|
||||||
|
grid-area: b;
|
||||||
|
color: green;
|
||||||
|
}
|
||||||
|
#main h2,
|
||||||
|
#main h3 {
|
||||||
|
color: blue;
|
||||||
|
}
|
||||||
|
#side {
|
||||||
|
grid-area: c;
|
||||||
|
color: #777;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script type="module" src="dist/example.bundle.js"></script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>DeltaChat JSON-RPC example</h1>
|
||||||
|
<div class="grid">
|
||||||
|
<div id="header"></div>
|
||||||
|
<div id="main"></div>
|
||||||
|
<div id="side"><h2>log</h2></div>
|
||||||
|
</div>
|
||||||
|
<p>
|
||||||
|
Tip: open the dev console and use the client with
|
||||||
|
<code>window.client</code>
|
||||||
|
</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
109
deltachat-jsonrpc/typescript/example/example.ts
Normal file
109
deltachat-jsonrpc/typescript/example/example.ts
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
import { DcEvent, DeltaChat } from "../deltachat.js";
|
||||||
|
|
||||||
|
var SELECTED_ACCOUNT = 0;
|
||||||
|
|
||||||
|
window.addEventListener("DOMContentLoaded", (_event) => {
|
||||||
|
(window as any).selectDeltaAccount = (id: string) => {
|
||||||
|
SELECTED_ACCOUNT = Number(id);
|
||||||
|
window.dispatchEvent(new Event("account-changed"));
|
||||||
|
};
|
||||||
|
console.log("launch run script...");
|
||||||
|
run().catch((err) => console.error("run failed", err));
|
||||||
|
});
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const $main = document.getElementById("main")!;
|
||||||
|
const $side = document.getElementById("side")!;
|
||||||
|
const $head = document.getElementById("header")!;
|
||||||
|
|
||||||
|
const client = new DeltaChat("ws://localhost:20808/ws");
|
||||||
|
|
||||||
|
(window as any).client = client.rpc;
|
||||||
|
|
||||||
|
client.on("ALL", (accountId, event) => {
|
||||||
|
onIncomingEvent(accountId, event);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener("account-changed", async (_event: Event) => {
|
||||||
|
listChatsForSelectedAccount();
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all([loadAccountsInHeader(), listChatsForSelectedAccount()]);
|
||||||
|
|
||||||
|
async function loadAccountsInHeader() {
|
||||||
|
console.log("load accounts");
|
||||||
|
const accounts = await client.rpc.getAllAccounts();
|
||||||
|
console.log("accounts loaded", accounts);
|
||||||
|
for (const account of accounts) {
|
||||||
|
if (account.kind === "Configured") {
|
||||||
|
write(
|
||||||
|
$head,
|
||||||
|
`<a href="#" onclick="selectDeltaAccount(${account.id})">
|
||||||
|
${account.id}: ${account.addr!}
|
||||||
|
</a> `
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
write(
|
||||||
|
$head,
|
||||||
|
`<a href="#">
|
||||||
|
${account.id}: (unconfigured)
|
||||||
|
</a> `
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function listChatsForSelectedAccount() {
|
||||||
|
clear($main);
|
||||||
|
const selectedAccount = SELECTED_ACCOUNT;
|
||||||
|
const info = await client.rpc.getAccountInfo(selectedAccount);
|
||||||
|
if (info.kind !== "Configured") {
|
||||||
|
return write($main, "Account is not configured");
|
||||||
|
}
|
||||||
|
write($main, `<h2>${info.addr!}</h2>`);
|
||||||
|
const chats = await client.rpc.getChatlistEntries(
|
||||||
|
selectedAccount,
|
||||||
|
0,
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
);
|
||||||
|
for (const chatId of chats) {
|
||||||
|
const chat = await client.rpc.getFullChatById(selectedAccount, chatId);
|
||||||
|
write($main, `<h3>${chat.name}</h3>`);
|
||||||
|
const messageIds = await client.rpc.getMessageIds(
|
||||||
|
selectedAccount,
|
||||||
|
chatId,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
);
|
||||||
|
const messages = await client.rpc.getMessages(
|
||||||
|
selectedAccount,
|
||||||
|
messageIds
|
||||||
|
);
|
||||||
|
for (const [_messageId, message] of Object.entries(messages)) {
|
||||||
|
if (message.kind === "message") write($main, `<p>${message.text}</p>`);
|
||||||
|
else write($main, `<p>loading error: ${message.error}</p>`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onIncomingEvent(accountId: number, event: DcEvent) {
|
||||||
|
write(
|
||||||
|
$side,
|
||||||
|
`
|
||||||
|
<p class="message">
|
||||||
|
[<strong>${event.kind}</strong> on account ${accountId}]<br>
|
||||||
|
<em>f1:</em> ${JSON.stringify(
|
||||||
|
Object.assign({}, event, { kind: undefined })
|
||||||
|
)}
|
||||||
|
</p>`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function write(el: HTMLElement, html: string) {
|
||||||
|
el.innerHTML += html;
|
||||||
|
}
|
||||||
|
function clear(el: HTMLElement) {
|
||||||
|
el.innerHTML = "";
|
||||||
|
}
|
||||||
29
deltachat-jsonrpc/typescript/example/node-add-account.js
Normal file
29
deltachat-jsonrpc/typescript/example/node-add-account.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
import { DeltaChat } from "../dist/deltachat.js";
|
||||||
|
|
||||||
|
run().catch(console.error);
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const delta = new DeltaChat("ws://localhost:20808/ws");
|
||||||
|
delta.on("event", (event) => {
|
||||||
|
console.log("event", event.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
const email = process.argv[2];
|
||||||
|
const password = process.argv[3];
|
||||||
|
if (!email || !password)
|
||||||
|
throw new Error(
|
||||||
|
"USAGE: node node-add-account.js <EMAILADDRESS> <PASSWORD>"
|
||||||
|
);
|
||||||
|
console.log(`creating account for ${email}`);
|
||||||
|
const id = await delta.rpc.addAccount();
|
||||||
|
console.log(`created account id ${id}`);
|
||||||
|
await delta.rpc.setConfig(id, "addr", email);
|
||||||
|
await delta.rpc.setConfig(id, "mail_pw", password);
|
||||||
|
console.log("configuration updated");
|
||||||
|
await delta.rpc.configure(id);
|
||||||
|
console.log("account configured!");
|
||||||
|
|
||||||
|
const accounts = await delta.rpc.getAllAccounts();
|
||||||
|
console.log("accounts", accounts);
|
||||||
|
console.log("waiting for events...");
|
||||||
|
}
|
||||||
14
deltachat-jsonrpc/typescript/example/node-demo.js
Normal file
14
deltachat-jsonrpc/typescript/example/node-demo.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { DeltaChat } from "../dist/deltachat.js";
|
||||||
|
|
||||||
|
run().catch(console.error);
|
||||||
|
|
||||||
|
async function run() {
|
||||||
|
const delta = new DeltaChat();
|
||||||
|
delta.on("event", (event) => {
|
||||||
|
console.log("event", event.data);
|
||||||
|
});
|
||||||
|
|
||||||
|
const accounts = await delta.rpc.getAllAccounts();
|
||||||
|
console.log("accounts", accounts);
|
||||||
|
console.log("waiting for events...");
|
||||||
|
}
|
||||||
@@ -2,46 +2,45 @@
|
|||||||
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
|
"author": "Delta Chat Developers (ML) <delta@codespeak.net>",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@deltachat/tiny-emitter": "3.0.0",
|
"@deltachat/tiny-emitter": "3.0.0",
|
||||||
"isomorphic-ws": "^5.0.0",
|
"isomorphic-ws": "^4.0.1",
|
||||||
"yerpc": "^0.6.2"
|
"yerpc": "^0.4.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@types/chai": "^4.3.10",
|
"@types/chai": "^4.2.21",
|
||||||
"@types/chai-as-promised": "^7.1.8",
|
"@types/chai-as-promised": "^7.1.5",
|
||||||
"@types/mocha": "^10.0.4",
|
"@types/mocha": "^9.0.0",
|
||||||
"@types/ws": "^8.5.9",
|
"@types/ws": "^7.2.4",
|
||||||
"c8": "^8.0.1",
|
"c8": "^7.10.0",
|
||||||
"chai": "^4.3.4",
|
"chai": "^4.3.4",
|
||||||
"chai-as-promised": "^7.1.1",
|
"chai-as-promised": "^7.1.1",
|
||||||
"esbuild": "^0.25.5",
|
"esbuild": "^0.17.9",
|
||||||
"http-server": "^14.1.1",
|
"http-server": "^14.1.1",
|
||||||
"mocha": "^10.2.0",
|
"mocha": "^9.1.1",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.5.3",
|
"prettier": "^2.6.2",
|
||||||
"typedoc": "^0.28.5",
|
"typedoc": "^0.23.2",
|
||||||
"typescript": "^5.8.3",
|
"typescript": "^4.5.5",
|
||||||
"ws": "^8.5.0"
|
"ws": "^8.5.0"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": {
|
".": {
|
||||||
"import": "./dist/deltachat.js",
|
"import": "./dist/deltachat.js",
|
||||||
"require": "./dist/deltachat.cjs",
|
"require": "./dist/deltachat.cjs"
|
||||||
"types": "./dist/deltachat.d.ts"
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"license": "MPL-2.0",
|
"license": "MPL-2.0",
|
||||||
"main": "dist/deltachat.js",
|
"main": "dist/deltachat.js",
|
||||||
"name": "@deltachat/jsonrpc-client",
|
"name": "@deltachat/jsonrpc-client",
|
||||||
"repository": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/chatmail/core.git"
|
|
||||||
},
|
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "run-s generate-bindings extract-constants build:tsc build:bundle build:cjs",
|
"build": "run-s generate-bindings extract-constants build:tsc build:bundle build:cjs",
|
||||||
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
|
"build:bundle": "esbuild --format=esm --bundle dist/deltachat.js --outfile=dist/deltachat.bundle.js",
|
||||||
"build:cjs": "esbuild --format=cjs --bundle --packages=external dist/deltachat.js --outfile=dist/deltachat.cjs",
|
"build:cjs": "esbuild --format=cjs --bundle --packages=external dist/deltachat.js --outfile=dist/deltachat.cjs",
|
||||||
"build:tsc": "tsc",
|
"build:tsc": "tsc",
|
||||||
"docs": "typedoc --out docs deltachat.ts",
|
"docs": "typedoc --out docs deltachat.ts",
|
||||||
|
"example": "run-s build example:build example:start",
|
||||||
|
"example:build": "esbuild --bundle dist/example/example.js --outfile=dist/example.bundle.js",
|
||||||
|
"example:dev": "esbuild example/example.ts --bundle --outfile=dist/example.bundle.js --servedir=.",
|
||||||
|
"example:start": "http-server .",
|
||||||
"extract-constants": "node ./scripts/generate-constants.js",
|
"extract-constants": "node ./scripts/generate-constants.js",
|
||||||
"generate-bindings": "cargo test",
|
"generate-bindings": "cargo test",
|
||||||
"prettier:check": "prettier --check .",
|
"prettier:check": "prettier --check .",
|
||||||
@@ -54,5 +53,5 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "dist/deltachat.d.ts",
|
"types": "dist/deltachat.d.ts",
|
||||||
"version": "2.31.0"
|
"version": "1.137.2"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,24 +5,24 @@ const json = JSON.parse(readFileSync("./coverage/coverage-final.json"));
|
|||||||
const jsonCoverage =
|
const jsonCoverage =
|
||||||
json[Object.keys(json).find((k) => k.includes(generatedFile))];
|
json[Object.keys(json).find((k) => k.includes(generatedFile))];
|
||||||
const fnMap = Object.keys(jsonCoverage.fnMap).map(
|
const fnMap = Object.keys(jsonCoverage.fnMap).map(
|
||||||
(key) => jsonCoverage.fnMap[key],
|
(key) => jsonCoverage.fnMap[key]
|
||||||
);
|
);
|
||||||
const htmlCoverage = readFileSync(
|
const htmlCoverage = readFileSync(
|
||||||
"./coverage/" + generatedFile + ".html",
|
"./coverage/" + generatedFile + ".html",
|
||||||
"utf8",
|
"utf8"
|
||||||
);
|
);
|
||||||
const uncoveredLines = htmlCoverage
|
const uncoveredLines = htmlCoverage
|
||||||
.split("\n")
|
.split("\n")
|
||||||
.filter((line) => line.includes(`"function not covered"`));
|
.filter((line) => line.includes(`"function not covered"`));
|
||||||
const uncoveredFunctions = uncoveredLines.map(
|
const uncoveredFunctions = uncoveredLines.map(
|
||||||
(line) => />([\w_]+)\(/.exec(line)[1],
|
(line) => />([\w_]+)\(/.exec(line)[1]
|
||||||
);
|
);
|
||||||
console.log(
|
console.log(
|
||||||
"\nUncovered api functions:\n" +
|
"\nUncovered api functions:\n" +
|
||||||
uncoveredFunctions
|
uncoveredFunctions
|
||||||
.map((uF) => fnMap.find(({ name }) => name === uF))
|
.map((uF) => fnMap.find(({ name }) => name === uF))
|
||||||
.map(
|
.map(
|
||||||
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`,
|
({ name, line }) => `.${name.padEnd(40)} (${generatedFile}:${line})`
|
||||||
)
|
)
|
||||||
.join("\n"),
|
.join("\n")
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ while (null != (match = regex.exec(header_data))) {
|
|||||||
|
|
||||||
const constants = data
|
const constants = data
|
||||||
.filter(
|
.filter(
|
||||||
({ key }) => key.toUpperCase()[0] === key[0], // check if define name is uppercase
|
({ key }) => key.toUpperCase()[0] === key[0] // check if define name is uppercase
|
||||||
)
|
)
|
||||||
.sort((lhs, rhs) => {
|
.sort((lhs, rhs) => {
|
||||||
if (lhs.key < rhs.key) return -1;
|
if (lhs.key < rhs.key) return -1;
|
||||||
@@ -40,35 +40,15 @@ const constants = data
|
|||||||
key.startsWith("DC_DOWNLOAD") ||
|
key.startsWith("DC_DOWNLOAD") ||
|
||||||
key.startsWith("DC_INFO_") ||
|
key.startsWith("DC_INFO_") ||
|
||||||
(key.startsWith("DC_MSG") && !key.startsWith("DC_MSG_ID")) ||
|
(key.startsWith("DC_MSG") && !key.startsWith("DC_MSG_ID")) ||
|
||||||
key.startsWith("DC_QR_") ||
|
key.startsWith("DC_QR_")
|
||||||
key.startsWith("DC_CERTCK_") ||
|
|
||||||
key.startsWith("DC_SOCKET_") ||
|
|
||||||
key.startsWith("DC_LP_AUTH_") ||
|
|
||||||
key.startsWith("DC_PUSH_") ||
|
|
||||||
key.startsWith("DC_TEXT1_") ||
|
|
||||||
key.startsWith("DC_CHAT_TYPE")
|
|
||||||
);
|
);
|
||||||
})
|
})
|
||||||
.map((row) => {
|
.map((row) => {
|
||||||
return ` export const ${row.key} = ${row.value};`;
|
return ` ${row.key}: ${row.value}`;
|
||||||
})
|
})
|
||||||
.join("\n");
|
.join(",\n");
|
||||||
|
|
||||||
writeFileSync(
|
writeFileSync(
|
||||||
resolve(__dirname, "../generated/constants.ts"),
|
resolve(__dirname, "../generated/constants.ts"),
|
||||||
`// Generated!
|
`// Generated!\n\nexport enum C {\n${constants.replace(/:/g, " =")},\n}\n`
|
||||||
|
|
||||||
export namespace C {
|
|
||||||
${constants}
|
|
||||||
/** @deprecated 10-8-2025 compare string directly with \`== "Group"\` */
|
|
||||||
export const DC_CHAT_TYPE_GROUP = "Group";
|
|
||||||
/** @deprecated 10-8-2025 compare string directly with \`== "InBroadcast"\`*/
|
|
||||||
export const DC_CHAT_TYPE_IN_BROADCAST = "InBroadcast";
|
|
||||||
/** @deprecated 10-8-2025 compare string directly with \`== "Mailinglist"\` */
|
|
||||||
export const DC_CHAT_TYPE_MAILINGLIST = "Mailinglist";
|
|
||||||
/** @deprecated 10-8-2025 compare string directly with \`== "OutBroadcast"\` */
|
|
||||||
export const DC_CHAT_TYPE_OUT_BROADCAST = "OutBroadcast";
|
|
||||||
/** @deprecated 10-8-2025 compare string directly with \`== "Single"\` */
|
|
||||||
export const DC_CHAT_TYPE_SINGLE = "Single";
|
|
||||||
}\n`,
|
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,19 +2,19 @@ import * as T from "../generated/types.js";
|
|||||||
import { EventType } from "../generated/types.js";
|
import { EventType } from "../generated/types.js";
|
||||||
import * as RPC from "../generated/jsonrpc.js";
|
import * as RPC from "../generated/jsonrpc.js";
|
||||||
import { RawClient } from "../generated/client.js";
|
import { RawClient } from "../generated/client.js";
|
||||||
import { BaseTransport, Request } from "yerpc";
|
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
|
||||||
import { TinyEmitter } from "@deltachat/tiny-emitter";
|
import { TinyEmitter } from "@deltachat/tiny-emitter";
|
||||||
|
|
||||||
type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
||||||
[Property in EventType["kind"]]: (
|
[Property in EventType["kind"]]: (
|
||||||
accountId: number,
|
accountId: number,
|
||||||
event: Extract<EventType, { kind: Property }>,
|
event: Extract<EventType, { kind: Property }>
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ContextEvents = { ALL: (event: EventType) => void } & {
|
type ContextEvents = { ALL: (event: EventType) => void } & {
|
||||||
[Property in EventType["kind"]]: (
|
[Property in EventType["kind"]]: (
|
||||||
event: Extract<EventType, { kind: Property }>,
|
event: Extract<EventType, { kind: Property }>
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -25,22 +25,16 @@ export type DcEventType<T extends EventType["kind"]> = Extract<
|
|||||||
>;
|
>;
|
||||||
|
|
||||||
export class BaseDeltaChat<
|
export class BaseDeltaChat<
|
||||||
Transport extends BaseTransport<any>,
|
Transport extends BaseTransport<any>
|
||||||
> extends TinyEmitter<Events> {
|
> extends TinyEmitter<Events> {
|
||||||
rpc: RawClient;
|
rpc: RawClient;
|
||||||
|
account?: T.Account;
|
||||||
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
|
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
|
||||||
|
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
private eventTask: Promise<void>;
|
private eventTask: Promise<void>;
|
||||||
|
|
||||||
constructor(
|
constructor(public transport: Transport, startEventLoop: boolean) {
|
||||||
public transport: Transport,
|
|
||||||
/**
|
|
||||||
* Whether to start calling {@linkcode RawClient.getNextEvent}
|
|
||||||
* and emitting the respective events on this class.
|
|
||||||
*/
|
|
||||||
startEventLoop: boolean,
|
|
||||||
) {
|
|
||||||
super();
|
super();
|
||||||
this.rpc = new RawClient(this.transport);
|
this.rpc = new RawClient(this.transport);
|
||||||
if (startEventLoop) {
|
if (startEventLoop) {
|
||||||
@@ -48,9 +42,6 @@ export class BaseDeltaChat<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @see the constructor's `startEventLoop`
|
|
||||||
*/
|
|
||||||
async eventLoop(): Promise<void> {
|
async eventLoop(): Promise<void> {
|
||||||
while (true) {
|
while (true) {
|
||||||
const event = await this.rpc.getNextEvent();
|
const event = await this.rpc.getNextEvent();
|
||||||
@@ -62,24 +53,17 @@ export class BaseDeltaChat<
|
|||||||
this.contextEmitters[event.contextId].emit(
|
this.contextEmitters[event.contextId].emit(
|
||||||
event.event.kind,
|
event.event.kind,
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
event.event as any,
|
event.event as any
|
||||||
);
|
);
|
||||||
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
|
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated use {@linkcode BaseDeltaChat.rpc.getAllAccounts} instead.
|
|
||||||
*/
|
|
||||||
async listAccounts(): Promise<T.Account[]> {
|
async listAccounts(): Promise<T.Account[]> {
|
||||||
return await this.rpc.getAllAccounts();
|
return await this.rpc.getAllAccounts();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* A convenience function to listen on events binned by `account_id`
|
|
||||||
* (see {@linkcode RawClient.getAllAccounts}).
|
|
||||||
*/
|
|
||||||
getContextEvents(account_id: number) {
|
getContextEvents(account_id: number) {
|
||||||
if (this.contextEmitters[account_id]) {
|
if (this.contextEmitters[account_id]) {
|
||||||
return this.contextEmitters[account_id];
|
return this.contextEmitters[account_id];
|
||||||
@@ -90,6 +74,34 @@ export class BaseDeltaChat<
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Opts = {
|
||||||
|
url: string;
|
||||||
|
startEventLoop: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DEFAULT_OPTS: Opts = {
|
||||||
|
url: "ws://localhost:20808/ws",
|
||||||
|
startEventLoop: true,
|
||||||
|
};
|
||||||
|
export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
|
||||||
|
opts: Opts;
|
||||||
|
close() {
|
||||||
|
this.transport.close();
|
||||||
|
}
|
||||||
|
constructor(opts?: Opts | string) {
|
||||||
|
if (typeof opts === "string") {
|
||||||
|
opts = { ...DEFAULT_OPTS, url: opts };
|
||||||
|
} else if (opts) {
|
||||||
|
opts = { ...DEFAULT_OPTS, ...opts };
|
||||||
|
} else {
|
||||||
|
opts = { ...DEFAULT_OPTS };
|
||||||
|
}
|
||||||
|
const transport = new WebsocketTransport(opts.url);
|
||||||
|
super(transport, opts.startEventLoop);
|
||||||
|
this.opts = opts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
|
export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
|
||||||
close() {}
|
close() {}
|
||||||
constructor(input: any, output: any, startEventLoop: boolean) {
|
constructor(input: any, output: any, startEventLoop: boolean) {
|
||||||
@@ -99,10 +111,7 @@ export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class StdioTransport extends BaseTransport {
|
export class StdioTransport extends BaseTransport {
|
||||||
constructor(
|
constructor(public input: any, public output: any) {
|
||||||
public input: any,
|
|
||||||
public output: any,
|
|
||||||
) {
|
|
||||||
super();
|
super();
|
||||||
|
|
||||||
var buffer = "";
|
var buffer = "";
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { strictEqual } from "assert";
|
||||||
import chai, { assert, expect } from "chai";
|
import chai, { assert, expect } from "chai";
|
||||||
import chaiAsPromised from "chai-as-promised";
|
import chaiAsPromised from "chai-as-promised";
|
||||||
chai.use(chaiAsPromised);
|
chai.use(chaiAsPromised);
|
||||||
@@ -31,14 +32,14 @@ describe("basic tests", () => {
|
|||||||
|
|
||||||
expect(
|
expect(
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
validAddresses.map((email) => dc.rpc.checkEmailValidity(email)),
|
validAddresses.map((email) => dc.rpc.checkEmailValidity(email))
|
||||||
),
|
)
|
||||||
).to.not.contain(false);
|
).to.not.contain(false);
|
||||||
|
|
||||||
expect(
|
expect(
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email)),
|
invalidAddresses.map((email) => dc.rpc.checkEmailValidity(email))
|
||||||
),
|
)
|
||||||
).to.not.contain(true);
|
).to.not.contain(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ describe("basic tests", () => {
|
|||||||
const contactId = await dc.rpc.createContact(
|
const contactId = await dc.rpc.createContact(
|
||||||
accountId,
|
accountId,
|
||||||
"example@delta.chat",
|
"example@delta.chat",
|
||||||
null,
|
null
|
||||||
);
|
);
|
||||||
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
|
expect((await dc.rpc.getContact(accountId, contactId)).isBlocked).to.be
|
||||||
.false;
|
.false;
|
||||||
@@ -126,7 +127,7 @@ describe("basic tests", () => {
|
|||||||
await dc.rpc.batchSetConfig(accountId, config);
|
await dc.rpc.batchSetConfig(accountId, config);
|
||||||
const retrieved = await dc.rpc.batchGetConfig(
|
const retrieved = await dc.rpc.batchGetConfig(
|
||||||
accountId,
|
accountId,
|
||||||
Object.keys(config),
|
Object.keys(config)
|
||||||
);
|
);
|
||||||
expect(retrieved).to.deep.equal(config);
|
expect(retrieved).to.deep.equal(config);
|
||||||
});
|
});
|
||||||
@@ -138,7 +139,7 @@ describe("basic tests", () => {
|
|||||||
await dc.rpc.batchSetConfig(accountId, config);
|
await dc.rpc.batchSetConfig(accountId, config);
|
||||||
const retrieved = await dc.rpc.batchGetConfig(
|
const retrieved = await dc.rpc.batchGetConfig(
|
||||||
accountId,
|
accountId,
|
||||||
Object.keys(config),
|
Object.keys(config)
|
||||||
);
|
);
|
||||||
expect(retrieved).to.deep.equal(config);
|
expect(retrieved).to.deep.equal(config);
|
||||||
});
|
});
|
||||||
@@ -152,7 +153,7 @@ describe("basic tests", () => {
|
|||||||
await dc.rpc.batchSetConfig(accountId, config);
|
await dc.rpc.batchSetConfig(accountId, config);
|
||||||
const retrieved = await dc.rpc.batchGetConfig(
|
const retrieved = await dc.rpc.batchGetConfig(
|
||||||
accountId,
|
accountId,
|
||||||
Object.keys(config),
|
Object.keys(config)
|
||||||
);
|
);
|
||||||
expect(retrieved).to.deep.equal(config);
|
expect(retrieved).to.deep.equal(config);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { assert, expect } from "chai";
|
import { assert, expect } from "chai";
|
||||||
import { StdioDeltaChat as DeltaChat, DcEvent, C } from "../deltachat.js";
|
import { StdioDeltaChat as DeltaChat, DcEvent } from "../deltachat.js";
|
||||||
import { RpcServerHandle, createTempUser, startServer } from "./test_base.js";
|
import { RpcServerHandle, createTempUser, startServer } from "./test_base.js";
|
||||||
|
|
||||||
const EVENT_TIMEOUT = 20000;
|
const EVENT_TIMEOUT = 20000;
|
||||||
@@ -17,12 +17,12 @@ describe("online tests", function () {
|
|||||||
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
|
if (process.env.COVERAGE && !process.env.COVERAGE_OFFLINE) {
|
||||||
console.error(
|
console.error(
|
||||||
"CAN NOT RUN COVERAGE correctly: Missing CHATMAIL_DOMAIN environment variable!\n\n",
|
"CAN NOT RUN COVERAGE correctly: Missing CHATMAIL_DOMAIN environment variable!\n\n",
|
||||||
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test",
|
"You can set COVERAGE_OFFLINE=1 to circumvent this check and skip the online tests, but those coverage results will be wrong, because some functions can only be tested in the online test"
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
console.log(
|
console.log(
|
||||||
"Missing CHATMAIL_DOMAIN environment variable!, skip integration tests",
|
"Missing CHATMAIL_DOMAIN environment variable!, skip integration tests"
|
||||||
);
|
);
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -36,7 +36,7 @@ describe("online tests", function () {
|
|||||||
account1 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
account1 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
||||||
if (!account1 || !account1.email || !account1.password) {
|
if (!account1 || !account1.email || !account1.password) {
|
||||||
console.log(
|
console.log(
|
||||||
"We didn't got back an account from the api, skip integration tests",
|
"We didn't got back an account from the api, skip integration tests"
|
||||||
);
|
);
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ describe("online tests", function () {
|
|||||||
account2 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
account2 = createTempUser(process.env.CHATMAIL_DOMAIN);
|
||||||
if (!account2 || !account2.email || !account2.password) {
|
if (!account2 || !account2.email || !account2.password) {
|
||||||
console.log(
|
console.log(
|
||||||
"We didn't got back an account2 from the api, skip integration tests",
|
"We didn't got back an account2 from the api, skip integration tests"
|
||||||
);
|
);
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
@@ -64,7 +64,6 @@ describe("online tests", function () {
|
|||||||
await dc.rpc.setConfig(accountId1, "addr", account1.email);
|
await dc.rpc.setConfig(accountId1, "addr", account1.email);
|
||||||
await dc.rpc.setConfig(accountId1, "mail_pw", account1.password);
|
await dc.rpc.setConfig(accountId1, "mail_pw", account1.password);
|
||||||
await dc.rpc.configure(accountId1);
|
await dc.rpc.configure(accountId1);
|
||||||
await waitForEvent(dc, "ImapInboxIdle", accountId1);
|
|
||||||
|
|
||||||
accountId2 = await dc.rpc.addAccount();
|
accountId2 = await dc.rpc.addAccount();
|
||||||
await dc.rpc.batchSetConfig(accountId2, {
|
await dc.rpc.batchSetConfig(accountId2, {
|
||||||
@@ -72,7 +71,6 @@ describe("online tests", function () {
|
|||||||
mail_pw: account2.password,
|
mail_pw: account2.password,
|
||||||
});
|
});
|
||||||
await dc.rpc.configure(accountId2);
|
await dc.rpc.configure(accountId2);
|
||||||
await waitForEvent(dc, "ImapInboxIdle", accountId2);
|
|
||||||
accountsConfigured = true;
|
accountsConfigured = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -82,10 +80,16 @@ describe("online tests", function () {
|
|||||||
}
|
}
|
||||||
this.timeout(15000);
|
this.timeout(15000);
|
||||||
|
|
||||||
const vcard = await dc.rpc.makeVcard(accountId2, [C.DC_CONTACT_ID_SELF]);
|
const contactId = await dc.rpc.createContact(
|
||||||
const contactId = (await dc.rpc.importVcardContents(accountId1, vcard))[0];
|
accountId1,
|
||||||
|
account2.email,
|
||||||
|
null
|
||||||
|
);
|
||||||
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
|
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
|
||||||
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
|
const eventPromise = Promise.race([
|
||||||
|
waitForEvent(dc, "MsgsChanged", accountId2),
|
||||||
|
waitForEvent(dc, "IncomingMsg", accountId2),
|
||||||
|
]);
|
||||||
|
|
||||||
await dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello");
|
await dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello");
|
||||||
const { chatId: chatIdOnAccountB } = await eventPromise;
|
const { chatId: chatIdOnAccountB } = await eventPromise;
|
||||||
@@ -94,28 +98,31 @@ describe("online tests", function () {
|
|||||||
accountId2,
|
accountId2,
|
||||||
chatIdOnAccountB,
|
chatIdOnAccountB,
|
||||||
false,
|
false,
|
||||||
false,
|
false
|
||||||
);
|
);
|
||||||
|
|
||||||
// There are 2 messages in the chat:
|
expect(messageList).have.length(1);
|
||||||
// 'Messages are end-to-end encrypted' (info message) and 'Hello'
|
const message = await dc.rpc.getMessage(accountId2, messageList[0]);
|
||||||
expect(messageList).have.length(2);
|
|
||||||
const message = await dc.rpc.getMessage(accountId2, messageList[1]);
|
|
||||||
expect(message.text).equal("Hello");
|
expect(message.text).equal("Hello");
|
||||||
expect(message.showPadlock).equal(true);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("send and receive text message roundtrip", async function () {
|
it("send and receive text message roundtrip, encrypted on answer onwards", async function () {
|
||||||
if (!accountsConfigured) {
|
if (!accountsConfigured) {
|
||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
this.timeout(10000);
|
this.timeout(10000);
|
||||||
|
|
||||||
// send message from A to B
|
// send message from A to B
|
||||||
const vcard = await dc.rpc.makeVcard(accountId2, [C.DC_CONTACT_ID_SELF]);
|
const contactId = await dc.rpc.createContact(
|
||||||
const contactId = (await dc.rpc.importVcardContents(accountId1, vcard))[0];
|
accountId1,
|
||||||
|
account2.email,
|
||||||
|
null
|
||||||
|
);
|
||||||
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
|
const chatId = await dc.rpc.createChatByContactId(accountId1, contactId);
|
||||||
const eventPromise = waitForEvent(dc, "IncomingMsg", accountId2);
|
const eventPromise = Promise.race([
|
||||||
|
waitForEvent(dc, "MsgsChanged", accountId2),
|
||||||
|
waitForEvent(dc, "IncomingMsg", accountId2),
|
||||||
|
]);
|
||||||
dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello2");
|
dc.rpc.miscSendTextMessage(accountId1, chatId, "Hello2");
|
||||||
// wait for message from A
|
// wait for message from A
|
||||||
console.log("wait for message from A");
|
console.log("wait for message from A");
|
||||||
@@ -128,15 +135,18 @@ describe("online tests", function () {
|
|||||||
accountId2,
|
accountId2,
|
||||||
chatIdOnAccountB,
|
chatIdOnAccountB,
|
||||||
false,
|
false,
|
||||||
false,
|
false
|
||||||
);
|
);
|
||||||
const message = await dc.rpc.getMessage(
|
const message = await dc.rpc.getMessage(
|
||||||
accountId2,
|
accountId2,
|
||||||
messageList.reverse()[0],
|
messageList.reverse()[0]
|
||||||
);
|
);
|
||||||
expect(message.text).equal("Hello2");
|
expect(message.text).equal("Hello2");
|
||||||
// Send message back from B to A
|
// Send message back from B to A
|
||||||
const eventPromise2 = waitForEvent(dc, "IncomingMsg", accountId1);
|
const eventPromise2 = Promise.race([
|
||||||
|
waitForEvent(dc, "MsgsChanged", accountId1),
|
||||||
|
waitForEvent(dc, "IncomingMsg", accountId1),
|
||||||
|
]);
|
||||||
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
|
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
|
||||||
// Check if answer arrives at A and if it is encrypted
|
// Check if answer arrives at A and if it is encrypted
|
||||||
await eventPromise2;
|
await eventPromise2;
|
||||||
@@ -154,7 +164,7 @@ describe("online tests", function () {
|
|||||||
const info = await dc.rpc.getProviderInfo(acc, "example.com");
|
const info = await dc.rpc.getProviderInfo(acc, "example.com");
|
||||||
expect(info).to.be.not.null;
|
expect(info).to.be.not.null;
|
||||||
expect(info?.overviewPage).to.equal(
|
expect(info?.overviewPage).to.equal(
|
||||||
"https://providers.delta.chat/example-com",
|
"https://providers.delta.chat/example-com"
|
||||||
);
|
);
|
||||||
expect(info?.status).to.equal(3);
|
expect(info?.status).to.equal(3);
|
||||||
});
|
});
|
||||||
@@ -171,12 +181,12 @@ async function waitForEvent<T extends DcEvent["kind"]>(
|
|||||||
dc: DeltaChat,
|
dc: DeltaChat,
|
||||||
eventType: T,
|
eventType: T,
|
||||||
accountId: number,
|
accountId: number,
|
||||||
timeout: number = EVENT_TIMEOUT,
|
timeout: number = EVENT_TIMEOUT
|
||||||
): Promise<Extract<DcEvent, { kind: T }>> {
|
): Promise<Extract<DcEvent, { kind: T }>> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const rejectTimeout = setTimeout(
|
const rejectTimeout = setTimeout(
|
||||||
() => reject(new Error("Timeout reached before event came in")),
|
() => reject(new Error("Timeout reached before event came in")),
|
||||||
timeout,
|
timeout
|
||||||
);
|
);
|
||||||
const callback = (contextId: number, event: DcEvent) => {
|
const callback = (contextId: number, event: DcEvent) => {
|
||||||
if (contextId == accountId) {
|
if (contextId == accountId) {
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export async function startServer(): Promise<RpcServerHandle> {
|
|||||||
const tmpDir = await mkdtemp(join(tmpdir(), "deltachat-jsonrpc-test"));
|
const tmpDir = await mkdtemp(join(tmpdir(), "deltachat-jsonrpc-test"));
|
||||||
|
|
||||||
const pathToServerBinary = resolve(
|
const pathToServerBinary = resolve(
|
||||||
join(await getTargetDir(), "debug/deltachat-rpc-server"),
|
join(await getTargetDir(), "debug/deltachat-rpc-server")
|
||||||
);
|
);
|
||||||
|
|
||||||
const server = spawn(pathToServerBinary, {
|
const server = spawn(pathToServerBinary, {
|
||||||
@@ -29,7 +29,7 @@ export async function startServer(): Promise<RpcServerHandle> {
|
|||||||
throw new Error(
|
throw new Error(
|
||||||
"Failed to start server executable " +
|
"Failed to start server executable " +
|
||||||
pathToServerBinary +
|
pathToServerBinary +
|
||||||
", make sure you built it first.",
|
", make sure you built it first."
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
let shouldClose = false;
|
let shouldClose = false;
|
||||||
@@ -83,7 +83,7 @@ function getTargetDir(): Promise<string> {
|
|||||||
reject(error);
|
reject(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,6 +15,6 @@
|
|||||||
"noImplicitAny": true,
|
"noImplicitAny": true,
|
||||||
"isolatedModules": true
|
"isolatedModules": true
|
||||||
},
|
},
|
||||||
"include": ["*.ts", "test/*.ts"],
|
"include": ["*.ts", "example/*.ts", "test/*.ts"],
|
||||||
"compileOnSave": false
|
"compileOnSave": false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -90,11 +90,6 @@ impl Ratelimit {
|
|||||||
pub fn until_can_send(&self) -> Duration {
|
pub fn until_can_send(&self) -> Duration {
|
||||||
self.until_can_send_at(SystemTime::now())
|
self.until_can_send_at(SystemTime::now())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns minimum possible update interval in milliseconds.
|
|
||||||
pub fn update_interval(&self) -> usize {
|
|
||||||
(self.window.as_millis() as f64 / self.quota) as usize
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -107,7 +102,6 @@ mod tests {
|
|||||||
|
|
||||||
let mut ratelimit = Ratelimit::new_at(Duration::new(60, 0), 3.0, now);
|
let mut ratelimit = Ratelimit::new_at(Duration::new(60, 0), 3.0, now);
|
||||||
assert!(ratelimit.can_send_at(now));
|
assert!(ratelimit.can_send_at(now));
|
||||||
assert_eq!(ratelimit.update_interval(), 20_000);
|
|
||||||
|
|
||||||
// Send burst of 3 messages.
|
// Send burst of 3 messages.
|
||||||
ratelimit.send_at(now);
|
ratelimit.send_at(now);
|
||||||
|
|||||||
@@ -1,21 +1,20 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-repl"
|
name = "deltachat-repl"
|
||||||
version = "2.31.0"
|
version = "1.137.2"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
repository = "https://github.com/chatmail/core"
|
repository = "https://github.com/deltachat/deltachat-core-rust"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow = { workspace = true }
|
ansi_term = "0.12.1"
|
||||||
deltachat = { workspace = true, features = ["internals"]}
|
anyhow = "1"
|
||||||
dirs = "6"
|
deltachat = { path = "..", features = ["internals"]}
|
||||||
log = { workspace = true }
|
dirs = "5"
|
||||||
nu-ansi-term = { workspace = true }
|
log = "0.4.21"
|
||||||
qr2term = "0.3.3"
|
pretty_env_logger = "0.5"
|
||||||
rusqlite = { workspace = true }
|
rusqlite = "0.31"
|
||||||
rustyline = "16"
|
rustyline = "14"
|
||||||
tokio = { workspace = true, features = ["fs", "rt-multi-thread", "macros"] }
|
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||||
tracing-subscriber = { workspace = true, features = ["env-filter"] }
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
|
|||||||
@@ -6,7 +6,9 @@ use std::str::FromStr;
|
|||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use anyhow::{bail, ensure, Result};
|
use anyhow::{bail, ensure, Result};
|
||||||
use deltachat::chat::{self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration};
|
use deltachat::chat::{
|
||||||
|
self, Chat, ChatId, ChatItem, ChatVisibility, MuteDuration, ProtectionStatus,
|
||||||
|
};
|
||||||
use deltachat::chatlist::*;
|
use deltachat::chatlist::*;
|
||||||
use deltachat::constants::*;
|
use deltachat::constants::*;
|
||||||
use deltachat::contact::*;
|
use deltachat::contact::*;
|
||||||
@@ -17,9 +19,8 @@ use deltachat::location;
|
|||||||
use deltachat::log::LogExt;
|
use deltachat::log::LogExt;
|
||||||
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
use deltachat::message::{self, Message, MessageState, MsgId, Viewtype};
|
||||||
use deltachat::mimeparser::SystemMessage;
|
use deltachat::mimeparser::SystemMessage;
|
||||||
use deltachat::peer_channels::{send_webxdc_realtime_advertisement, send_webxdc_realtime_data};
|
use deltachat::peerstate::*;
|
||||||
use deltachat::qr::*;
|
use deltachat::qr::*;
|
||||||
use deltachat::qr_code_generator::create_qr_svg;
|
|
||||||
use deltachat::reaction::send_reaction;
|
use deltachat::reaction::send_reaction;
|
||||||
use deltachat::receive_imf::*;
|
use deltachat::receive_imf::*;
|
||||||
use deltachat::sql;
|
use deltachat::sql;
|
||||||
@@ -32,6 +33,14 @@ use tokio::fs;
|
|||||||
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
/// e.g. bitmask 7 triggers actions defined with bits 1, 2 and 4.
|
||||||
async fn reset_tables(context: &Context, bits: i32) {
|
async fn reset_tables(context: &Context, bits: i32) {
|
||||||
println!("Resetting tables ({bits})...");
|
println!("Resetting tables ({bits})...");
|
||||||
|
if 0 != bits & 2 {
|
||||||
|
context
|
||||||
|
.sql()
|
||||||
|
.execute("DELETE FROM acpeerstates;", ())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
println!("(2) Peerstates reset.");
|
||||||
|
}
|
||||||
if 0 != bits & 4 {
|
if 0 != bits & 4 {
|
||||||
context
|
context
|
||||||
.sql()
|
.sql()
|
||||||
@@ -70,17 +79,22 @@ async fn reset_tables(context: &Context, bits: i32) {
|
|||||||
.await
|
.await
|
||||||
.unwrap();
|
.unwrap();
|
||||||
context.sql().config_cache().write().await.clear();
|
context.sql().config_cache().write().await.clear();
|
||||||
|
context
|
||||||
|
.sql()
|
||||||
|
.execute("DELETE FROM leftgrps;", ())
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
println!("(8) Rest but server config reset.");
|
println!("(8) Rest but server config reset.");
|
||||||
}
|
}
|
||||||
|
|
||||||
context.emit_msgs_changed_without_ids();
|
context.emit_msgs_changed_without_ids();
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn poke_eml_file(context: &Context, filename: &Path) -> Result<()> {
|
async fn poke_eml_file(context: &Context, filename: impl AsRef<Path>) -> Result<()> {
|
||||||
let data = read_file(context, filename).await?;
|
let data = read_file(context, filename).await?;
|
||||||
|
|
||||||
if let Err(err) = receive_imf(context, &data, false).await {
|
if let Err(err) = receive_imf(context, &data, false).await {
|
||||||
eprintln!("receive_imf errored: {err:?}");
|
println!("receive_imf errored: {err:?}");
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -104,13 +118,13 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
|||||||
} else {
|
} else {
|
||||||
let rs = context.sql().get_raw_config("import_spec").await.unwrap();
|
let rs = context.sql().get_raw_config("import_spec").await.unwrap();
|
||||||
if rs.is_none() {
|
if rs.is_none() {
|
||||||
eprintln!("Import: No file or folder given.");
|
error!(context, "Import: No file or folder given.");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
real_spec = rs.unwrap();
|
real_spec = rs.unwrap();
|
||||||
}
|
}
|
||||||
if let Some(suffix) = get_filesuffix_lc(&real_spec) {
|
if let Some(suffix) = get_filesuffix_lc(&real_spec) {
|
||||||
if suffix == "eml" && poke_eml_file(context, Path::new(&real_spec)).await.is_ok() {
|
if suffix == "eml" && poke_eml_file(context, &real_spec).await.is_ok() {
|
||||||
read_cnt += 1
|
read_cnt += 1
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -124,16 +138,13 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
|||||||
if name.ends_with(".eml") {
|
if name.ends_with(".eml") {
|
||||||
let path_plus_name = format!("{}/{}", &real_spec, name);
|
let path_plus_name = format!("{}/{}", &real_spec, name);
|
||||||
println!("Import: {path_plus_name}");
|
println!("Import: {path_plus_name}");
|
||||||
if poke_eml_file(context, Path::new(&path_plus_name))
|
if poke_eml_file(context, path_plus_name).await.is_ok() {
|
||||||
.await
|
|
||||||
.is_ok()
|
|
||||||
{
|
|
||||||
read_cnt += 1
|
read_cnt += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Import: Cannot open directory \"{}\".", &real_spec);
|
error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -203,7 +214,13 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
|||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
},
|
},
|
||||||
if msg.get_viewtype() == Viewtype::Webxdc {
|
if msg.get_viewtype() == Viewtype::VideochatInvitation {
|
||||||
|
format!(
|
||||||
|
"[VIDEOCHAT-INVITATION: {}, type={}]",
|
||||||
|
msg.get_videochat_url().unwrap_or_default(),
|
||||||
|
msg.get_videochat_type().unwrap_or_default()
|
||||||
|
)
|
||||||
|
} else if msg.get_viewtype() == Viewtype::Webxdc {
|
||||||
match msg.get_webxdc_info(context).await {
|
match msg.get_webxdc_info(context).await {
|
||||||
Ok(info) => format!(
|
Ok(info) => format!(
|
||||||
"[WEBXDC: {}, icon={}, document={}, summary={}, source_code_url={}]",
|
"[WEBXDC: {}, icon={}, document={}, summary={}, source_code_url={}]",
|
||||||
@@ -255,7 +272,7 @@ async fn log_msglist(context: &Context, msglist: &[MsgId]) -> Result<()> {
|
|||||||
|
|
||||||
async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()> {
|
async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()> {
|
||||||
for contact_id in contacts {
|
for contact_id in contacts {
|
||||||
let line2 = "".to_string();
|
let mut line2 = "".to_string();
|
||||||
let contact = Contact::get_by_id(context, *contact_id).await?;
|
let contact = Contact::get_by_id(context, *contact_id).await?;
|
||||||
let name = contact.get_display_name();
|
let name = contact.get_display_name();
|
||||||
let addr = contact.get_addr();
|
let addr = contact.get_addr();
|
||||||
@@ -274,6 +291,15 @@ async fn log_contactlist(context: &Context, contacts: &[ContactId]) -> Result<()
|
|||||||
verified_str,
|
verified_str,
|
||||||
if !addr.is_empty() { addr } else { "addr unset" }
|
if !addr.is_empty() { addr } else { "addr unset" }
|
||||||
);
|
);
|
||||||
|
let peerstate = Peerstate::from_addr(context, addr)
|
||||||
|
.await
|
||||||
|
.expect("peerstate error");
|
||||||
|
if peerstate.is_some() && *contact_id != ContactId::SELF {
|
||||||
|
line2 = format!(
|
||||||
|
", prefer-encrypt={}",
|
||||||
|
peerstate.as_ref().unwrap().prefer_encrypt
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
println!("Contact#{}: {}{}", *contact_id, line, line2);
|
println!("Contact#{}: {}{}", *contact_id, line, line2);
|
||||||
}
|
}
|
||||||
@@ -311,7 +337,10 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
send-backup\n\
|
send-backup\n\
|
||||||
receive-backup <qr>\n\
|
receive-backup <qr>\n\
|
||||||
export-keys\n\
|
export-keys\n\
|
||||||
import-keys <key-file>\n\
|
import-keys\n\
|
||||||
|
export-setup\n\
|
||||||
|
dump <filename>\n\n
|
||||||
|
read <filename>\n\n
|
||||||
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
poke [<eml-file>|<folder>|<addr> <key-file>]\n\
|
||||||
reset <flags>\n\
|
reset <flags>\n\
|
||||||
stop\n\
|
stop\n\
|
||||||
@@ -320,13 +349,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
_ => println!(
|
_ => println!(
|
||||||
"==========================Database commands==\n\
|
"==========================Database commands==\n\
|
||||||
info\n\
|
info\n\
|
||||||
|
open <file to open or create>\n\
|
||||||
|
close\n\
|
||||||
set <configuration-key> [<value>]\n\
|
set <configuration-key> [<value>]\n\
|
||||||
get <configuration-key>\n\
|
get <configuration-key>\n\
|
||||||
oauth2\n\
|
oauth2\n\
|
||||||
configure\n\
|
configure\n\
|
||||||
connect\n\
|
connect\n\
|
||||||
disconnect\n\
|
disconnect\n\
|
||||||
fetch\n\
|
|
||||||
connectivity\n\
|
connectivity\n\
|
||||||
maybenetwork\n\
|
maybenetwork\n\
|
||||||
housekeeping\n\
|
housekeeping\n\
|
||||||
@@ -334,30 +364,28 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
==============================Chat commands==\n\
|
==============================Chat commands==\n\
|
||||||
listchats [<query>]\n\
|
listchats [<query>]\n\
|
||||||
listarchived\n\
|
listarchived\n\
|
||||||
start-realtime <msg-id>\n\
|
|
||||||
send-realtime <msg-id> <data>\n\
|
|
||||||
chat [<chat-id>|0]\n\
|
chat [<chat-id>|0]\n\
|
||||||
createchat <contact-id>\n\
|
createchat <contact-id>\n\
|
||||||
creategroup <name>\n\
|
creategroup <name>\n\
|
||||||
createbroadcast <name>\n\
|
createbroadcast\n\
|
||||||
|
createprotected <name>\n\
|
||||||
addmember <contact-id>\n\
|
addmember <contact-id>\n\
|
||||||
removemember <contact-id>\n\
|
removemember <contact-id>\n\
|
||||||
groupname <name>\n\
|
groupname <name>\n\
|
||||||
groupimage <image>\n\
|
groupimage [<file>]\n\
|
||||||
chatinfo\n\
|
chatinfo\n\
|
||||||
sendlocations <seconds>\n\
|
sendlocations <seconds>\n\
|
||||||
setlocation <lat> <lng>\n\
|
setlocation <lat> <lng>\n\
|
||||||
dellocations\n\
|
dellocations\n\
|
||||||
getlocations [<contact-id>]\n\
|
getlocations [<contact-id>]\n\
|
||||||
send <text>\n\
|
send <text>\n\
|
||||||
send-sync <text>\n\
|
|
||||||
sendempty\n\
|
|
||||||
sendimage <file> [<text>]\n\
|
sendimage <file> [<text>]\n\
|
||||||
sendsticker <file> [<text>]\n\
|
sendsticker <file> [<text>]\n\
|
||||||
sendfile <file> [<text>]\n\
|
sendfile <file> [<text>]\n\
|
||||||
sendhtml <file for html-part> [<text for plain-part>]\n\
|
sendhtml <file for html-part> [<text for plain-part>]\n\
|
||||||
sendsyncmsg\n\
|
sendsyncmsg\n\
|
||||||
sendupdate <msg-id> <json status update>\n\
|
sendupdate <msg-id> <json status update>\n\
|
||||||
|
videochat\n\
|
||||||
draft [<text>]\n\
|
draft [<text>]\n\
|
||||||
devicemsg <text>\n\
|
devicemsg <text>\n\
|
||||||
listmedia\n\
|
listmedia\n\
|
||||||
@@ -369,7 +397,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
unmute <chat-id>\n\
|
unmute <chat-id>\n\
|
||||||
delchat <chat-id>\n\
|
delchat <chat-id>\n\
|
||||||
accept <chat-id>\n\
|
accept <chat-id>\n\
|
||||||
blockchat <chat-id>\n\
|
decline <chat-id>\n\
|
||||||
===========================Message commands==\n\
|
===========================Message commands==\n\
|
||||||
listmsgs <query>\n\
|
listmsgs <query>\n\
|
||||||
msginfo <msg-id>\n\
|
msginfo <msg-id>\n\
|
||||||
@@ -383,14 +411,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
react <msg-id> [<reaction>]\n\
|
react <msg-id> [<reaction>]\n\
|
||||||
===========================Contact commands==\n\
|
===========================Contact commands==\n\
|
||||||
listcontacts [<query>]\n\
|
listcontacts [<query>]\n\
|
||||||
|
listverified [<query>]\n\
|
||||||
addcontact [<name>] <addr>\n\
|
addcontact [<name>] <addr>\n\
|
||||||
contactinfo <contact-id>\n\
|
contactinfo <contact-id>\n\
|
||||||
delcontact <contact-id>\n\
|
delcontact <contact-id>\n\
|
||||||
|
cleanupcontacts\n\
|
||||||
block <contact-id>\n\
|
block <contact-id>\n\
|
||||||
unblock <contact-id>\n\
|
unblock <contact-id>\n\
|
||||||
listblocked\n\
|
listblocked\n\
|
||||||
import-vcard <file>\n\
|
|
||||||
make-vcard <file> <contact-id> [contact-id ...]\n\
|
|
||||||
======================================Misc.==\n\
|
======================================Misc.==\n\
|
||||||
getqr [<chat-id>]\n\
|
getqr [<chat-id>]\n\
|
||||||
getqrsvg [<chat-id>]\n\
|
getqrsvg [<chat-id>]\n\
|
||||||
@@ -398,7 +426,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
checkqr <qr-content>\n\
|
checkqr <qr-content>\n\
|
||||||
joinqr <qr-content>\n\
|
joinqr <qr-content>\n\
|
||||||
setqr <qr-content>\n\
|
setqr <qr-content>\n\
|
||||||
createqrsvg <qr-content>\n\
|
|
||||||
providerinfo <addr>\n\
|
providerinfo <addr>\n\
|
||||||
fileinfo <file>\n\
|
fileinfo <file>\n\
|
||||||
estimatedeletion <seconds>\n\
|
estimatedeletion <seconds>\n\
|
||||||
@@ -411,7 +438,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
Ok(setup_code) => {
|
Ok(setup_code) => {
|
||||||
println!("Setup code for the transferred setup message: {setup_code}",)
|
println!("Setup code for the transferred setup message: {setup_code}",)
|
||||||
}
|
}
|
||||||
Err(err) => bail!("Failed to generate setup code: {err}"),
|
Err(err) => bail!("Failed to generate setup code: {}", err),
|
||||||
},
|
},
|
||||||
"get-setupcodebegin" => {
|
"get-setupcodebegin" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||||
@@ -425,7 +452,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
setupcodebegin.unwrap_or_default(),
|
setupcodebegin.unwrap_or_default(),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
bail!("{msg_id} is no setup message.",);
|
bail!("{} is no setup message.", msg_id,);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"continue-key-transfer" => {
|
"continue-key-transfer" => {
|
||||||
@@ -461,9 +488,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
}
|
}
|
||||||
"send-backup" => {
|
"send-backup" => {
|
||||||
let provider = BackupProvider::prepare(&context).await?;
|
let provider = BackupProvider::prepare(&context).await?;
|
||||||
let qr = format_backup(&provider.qr())?;
|
let qr = provider.qr();
|
||||||
println!("QR code: {qr}");
|
println!("QR code: {}", format_backup(&qr)?);
|
||||||
qr2term::print_qr(qr.as_str())?;
|
|
||||||
provider.await?;
|
provider.await?;
|
||||||
}
|
}
|
||||||
"receive-backup" => {
|
"receive-backup" => {
|
||||||
@@ -477,17 +503,32 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
println!("Exported to {}.", dir.to_string_lossy());
|
println!("Exported to {}.", dir.to_string_lossy());
|
||||||
}
|
}
|
||||||
"import-keys" => {
|
"import-keys" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <key-file> missing.");
|
|
||||||
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?;
|
imex(&context, ImexMode::ImportSelfKeys, arg1.as_ref(), None).await?;
|
||||||
}
|
}
|
||||||
|
"export-setup" => {
|
||||||
|
let setup_code = create_setup_code(&context);
|
||||||
|
let file_name = blobdir.join("autocrypt-setup-message.html");
|
||||||
|
let file_content = render_setup_file(&context, &setup_code).await?;
|
||||||
|
fs::write(&file_name, file_content).await?;
|
||||||
|
println!(
|
||||||
|
"Setup message written to: {}\nSetup code: {}",
|
||||||
|
file_name.display(),
|
||||||
|
&setup_code,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
"dump" => {
|
||||||
|
ensure!(!arg1.is_empty(), "Argument <filename> missing.");
|
||||||
|
serialize_database(&context, arg1).await?;
|
||||||
|
}
|
||||||
|
"read" => {
|
||||||
|
ensure!(!arg1.is_empty(), "Argument <filename> missing.");
|
||||||
|
deserialize_database(&context, arg1).await?;
|
||||||
|
}
|
||||||
"poke" => {
|
"poke" => {
|
||||||
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
ensure!(poke_spec(&context, Some(arg1)).await, "Poke failed");
|
||||||
}
|
}
|
||||||
"reset" => {
|
"reset" => {
|
||||||
ensure!(
|
ensure!(!arg1.is_empty(), "Argument <bits> missing: 1=jobs, 2=peerstates, 4=private keys, 8=rest but server config");
|
||||||
!arg1.is_empty(),
|
|
||||||
"Argument <bits> missing: 4=private keys, 8=rest but server config"
|
|
||||||
);
|
|
||||||
let bits: i32 = arg1.parse()?;
|
let bits: i32 = arg1.parse()?;
|
||||||
ensure!(bits < 16, "<bits> must be lower than 16.");
|
ensure!(bits < 16, "<bits> must be lower than 16.");
|
||||||
reset_tables(&context, bits).await;
|
reset_tables(&context, bits).await;
|
||||||
@@ -520,7 +561,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
println!("Report written to: {file:#?}");
|
println!("Report written to: {file:#?}");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
bail!("Failed to get connectivity html: {err}");
|
bail!("Failed to get connectivity html: {}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -555,7 +596,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
for i in (0..cnt).rev() {
|
for i in (0..cnt).rev() {
|
||||||
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
|
let chat = Chat::load_from_db(&context, chatlist.get_chat_id(i)?).await?;
|
||||||
println!(
|
println!(
|
||||||
"{}#{}: {} [{} fresh] {}{}{}",
|
"{}#{}: {} [{} fresh] {}{}{}{}",
|
||||||
chat_prefix(&chat),
|
chat_prefix(&chat),
|
||||||
chat.get_id(),
|
chat.get_id(),
|
||||||
chat.get_name(),
|
chat.get_name(),
|
||||||
@@ -566,6 +607,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
ChatVisibility::Archived => "📦",
|
ChatVisibility::Archived => "📦",
|
||||||
ChatVisibility::Pinned => "📌",
|
ChatVisibility::Pinned => "📌",
|
||||||
},
|
},
|
||||||
|
if chat.is_protected() { "🛡️" } else { "" },
|
||||||
if chat.is_contact_request() {
|
if chat.is_contact_request() {
|
||||||
"🆕"
|
"🆕"
|
||||||
} else {
|
} else {
|
||||||
@@ -608,31 +650,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
println!("Location streaming enabled.");
|
println!("Location streaming enabled.");
|
||||||
}
|
}
|
||||||
println!("{cnt} chats");
|
println!("{cnt} chats");
|
||||||
eprintln!("{time_needed:?} to create this list");
|
println!("{time_needed:?} to create this list");
|
||||||
}
|
|
||||||
"start-realtime" => {
|
|
||||||
if arg1.is_empty() {
|
|
||||||
bail!("missing msgid");
|
|
||||||
}
|
|
||||||
let msg_id = MsgId::new(arg1.parse()?);
|
|
||||||
let res = send_webxdc_realtime_advertisement(&context, msg_id).await?;
|
|
||||||
|
|
||||||
if let Some(res) = res {
|
|
||||||
println!("waiting for peer channel join");
|
|
||||||
res.await?;
|
|
||||||
}
|
|
||||||
println!("joined peer channel");
|
|
||||||
}
|
|
||||||
"send-realtime" => {
|
|
||||||
if arg1.is_empty() {
|
|
||||||
bail!("missing msgid");
|
|
||||||
}
|
|
||||||
if arg2.is_empty() {
|
|
||||||
bail!("no message");
|
|
||||||
}
|
|
||||||
let msg_id = MsgId::new(arg1.parse()?);
|
|
||||||
send_webxdc_realtime_data(&context, msg_id, arg2.as_bytes().to_vec()).await?;
|
|
||||||
println!("sent realtime message");
|
|
||||||
}
|
}
|
||||||
"chat" => {
|
"chat" => {
|
||||||
if sel_chat.is_none() && arg1.is_empty() {
|
if sel_chat.is_none() && arg1.is_empty() {
|
||||||
@@ -680,7 +698,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
format!("{} member(s)", members.len())
|
format!("{} member(s)", members.len())
|
||||||
};
|
};
|
||||||
println!(
|
println!(
|
||||||
"{}#{}: {} [{}]{}{}{}",
|
"{}#{}: {} [{}]{}{}{} {}",
|
||||||
chat_prefix(sel_chat),
|
chat_prefix(sel_chat),
|
||||||
sel_chat.get_id(),
|
sel_chat.get_id(),
|
||||||
sel_chat.get_name(),
|
sel_chat.get_name(),
|
||||||
@@ -698,6 +716,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
},
|
},
|
||||||
_ => "".to_string(),
|
_ => "".to_string(),
|
||||||
},
|
},
|
||||||
|
if sel_chat.is_protected() {
|
||||||
|
"🛡️"
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
);
|
);
|
||||||
log_msglist(&context, &msglist).await?;
|
log_msglist(&context, &msglist).await?;
|
||||||
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
|
if let Some(draft) = sel_chat.get_id().get_draft(&context).await? {
|
||||||
@@ -713,7 +736,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
|
chat::marknoticed_chat(&context, sel_chat.get_id()).await?;
|
||||||
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
|
let time_noticed_needed = time_noticed_start.elapsed().unwrap_or_default();
|
||||||
|
|
||||||
eprintln!(
|
println!(
|
||||||
"{time_needed:?} to create this list, {time_noticed_needed:?} to mark all messages as noticed."
|
"{time_needed:?} to create this list, {time_noticed_needed:?} to mark all messages as noticed."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -726,16 +749,23 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
}
|
}
|
||||||
"creategroup" => {
|
"creategroup" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||||
let chat_id = chat::create_group(&context, arg1).await?;
|
let chat_id =
|
||||||
|
chat::create_group_chat(&context, ProtectionStatus::Unprotected, arg1).await?;
|
||||||
|
|
||||||
println!("Group#{chat_id} created successfully.");
|
println!("Group#{chat_id} created successfully.");
|
||||||
}
|
}
|
||||||
"createbroadcast" => {
|
"createbroadcast" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
let chat_id = chat::create_broadcast_list(&context).await?;
|
||||||
let chat_id = chat::create_broadcast(&context, arg1.to_string()).await?;
|
|
||||||
|
|
||||||
println!("Broadcast#{chat_id} created successfully.");
|
println!("Broadcast#{chat_id} created successfully.");
|
||||||
}
|
}
|
||||||
|
"createprotected" => {
|
||||||
|
ensure!(!arg1.is_empty(), "Argument <name> missing.");
|
||||||
|
let chat_id =
|
||||||
|
chat::create_group_chat(&context, ProtectionStatus::Protected, arg1).await?;
|
||||||
|
|
||||||
|
println!("Group#{chat_id} created and protected successfully.");
|
||||||
|
}
|
||||||
"addmember" => {
|
"addmember" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected");
|
ensure!(sel_chat.is_some(), "No chat selected");
|
||||||
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
ensure!(!arg1.is_empty(), "Argument <contact-id> missing.");
|
||||||
@@ -887,23 +917,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
|
|
||||||
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?;
|
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), msg).await?;
|
||||||
}
|
}
|
||||||
"send-sync" => {
|
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
|
||||||
ensure!(!arg1.is_empty(), "No message text given.");
|
|
||||||
|
|
||||||
// Send message over a dedicated SMTP connection
|
|
||||||
// and measure time.
|
|
||||||
//
|
|
||||||
// This can be used to benchmark SMTP connection establishment.
|
|
||||||
let time_start = std::time::Instant::now();
|
|
||||||
|
|
||||||
let msg = format!("{arg1} {arg2}");
|
|
||||||
let mut msg = Message::new_text(msg);
|
|
||||||
chat::send_msg_sync(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
|
||||||
|
|
||||||
let time_needed = time_start.elapsed();
|
|
||||||
println!("Sent message in {time_needed:?}.");
|
|
||||||
}
|
|
||||||
"sendempty" => {
|
"sendempty" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
|
chat::send_text_msg(&context, sel_chat.as_ref().unwrap().get_id(), "".into()).await?;
|
||||||
@@ -919,7 +932,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
} else {
|
} else {
|
||||||
Viewtype::File
|
Viewtype::File
|
||||||
});
|
});
|
||||||
msg.set_file_and_deduplicate(&context, Path::new(arg1), None, None)?;
|
msg.set_file(arg1, None);
|
||||||
msg.set_text(arg2.to_string());
|
msg.set_text(arg2.to_string());
|
||||||
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
chat::send_msg(&context, sel_chat.as_ref().unwrap().get_id(), &mut msg).await?;
|
||||||
}
|
}
|
||||||
@@ -949,7 +962,13 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
"Arguments <msg-id> <json status update> expected"
|
"Arguments <msg-id> <json status update> expected"
|
||||||
);
|
);
|
||||||
let msg_id = MsgId::new(arg1.parse()?);
|
let msg_id = MsgId::new(arg1.parse()?);
|
||||||
context.send_webxdc_status_update(msg_id, arg2).await?;
|
context
|
||||||
|
.send_webxdc_status_update(msg_id, arg2, "this is a webxdc status update")
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
"videochat" => {
|
||||||
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
|
chat::send_videochat_invitation(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
||||||
}
|
}
|
||||||
"listmsgs" => {
|
"listmsgs" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <query> missing.");
|
ensure!(!arg1.is_empty(), "Argument <query> missing.");
|
||||||
@@ -972,13 +991,14 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
},
|
},
|
||||||
query,
|
query,
|
||||||
);
|
);
|
||||||
eprintln!("{time_needed:?} to create this list");
|
println!("{time_needed:?} to create this list");
|
||||||
}
|
}
|
||||||
"draft" => {
|
"draft" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
|
|
||||||
if !arg1.is_empty() {
|
if !arg1.is_empty() {
|
||||||
let mut draft = Message::new_text(arg1.to_string());
|
let mut draft = Message::new(Viewtype::Text);
|
||||||
|
draft.set_text(arg1.to_string());
|
||||||
sel_chat
|
sel_chat
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -1001,7 +1021,8 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
!arg1.is_empty(),
|
!arg1.is_empty(),
|
||||||
"Please specify text to add as device message."
|
"Please specify text to add as device message."
|
||||||
);
|
);
|
||||||
let mut msg = Message::new_text(arg1.to_string());
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
|
msg.set_text(arg1.to_string());
|
||||||
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
|
chat::add_device_msg(&context, None, Some(&mut msg)).await?;
|
||||||
}
|
}
|
||||||
"listmedia" => {
|
"listmedia" => {
|
||||||
@@ -1135,13 +1156,19 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
let reaction = arg2;
|
let reaction = arg2;
|
||||||
send_reaction(&context, msg_id, reaction).await?;
|
send_reaction(&context, msg_id, reaction).await?;
|
||||||
}
|
}
|
||||||
"listcontacts" | "contacts" => {
|
"listcontacts" | "contacts" | "listverified" => {
|
||||||
let contacts = Contact::get_all(&context, DC_GCL_ADD_SELF, Some(arg1)).await?;
|
let contacts = Contact::get_all(
|
||||||
|
&context,
|
||||||
|
if arg0 == "listverified" {
|
||||||
|
DC_GCL_VERIFIED_ONLY | DC_GCL_ADD_SELF
|
||||||
|
} else {
|
||||||
|
DC_GCL_ADD_SELF
|
||||||
|
},
|
||||||
|
Some(arg1),
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
log_contactlist(&context, &contacts).await?;
|
log_contactlist(&context, &contacts).await?;
|
||||||
println!("{} key contacts.", contacts.len());
|
println!("{} contacts.", contacts.len());
|
||||||
let addrcontacts = Contact::get_all(&context, DC_GCL_ADDRESS, Some(arg1)).await?;
|
|
||||||
log_contactlist(&context, &addrcontacts).await?;
|
|
||||||
println!("{} address contacts.", addrcontacts.len());
|
|
||||||
}
|
}
|
||||||
"addcontact" => {
|
"addcontact" => {
|
||||||
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
|
ensure!(!arg1.is_empty(), "Arguments [<name>] <addr> expected.");
|
||||||
@@ -1205,24 +1232,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
log_contactlist(&context, &contacts).await?;
|
log_contactlist(&context, &contacts).await?;
|
||||||
println!("{} blocked contacts.", contacts.len());
|
println!("{} blocked contacts.", contacts.len());
|
||||||
}
|
}
|
||||||
"import-vcard" => {
|
|
||||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
|
||||||
let vcard_content = fs::read_to_string(&arg1.to_string()).await?;
|
|
||||||
let contacts = import_vcard(&context, &vcard_content).await?;
|
|
||||||
println!("vCard contacts imported:");
|
|
||||||
log_contactlist(&context, &contacts).await?;
|
|
||||||
}
|
|
||||||
"make-vcard" => {
|
|
||||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
|
||||||
ensure!(!arg2.is_empty(), "Argument <contact-id> missing.");
|
|
||||||
let mut contact_ids = vec![];
|
|
||||||
for x in arg2.split_whitespace() {
|
|
||||||
contact_ids.push(ContactId::new(x.parse()?))
|
|
||||||
}
|
|
||||||
let vcard_content = make_vcard(&context, &contact_ids).await?;
|
|
||||||
fs::write(&arg1.to_string(), vcard_content).await?;
|
|
||||||
println!("vCard written to: {arg1}");
|
|
||||||
}
|
|
||||||
"checkqr" => {
|
"checkqr" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||||
let qr = check_qr(&context, arg1).await?;
|
let qr = check_qr(&context, arg1).await?;
|
||||||
@@ -1232,19 +1241,15 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
||||||
match set_config_from_qr(&context, arg1).await {
|
match set_config_from_qr(&context, arg1).await {
|
||||||
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
|
Ok(()) => println!("Config set from QR code, you can now call 'configure'"),
|
||||||
Err(err) => eprintln!("Cannot set config from QR code: {err:?}"),
|
Err(err) => println!("Cannot set config from QR code: {err:?}"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"createqrsvg" => {
|
|
||||||
ensure!(!arg1.is_empty(), "Argument <qr-content> missing.");
|
|
||||||
let svg = create_qr_svg(arg1)?;
|
|
||||||
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
|
||||||
fs::write(&file, svg).await?;
|
|
||||||
println!("{file:#?} written.");
|
|
||||||
}
|
|
||||||
"providerinfo" => {
|
"providerinfo" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
|
ensure!(!arg1.is_empty(), "Argument <addr> missing.");
|
||||||
match provider::get_provider_info(arg1) {
|
let socks5_enabled = context
|
||||||
|
.get_config_bool(config::Config::Socks5Enabled)
|
||||||
|
.await?;
|
||||||
|
match provider::get_provider_info(&context, arg1, socks5_enabled).await {
|
||||||
Some(info) => {
|
Some(info) => {
|
||||||
println!("Information for provider belonging to {arg1}:");
|
println!("Information for provider belonging to {arg1}:");
|
||||||
println!("status: {}", info.status as u32);
|
println!("status: {}", info.status as u32);
|
||||||
@@ -1263,7 +1268,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
"fileinfo" => {
|
"fileinfo" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
ensure!(!arg1.is_empty(), "Argument <file> missing.");
|
||||||
|
|
||||||
if let Ok(buf) = read_file(&context, Path::new(arg1)).await {
|
if let Ok(buf) = read_file(&context, &arg1).await {
|
||||||
let (width, height) = get_filemeta(&buf)?;
|
let (width, height) = get_filemeta(&buf)?;
|
||||||
println!("width={width}, height={height}");
|
println!("width={width}, height={height}");
|
||||||
} else {
|
} else {
|
||||||
@@ -1280,7 +1285,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
"" => (),
|
"" => (),
|
||||||
_ => bail!("Unknown command: \"{arg0}\" type ? for help."),
|
_ => bail!("Unknown command: \"{}\" type ? for help.", arg0),
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,14 +1,17 @@
|
|||||||
#![recursion_limit = "256"]
|
|
||||||
//! This is a CLI program and a little testing frame. This file must not be
|
//! This is a CLI program and a little testing frame. This file must not be
|
||||||
//! included when using Delta Chat Core as a library.
|
//! included when using Delta Chat Core as a library.
|
||||||
//!
|
//!
|
||||||
//! Usage: cargo run --example repl --release -- <databasefile>
|
//! Usage: cargo run --example repl --release -- <databasefile>
|
||||||
//! All further options can be set using the set-command (type ? for help).
|
//! All further options can be set using the set-command (type ? for help).
|
||||||
|
|
||||||
|
#[macro_use]
|
||||||
extern crate deltachat;
|
extern crate deltachat;
|
||||||
|
|
||||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||||
|
use std::io::{self, Write};
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use ansi_term::Color;
|
||||||
use anyhow::{bail, Error};
|
use anyhow::{bail, Error};
|
||||||
use deltachat::chat::ChatId;
|
use deltachat::chat::ChatId;
|
||||||
use deltachat::config;
|
use deltachat::config;
|
||||||
@@ -18,10 +21,9 @@ use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
|||||||
use deltachat::securejoin::*;
|
use deltachat::securejoin::*;
|
||||||
use deltachat::EventType;
|
use deltachat::EventType;
|
||||||
use log::{error, info, warn};
|
use log::{error, info, warn};
|
||||||
use nu_ansi_term::Color;
|
|
||||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||||
use rustyline::error::ReadlineError;
|
use rustyline::error::ReadlineError;
|
||||||
use rustyline::highlight::{CmdKind as HighlightCmdKind, Highlighter, MatchingBracketHighlighter};
|
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
|
||||||
use rustyline::hint::{Hinter, HistoryHinter};
|
use rustyline::hint::{Hinter, HistoryHinter};
|
||||||
use rustyline::validate::Validator;
|
use rustyline::validate::Validator;
|
||||||
use rustyline::{
|
use rustyline::{
|
||||||
@@ -29,7 +31,6 @@ use rustyline::{
|
|||||||
};
|
};
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tokio::runtime::Handle;
|
use tokio::runtime::Handle;
|
||||||
use tracing_subscriber::EnvFilter;
|
|
||||||
|
|
||||||
mod cmdline;
|
mod cmdline;
|
||||||
use self::cmdline::*;
|
use self::cmdline::*;
|
||||||
@@ -40,25 +41,25 @@ fn receive_event(event: EventType) {
|
|||||||
match event {
|
match event {
|
||||||
EventType::Info(msg) => {
|
EventType::Info(msg) => {
|
||||||
/* do not show the event as this would fill the screen */
|
/* do not show the event as this would fill the screen */
|
||||||
info!("{msg}");
|
info!("{}", msg);
|
||||||
}
|
}
|
||||||
EventType::SmtpConnected(msg) => {
|
EventType::SmtpConnected(msg) => {
|
||||||
info!("[SMTP_CONNECTED] {msg}");
|
info!("[SMTP_CONNECTED] {}", msg);
|
||||||
}
|
}
|
||||||
EventType::ImapConnected(msg) => {
|
EventType::ImapConnected(msg) => {
|
||||||
info!("[IMAP_CONNECTED] {msg}");
|
info!("[IMAP_CONNECTED] {}", msg);
|
||||||
}
|
}
|
||||||
EventType::SmtpMessageSent(msg) => {
|
EventType::SmtpMessageSent(msg) => {
|
||||||
info!("[SMTP_MESSAGE_SENT] {msg}");
|
info!("[SMTP_MESSAGE_SENT] {}", msg);
|
||||||
}
|
}
|
||||||
EventType::Warning(msg) => {
|
EventType::Warning(msg) => {
|
||||||
warn!("{msg}");
|
warn!("{}", msg);
|
||||||
}
|
}
|
||||||
EventType::Error(msg) => {
|
EventType::Error(msg) => {
|
||||||
error!("{msg}");
|
error!("{}", msg);
|
||||||
}
|
}
|
||||||
EventType::ErrorSelfNotInGroup(msg) => {
|
EventType::ErrorSelfNotInGroup(msg) => {
|
||||||
error!("[SELF_NOT_IN_GROUP] {msg}");
|
error!("[SELF_NOT_IN_GROUP] {}", msg);
|
||||||
}
|
}
|
||||||
EventType::MsgsChanged { chat_id, msg_id } => {
|
EventType::MsgsChanged { chat_id, msg_id } => {
|
||||||
info!(
|
info!(
|
||||||
@@ -123,7 +124,7 @@ fn receive_event(event: EventType) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
info!("Received {event:?}");
|
info!("Received {:?}", event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,7 +150,7 @@ impl Completer for DcHelper {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const IMEX_COMMANDS: [&str; 13] = [
|
const IMEX_COMMANDS: [&str; 14] = [
|
||||||
"initiate-key-transfer",
|
"initiate-key-transfer",
|
||||||
"get-setupcodebegin",
|
"get-setupcodebegin",
|
||||||
"continue-key-transfer",
|
"continue-key-transfer",
|
||||||
@@ -160,12 +161,13 @@ const IMEX_COMMANDS: [&str; 13] = [
|
|||||||
"receive-backup",
|
"receive-backup",
|
||||||
"export-keys",
|
"export-keys",
|
||||||
"import-keys",
|
"import-keys",
|
||||||
|
"export-setup",
|
||||||
"poke",
|
"poke",
|
||||||
"reset",
|
"reset",
|
||||||
"stop",
|
"stop",
|
||||||
];
|
];
|
||||||
|
|
||||||
const DB_COMMANDS: [&str; 11] = [
|
const DB_COMMANDS: [&str; 10] = [
|
||||||
"info",
|
"info",
|
||||||
"set",
|
"set",
|
||||||
"get",
|
"get",
|
||||||
@@ -173,17 +175,14 @@ const DB_COMMANDS: [&str; 11] = [
|
|||||||
"configure",
|
"configure",
|
||||||
"connect",
|
"connect",
|
||||||
"disconnect",
|
"disconnect",
|
||||||
"fetch",
|
|
||||||
"connectivity",
|
"connectivity",
|
||||||
"maybenetwork",
|
"maybenetwork",
|
||||||
"housekeeping",
|
"housekeeping",
|
||||||
];
|
];
|
||||||
|
|
||||||
const CHAT_COMMANDS: [&str; 39] = [
|
const CHAT_COMMANDS: [&str; 36] = [
|
||||||
"listchats",
|
"listchats",
|
||||||
"listarchived",
|
"listarchived",
|
||||||
"start-realtime",
|
|
||||||
"send-realtime",
|
|
||||||
"chat",
|
"chat",
|
||||||
"createchat",
|
"createchat",
|
||||||
"creategroup",
|
"creategroup",
|
||||||
@@ -199,16 +198,13 @@ const CHAT_COMMANDS: [&str; 39] = [
|
|||||||
"dellocations",
|
"dellocations",
|
||||||
"getlocations",
|
"getlocations",
|
||||||
"send",
|
"send",
|
||||||
"send-sync",
|
|
||||||
"sendempty",
|
|
||||||
"sendimage",
|
"sendimage",
|
||||||
"sendsticker",
|
|
||||||
"sendfile",
|
"sendfile",
|
||||||
"sendhtml",
|
"sendhtml",
|
||||||
"sendsyncmsg",
|
"sendsyncmsg",
|
||||||
"sendupdate",
|
"sendupdate",
|
||||||
|
"videochat",
|
||||||
"draft",
|
"draft",
|
||||||
"devicemsg",
|
|
||||||
"listmedia",
|
"listmedia",
|
||||||
"archive",
|
"archive",
|
||||||
"unarchive",
|
"unarchive",
|
||||||
@@ -216,48 +212,46 @@ const CHAT_COMMANDS: [&str; 39] = [
|
|||||||
"unpin",
|
"unpin",
|
||||||
"mute",
|
"mute",
|
||||||
"unmute",
|
"unmute",
|
||||||
|
"protect",
|
||||||
|
"unprotect",
|
||||||
"delchat",
|
"delchat",
|
||||||
"accept",
|
"accept",
|
||||||
"blockchat",
|
"blockchat",
|
||||||
];
|
];
|
||||||
const MESSAGE_COMMANDS: [&str; 10] = [
|
const MESSAGE_COMMANDS: [&str; 9] = [
|
||||||
"listmsgs",
|
"listmsgs",
|
||||||
"msginfo",
|
"msginfo",
|
||||||
"download",
|
|
||||||
"html",
|
|
||||||
"listfresh",
|
"listfresh",
|
||||||
"forward",
|
"forward",
|
||||||
"resend",
|
"resend",
|
||||||
"markseen",
|
"markseen",
|
||||||
"delmsg",
|
"delmsg",
|
||||||
|
"download",
|
||||||
"react",
|
"react",
|
||||||
];
|
];
|
||||||
const CONTACT_COMMANDS: [&str; 9] = [
|
const CONTACT_COMMANDS: [&str; 9] = [
|
||||||
"listcontacts",
|
"listcontacts",
|
||||||
|
"listverified",
|
||||||
"addcontact",
|
"addcontact",
|
||||||
"contactinfo",
|
"contactinfo",
|
||||||
"delcontact",
|
"delcontact",
|
||||||
|
"cleanupcontacts",
|
||||||
"block",
|
"block",
|
||||||
"unblock",
|
"unblock",
|
||||||
"listblocked",
|
"listblocked",
|
||||||
"import-vcard",
|
|
||||||
"make-vcard",
|
|
||||||
];
|
];
|
||||||
const MISC_COMMANDS: [&str; 14] = [
|
const MISC_COMMANDS: [&str; 11] = [
|
||||||
"getqr",
|
"getqr",
|
||||||
"getqrsvg",
|
"getqrsvg",
|
||||||
"getbadqr",
|
"getbadqr",
|
||||||
"checkqr",
|
"checkqr",
|
||||||
"joinqr",
|
"joinqr",
|
||||||
"setqr",
|
|
||||||
"createqrsvg",
|
|
||||||
"providerinfo",
|
|
||||||
"fileinfo",
|
"fileinfo",
|
||||||
"estimatedeletion",
|
|
||||||
"clear",
|
"clear",
|
||||||
"exit",
|
"exit",
|
||||||
"quit",
|
"quit",
|
||||||
"help",
|
"help",
|
||||||
|
"estimatedeletion",
|
||||||
];
|
];
|
||||||
|
|
||||||
impl Hinter for DcHelper {
|
impl Hinter for DcHelper {
|
||||||
@@ -303,8 +297,8 @@ impl Highlighter for DcHelper {
|
|||||||
self.highlighter.highlight(line, pos)
|
self.highlighter.highlight(line, pos)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn highlight_char(&self, line: &str, pos: usize, kind: HighlightCmdKind) -> bool {
|
fn highlight_char(&self, line: &str, pos: usize, forced: bool) -> bool {
|
||||||
self.highlighter.highlight_char(line, pos, kind)
|
self.highlighter.highlight_char(line, pos, forced)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,7 +307,7 @@ impl Validator for DcHelper {}
|
|||||||
|
|
||||||
async fn start(args: Vec<String>) -> Result<(), Error> {
|
async fn start(args: Vec<String>) -> Result<(), Error> {
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
eprintln!("Error: Bad arguments, expected [db-name].");
|
println!("Error: Bad arguments, expected [db-name].");
|
||||||
bail!("No db-name specified");
|
bail!("No db-name specified");
|
||||||
}
|
}
|
||||||
let context = ContextBuilder::new(args[1].clone().into())
|
let context = ContextBuilder::new(args[1].clone().into())
|
||||||
@@ -328,7 +322,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
println!("Chatmail is awaiting your commands.");
|
println!("Delta Chat Core is awaiting your commands.");
|
||||||
|
|
||||||
let config = Config::builder()
|
let config = Config::builder()
|
||||||
.history_ignore_space(true)
|
.history_ignore_space(true)
|
||||||
@@ -368,7 +362,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
|||||||
false
|
false
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error: {err:#}");
|
println!("Error: {err:#}");
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -383,7 +377,7 @@ async fn start(args: Vec<String>) -> Result<(), Error> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
eprintln!("Error: {err:#}");
|
println!("Error: {err:#}");
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -422,9 +416,6 @@ async fn handle_cmd(
|
|||||||
"disconnect" => {
|
"disconnect" => {
|
||||||
ctx.stop_io().await;
|
ctx.stop_io().await;
|
||||||
}
|
}
|
||||||
"fetch" => {
|
|
||||||
ctx.background_fetch().await?;
|
|
||||||
}
|
|
||||||
"configure" => {
|
"configure" => {
|
||||||
ctx.configure().await?;
|
ctx.configure().await?;
|
||||||
}
|
}
|
||||||
@@ -454,7 +445,12 @@ async fn handle_cmd(
|
|||||||
qr.replace_range(12..22, "0000000000")
|
qr.replace_range(12..22, "0000000000")
|
||||||
}
|
}
|
||||||
println!("{qr}");
|
println!("{qr}");
|
||||||
qr2term::print_qr(qr.as_str())?;
|
let output = Command::new("qrencode")
|
||||||
|
.args(["-t", "ansiutf8", qr.as_str(), "-o", "-"])
|
||||||
|
.output()
|
||||||
|
.expect("failed to execute process");
|
||||||
|
io::stdout().write_all(&output.stdout).unwrap();
|
||||||
|
io::stderr().write_all(&output.stderr).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"getqrsvg" => {
|
"getqrsvg" => {
|
||||||
@@ -467,7 +463,7 @@ async fn handle_cmd(
|
|||||||
println!("QR code svg written to: {file:#?}");
|
println!("QR code svg written to: {file:#?}");
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
bail!("Failed to get QR code svg: {err}");
|
bail!("Failed to get QR code svg: {}", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -486,10 +482,9 @@ async fn handle_cmd(
|
|||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() -> Result<(), Error> {
|
async fn main() -> Result<(), Error> {
|
||||||
tracing_subscriber::fmt()
|
pretty_env_logger::formatted_timed_builder()
|
||||||
.with_env_filter(
|
.parse_default_env()
|
||||||
EnvFilter::from_default_env().add_directive("deltachat_repl=info".parse()?),
|
.filter_module("deltachat_repl", log::LevelFilter::Info)
|
||||||
)
|
|
||||||
.init();
|
.init();
|
||||||
|
|
||||||
let args = std::env::args().collect();
|
let args = std::env::args().collect();
|
||||||
|
|||||||
@@ -25,20 +25,10 @@ $ pip install .
|
|||||||
## Testing
|
## Testing
|
||||||
|
|
||||||
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
1. Build `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
||||||
2. Install tox `pip install -U tox`
|
2. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
|
||||||
3. Run `CHATMAIL_DOMAIN=nine.testrun.org PATH="../target/debug:$PATH" tox`.
|
|
||||||
|
|
||||||
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
|
Additional arguments to `tox` are passed to pytest, e.g. `tox -- -s` does not capture test output.
|
||||||
|
|
||||||
|
|
||||||
## Activating current checkout of deltachat-rpc-client and -server for development
|
|
||||||
|
|
||||||
Go to root repository directory and run:
|
|
||||||
```
|
|
||||||
$ scripts/make-rpc-testenv.sh
|
|
||||||
$ source venv/bin/activate
|
|
||||||
```
|
|
||||||
|
|
||||||
## Using in REPL
|
## Using in REPL
|
||||||
|
|
||||||
Setup a development environment:
|
Setup a development environment:
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
it will echo back any text send to it, it also will print to console all Delta Chat core events.
|
it will echo back any text send to it, it also will print to console all Delta Chat core events.
|
||||||
Pass --help to the CLI to see available options.
|
Pass --help to the CLI to see available options.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from deltachat_rpc_client import events, run_bot_cli
|
from deltachat_rpc_client import events, run_bot_cli
|
||||||
|
|
||||||
hooks = events.HookCollection()
|
hooks = events.HookCollection()
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
it will echo back any message that has non-empty text and also supports the /help command.
|
it will echo back any message that has non-empty text and also supports the /help command.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
"""
|
"""
|
||||||
Example echo bot without using hooks
|
Example echo bot without using hooks
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
[build-system]
|
[build-system]
|
||||||
requires = ["setuptools>=77"]
|
requires = ["setuptools>=45"]
|
||||||
build-backend = "setuptools.build_meta"
|
build-backend = "setuptools.build_meta"
|
||||||
|
|
||||||
[project]
|
[project]
|
||||||
name = "deltachat-rpc-client"
|
name = "deltachat-rpc-client"
|
||||||
version = "2.31.0"
|
version = "1.137.2"
|
||||||
license = "MPL-2.0"
|
|
||||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 5 - Production/Stable",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||||
"Operating System :: POSIX :: Linux",
|
"Operating System :: POSIX :: Linux",
|
||||||
"Operating System :: MacOS :: MacOS X",
|
"Operating System :: MacOS :: MacOS X",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
"Programming Language :: Python :: 3.10",
|
"Programming Language :: Python :: 3.10",
|
||||||
"Programming Language :: Python :: 3.11",
|
"Programming Language :: Python :: 3.11",
|
||||||
"Programming Language :: Python :: 3.12",
|
|
||||||
"Programming Language :: Python :: 3.13",
|
|
||||||
"Programming Language :: Python :: 3.14",
|
|
||||||
"Topic :: Communications :: Chat",
|
"Topic :: Communications :: Chat",
|
||||||
"Topic :: Communications :: Email"
|
"Topic :: Communications :: Email"
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.10"
|
|
||||||
|
|
||||||
[tool.setuptools.package-data]
|
[tool.setuptools.package-data]
|
||||||
deltachat_rpc_client = [
|
deltachat_rpc_client = [
|
||||||
@@ -36,7 +34,7 @@ deltachat_rpc_client = [
|
|||||||
line-length = 120
|
line-length = 120
|
||||||
|
|
||||||
[tool.ruff]
|
[tool.ruff]
|
||||||
lint.select = [
|
select = [
|
||||||
"E", "W", # pycodestyle
|
"E", "W", # pycodestyle
|
||||||
"F", # Pyflakes
|
"F", # Pyflakes
|
||||||
"N", # pep8-naming
|
"N", # pep8-naming
|
||||||
@@ -65,18 +63,7 @@ lint.select = [
|
|||||||
|
|
||||||
"RUF006" # asyncio-dangling-task
|
"RUF006" # asyncio-dangling-task
|
||||||
]
|
]
|
||||||
lint.ignore = [
|
|
||||||
"PLC0415" # `import` should be at the top-level of a file
|
|
||||||
]
|
|
||||||
line-length = 120
|
line-length = 120
|
||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
profile = "black"
|
profile = "black"
|
||||||
|
|
||||||
[dependency-groups]
|
|
||||||
dev = [
|
|
||||||
"imap-tools",
|
|
||||||
"pytest",
|
|
||||||
"pytest-timeout",
|
|
||||||
"pytest-xdist",
|
|
||||||
]
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Delta Chat JSON-RPC high-level API."""
|
"""Delta Chat JSON-RPC high-level API"""
|
||||||
|
|
||||||
from ._utils import AttrDict, run_bot_cli, run_client_cli
|
from ._utils import AttrDict, run_bot_cli, run_client_cli
|
||||||
from .account import Account
|
from .account import Account
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import os
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
@@ -90,8 +89,8 @@ def _run_cli(
|
|||||||
help="accounts folder (default: current working directory)",
|
help="accounts folder (default: current working directory)",
|
||||||
nargs="?",
|
nargs="?",
|
||||||
)
|
)
|
||||||
parser.add_argument("--email", action="store", help="email address", default=os.getenv("DELTACHAT_EMAIL"))
|
parser.add_argument("--email", action="store", help="email address")
|
||||||
parser.add_argument("--password", action="store", help="password", default=os.getenv("DELTACHAT_PASSWORD"))
|
parser.add_argument("--password", action="store", help="password")
|
||||||
args = parser.parse_args(argv[1:])
|
args = parser.parse_args(argv[1:])
|
||||||
|
|
||||||
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
|
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
|
||||||
@@ -105,17 +104,13 @@ def _run_cli(
|
|||||||
if not client.is_configured():
|
if not client.is_configured():
|
||||||
assert args.email, "Account is not configured and email must be provided"
|
assert args.email, "Account is not configured and email must be provided"
|
||||||
assert args.password, "Account is not configured and password must be provided"
|
assert args.password, "Account is not configured and password must be provided"
|
||||||
configure_thread = Thread(
|
configure_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password})
|
||||||
target=client.configure,
|
|
||||||
daemon=True,
|
|
||||||
kwargs={"email": args.email, "password": args.password},
|
|
||||||
)
|
|
||||||
configure_thread.start()
|
configure_thread.start()
|
||||||
client.run_forever()
|
client.run_forever()
|
||||||
|
|
||||||
|
|
||||||
def extract_addr(text: str) -> str:
|
def extract_addr(text: str) -> str:
|
||||||
"""Extract email address from the given text."""
|
"""extract email address from the given text."""
|
||||||
match = re.match(r".*\((.+@.+)\)", text)
|
match = re.match(r".*\((.+@.+)\)", text)
|
||||||
if match:
|
if match:
|
||||||
text = match.group(1)
|
text = match.group(1)
|
||||||
@@ -124,7 +119,7 @@ def extract_addr(text: str) -> str:
|
|||||||
|
|
||||||
|
|
||||||
def parse_system_image_changed(text: str) -> Optional[Tuple[str, bool]]:
|
def parse_system_image_changed(text: str) -> Optional[Tuple[str, bool]]:
|
||||||
"""Return image changed/deleted info from parsing the given system message text."""
|
"""return image changed/deleted info from parsing the given system message text."""
|
||||||
text = text.lower()
|
text = text.lower()
|
||||||
match = re.match(r"group image (changed|deleted) by (.+).", text)
|
match = re.match(r"group image (changed|deleted) by (.+).", text)
|
||||||
if match:
|
if match:
|
||||||
@@ -143,7 +138,7 @@ def parse_system_title_changed(text: str) -> Optional[Tuple[str, str]]:
|
|||||||
|
|
||||||
|
|
||||||
def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
|
def parse_system_add_remove(text: str) -> Optional[Tuple[str, str, str]]:
|
||||||
"""Return add/remove info from parsing the given system message text.
|
"""return add/remove info from parsing the given system message text.
|
||||||
|
|
||||||
returns a (action, affected, actor) tuple.
|
returns a (action, affected, actor) tuple.
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
"""Account module."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import json
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Optional, Union
|
from typing import TYPE_CHECKING, List, Optional, Tuple, Union
|
||||||
from warnings import warn
|
from warnings import warn
|
||||||
|
|
||||||
from ._utils import AttrDict, futuremethod
|
from ._utils import AttrDict, futuremethod
|
||||||
@@ -29,36 +24,14 @@ class Account:
|
|||||||
def _rpc(self) -> "Rpc":
|
def _rpc(self) -> "Rpc":
|
||||||
return self.manager.rpc
|
return self.manager.rpc
|
||||||
|
|
||||||
def wait_for_event(self, event_type=None) -> AttrDict:
|
def wait_for_event(self) -> AttrDict:
|
||||||
"""Wait until the next event and return it."""
|
"""Wait until the next event and return it."""
|
||||||
while True:
|
return AttrDict(self._rpc.wait_for_event(self.id))
|
||||||
next_event = AttrDict(self._rpc.wait_for_event(self.id))
|
|
||||||
if event_type is None or next_event.kind == event_type:
|
|
||||||
return next_event
|
|
||||||
|
|
||||||
def clear_all_events(self):
|
|
||||||
"""Remove all queued-up events for a given account.
|
|
||||||
|
|
||||||
Useful for tests.
|
|
||||||
"""
|
|
||||||
self._rpc.clear_all_events(self.id)
|
|
||||||
|
|
||||||
def remove(self) -> None:
|
def remove(self) -> None:
|
||||||
"""Remove the account."""
|
"""Remove the account."""
|
||||||
self._rpc.remove_account(self.id)
|
self._rpc.remove_account(self.id)
|
||||||
|
|
||||||
def clone(self) -> "Account":
|
|
||||||
"""Clone given account.
|
|
||||||
|
|
||||||
This uses backup-transfer via iroh, i.e. the 'Add second device' feature.
|
|
||||||
"""
|
|
||||||
future = self._rpc.provide_backup.future(self.id)
|
|
||||||
qr = self._rpc.get_backup_qr(self.id)
|
|
||||||
new_account = self.manager.add_account()
|
|
||||||
new_account._rpc.get_backup(new_account.id, qr)
|
|
||||||
future()
|
|
||||||
return new_account
|
|
||||||
|
|
||||||
def start_io(self) -> None:
|
def start_io(self) -> None:
|
||||||
"""Start the account I/O."""
|
"""Start the account I/O."""
|
||||||
self._rpc.start_io(self.id)
|
self._rpc.start_io(self.id)
|
||||||
@@ -88,7 +61,7 @@ class Account:
|
|||||||
return self._rpc.get_config(self.id, key)
|
return self._rpc.get_config(self.id, key)
|
||||||
|
|
||||||
def update_config(self, **kwargs) -> None:
|
def update_config(self, **kwargs) -> None:
|
||||||
"""Update config values."""
|
"""update config values."""
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
self.set_config(key, value)
|
self.set_config(key, value)
|
||||||
|
|
||||||
@@ -104,15 +77,9 @@ class Account:
|
|||||||
return self.get_config("selfavatar")
|
return self.get_config("selfavatar")
|
||||||
|
|
||||||
def check_qr(self, qr):
|
def check_qr(self, qr):
|
||||||
"""Parse QR code contents.
|
|
||||||
|
|
||||||
This function takes the raw text scanned
|
|
||||||
and checks what can be done with it.
|
|
||||||
"""
|
|
||||||
return self._rpc.check_qr(self.id, qr)
|
return self._rpc.check_qr(self.id, qr)
|
||||||
|
|
||||||
def set_config_from_qr(self, qr: str):
|
def set_config_from_qr(self, qr: str):
|
||||||
"""Set configuration values from a QR code."""
|
|
||||||
self._rpc.set_config_from_qr(self.id, qr)
|
self._rpc.set_config_from_qr(self.id, qr)
|
||||||
|
|
||||||
@futuremethod
|
@futuremethod
|
||||||
@@ -120,32 +87,7 @@ class Account:
|
|||||||
"""Configure an account."""
|
"""Configure an account."""
|
||||||
yield self._rpc.configure.future(self.id)
|
yield self._rpc.configure.future(self.id)
|
||||||
|
|
||||||
@futuremethod
|
def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
||||||
def add_or_update_transport(self, params):
|
|
||||||
"""Add a new transport."""
|
|
||||||
yield self._rpc.add_or_update_transport.future(self.id, params)
|
|
||||||
|
|
||||||
@futuremethod
|
|
||||||
def add_transport_from_qr(self, qr: str):
|
|
||||||
"""Add a new transport using a QR code."""
|
|
||||||
yield self._rpc.add_transport_from_qr.future(self.id, qr)
|
|
||||||
|
|
||||||
def delete_transport(self, addr: str):
|
|
||||||
"""Delete a transport."""
|
|
||||||
self._rpc.delete_transport(self.id, addr)
|
|
||||||
|
|
||||||
@futuremethod
|
|
||||||
def list_transports(self):
|
|
||||||
"""Return the list of all email accounts that are used as a transport in the current profile."""
|
|
||||||
transports = yield self._rpc.list_transports.future(self.id)
|
|
||||||
return transports
|
|
||||||
|
|
||||||
def bring_online(self):
|
|
||||||
"""Start I/O and wait until IMAP becomes IDLE."""
|
|
||||||
self.start_io()
|
|
||||||
self.wait_for_event(EventType.IMAP_INBOX_IDLE)
|
|
||||||
|
|
||||||
def create_contact(self, obj: Union[int, str, Contact, "Account"], name: Optional[str] = None) -> Contact:
|
|
||||||
"""Create a new Contact or return an existing one.
|
"""Create a new Contact or return an existing one.
|
||||||
|
|
||||||
Calling this method will always result in the same
|
Calling this method will always result in the same
|
||||||
@@ -153,67 +95,25 @@ class Account:
|
|||||||
with that e-mail address, it is unblocked and its display
|
with that e-mail address, it is unblocked and its display
|
||||||
name is updated if specified.
|
name is updated if specified.
|
||||||
|
|
||||||
:param obj: email-address, contact id or account.
|
:param obj: email-address or contact id.
|
||||||
:param name: (optional) display name for this contact.
|
:param name: (optional) display name for this contact.
|
||||||
"""
|
"""
|
||||||
if isinstance(obj, Account):
|
|
||||||
vcard = obj.self_contact.make_vcard()
|
|
||||||
[contact] = self.import_vcard(vcard)
|
|
||||||
if name:
|
|
||||||
contact.set_name(name)
|
|
||||||
return contact
|
|
||||||
if isinstance(obj, int):
|
if isinstance(obj, int):
|
||||||
obj = Contact(self, obj)
|
obj = Contact(self, obj)
|
||||||
if isinstance(obj, Contact):
|
if isinstance(obj, Contact):
|
||||||
obj = obj.get_snapshot().address
|
obj = obj.get_snapshot().address
|
||||||
return Contact(self, self._rpc.create_contact(self.id, obj, name))
|
return Contact(self, self._rpc.create_contact(self.id, obj, name))
|
||||||
|
|
||||||
def make_vcard(self, contacts: list[Contact]) -> str:
|
|
||||||
"""Create vCard with the given contacts."""
|
|
||||||
assert all(contact.account == self for contact in contacts)
|
|
||||||
contact_ids = [contact.id for contact in contacts]
|
|
||||||
return self._rpc.make_vcard(self.id, contact_ids)
|
|
||||||
|
|
||||||
def import_vcard(self, vcard: str) -> list[Contact]:
|
|
||||||
"""Import vCard.
|
|
||||||
|
|
||||||
Return created or modified contacts in the order they appear in vCard.
|
|
||||||
"""
|
|
||||||
contact_ids = self._rpc.import_vcard_contents(self.id, vcard)
|
|
||||||
return [Contact(self, contact_id) for contact_id in contact_ids]
|
|
||||||
|
|
||||||
def create_chat(self, account: "Account") -> Chat:
|
|
||||||
"""Create a 1:1 chat with another account."""
|
|
||||||
return self.create_contact(account).create_chat()
|
|
||||||
|
|
||||||
def get_device_chat(self) -> Chat:
|
|
||||||
"""Return device chat."""
|
|
||||||
return self.device_contact.create_chat()
|
|
||||||
|
|
||||||
def get_contact_by_id(self, contact_id: int) -> Contact:
|
def get_contact_by_id(self, contact_id: int) -> Contact:
|
||||||
"""Return Contact instance for the given contact ID."""
|
"""Return Contact instance for the given contact ID."""
|
||||||
return Contact(self, contact_id)
|
return Contact(self, contact_id)
|
||||||
|
|
||||||
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
||||||
"""Looks up a known and unblocked contact with a given e-mail address.
|
"""Check if an e-mail address belongs to a known and unblocked contact."""
|
||||||
To get a list of all known and unblocked contacts, use contacts_get_contacts().
|
|
||||||
|
|
||||||
**POTENTIAL SECURITY ISSUE**: If there are multiple contacts with this address
|
|
||||||
(e.g. an address-contact and a key-contact),
|
|
||||||
this looks up the most recently seen contact,
|
|
||||||
i.e. which contact is returned depends on which contact last sent a message.
|
|
||||||
If the user just clicked on a mailto: link, then this is the best thing you can do.
|
|
||||||
But **DO NOT** internally represent contacts by their email address
|
|
||||||
and do not use this function to look them up;
|
|
||||||
otherwise this function will sometimes look up the wrong contact.
|
|
||||||
Instead, you should internally represent contacts by their ids.
|
|
||||||
|
|
||||||
To validate an e-mail address independently of the contact database
|
|
||||||
use check_email_validity()."""
|
|
||||||
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
contact_id = self._rpc.lookup_contact_id_by_addr(self.id, address)
|
||||||
return contact_id and Contact(self, contact_id)
|
return contact_id and Contact(self, contact_id)
|
||||||
|
|
||||||
def get_blocked_contacts(self) -> list[AttrDict]:
|
def get_blocked_contacts(self) -> List[AttrDict]:
|
||||||
"""Return a list with snapshots of all blocked contacts."""
|
"""Return a list with snapshots of all blocked contacts."""
|
||||||
contacts = self._rpc.get_blocked_contacts(self.id)
|
contacts = self._rpc.get_blocked_contacts(self.id)
|
||||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||||
@@ -235,18 +135,21 @@ class Account:
|
|||||||
def get_contacts(
|
def get_contacts(
|
||||||
self,
|
self,
|
||||||
query: Optional[str] = None,
|
query: Optional[str] = None,
|
||||||
*,
|
|
||||||
with_self: bool = False,
|
with_self: bool = False,
|
||||||
|
verified_only: bool = False,
|
||||||
snapshot: bool = False,
|
snapshot: bool = False,
|
||||||
) -> Union[list[Contact], list[AttrDict]]:
|
) -> Union[List[Contact], List[AttrDict]]:
|
||||||
"""Get a filtered list of contacts.
|
"""Get a filtered list of contacts.
|
||||||
|
|
||||||
:param query: if a string is specified, only return contacts
|
:param query: if a string is specified, only return contacts
|
||||||
whose name or e-mail matches query.
|
whose name or e-mail matches query.
|
||||||
:param with_self: if True the self-contact is also included if it matches the query.
|
:param with_self: if True the self-contact is also included if it matches the query.
|
||||||
|
:param only_verified: if True only return verified contacts.
|
||||||
:param snapshot: If True return a list of contact snapshots instead of Contact instances.
|
:param snapshot: If True return a list of contact snapshots instead of Contact instances.
|
||||||
"""
|
"""
|
||||||
flags = 0
|
flags = 0
|
||||||
|
if verified_only:
|
||||||
|
flags |= ContactFlag.VERIFIED_ONLY
|
||||||
if with_self:
|
if with_self:
|
||||||
flags |= ContactFlag.ADD_SELF
|
flags |= ContactFlag.ADD_SELF
|
||||||
|
|
||||||
@@ -258,14 +161,9 @@ class Account:
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def self_contact(self) -> Contact:
|
def self_contact(self) -> Contact:
|
||||||
"""Account's identity as a Contact."""
|
"""This account's identity as a Contact."""
|
||||||
return Contact(self, SpecialContactId.SELF)
|
return Contact(self, SpecialContactId.SELF)
|
||||||
|
|
||||||
@property
|
|
||||||
def device_contact(self) -> Chat:
|
|
||||||
"""Account's device contact."""
|
|
||||||
return Contact(self, SpecialContactId.DEVICE)
|
|
||||||
|
|
||||||
def get_chatlist(
|
def get_chatlist(
|
||||||
self,
|
self,
|
||||||
query: Optional[str] = None,
|
query: Optional[str] = None,
|
||||||
@@ -275,7 +173,7 @@ class Account:
|
|||||||
no_specials: bool = False,
|
no_specials: bool = False,
|
||||||
alldone_hint: bool = False,
|
alldone_hint: bool = False,
|
||||||
snapshot: bool = False,
|
snapshot: bool = False,
|
||||||
) -> Union[list[Chat], list[AttrDict]]:
|
) -> Union[List[Chat], List[AttrDict]]:
|
||||||
"""Return list of chats.
|
"""Return list of chats.
|
||||||
|
|
||||||
:param query: if a string is specified only chats matching this query are returned.
|
:param query: if a string is specified only chats matching this query are returned.
|
||||||
@@ -309,86 +207,51 @@ class Account:
|
|||||||
chats.append(AttrDict(item))
|
chats.append(AttrDict(item))
|
||||||
return chats
|
return chats
|
||||||
|
|
||||||
def create_group(self, name: str) -> Chat:
|
def create_group(self, name: str, protect: bool = False) -> Chat:
|
||||||
"""Create a new group chat.
|
"""Create a new group chat.
|
||||||
|
|
||||||
After creation,
|
After creation, the group has only self-contact as member and is in unpromoted state.
|
||||||
the group has only self-contact as member one member (see `SpecialContactId.SELF`)
|
|
||||||
and is in _unpromoted_ state.
|
|
||||||
This means, you can add or remove members, change the name,
|
|
||||||
the group image and so on without messages being sent to all group members.
|
|
||||||
|
|
||||||
This changes as soon as the first message is sent to the group members
|
|
||||||
and the group becomes _promoted_.
|
|
||||||
After that, all changes are synced with all group members
|
|
||||||
by sending status message.
|
|
||||||
|
|
||||||
To check, if a chat is still unpromoted, you can look at the `is_unpromoted` property of a chat
|
|
||||||
(see `get_full_snapshot()` / `get_basic_snapshot()`).
|
|
||||||
This may be useful if you want to show some help for just created groups.
|
|
||||||
"""
|
"""
|
||||||
return Chat(self, self._rpc.create_group_chat(self.id, name, False))
|
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
|
||||||
|
|
||||||
def create_broadcast(self, name: str) -> Chat:
|
|
||||||
"""Create a new, outgoing **broadcast channel**
|
|
||||||
(called "Channel" in the UI).
|
|
||||||
|
|
||||||
Broadcast channels are similar to groups on the sending device,
|
|
||||||
however, recipients get the messages in a read-only chat
|
|
||||||
and will not see who the other members are.
|
|
||||||
|
|
||||||
Called `broadcast` here rather than `channel`,
|
|
||||||
because the word "channel" already appears a lot in the code,
|
|
||||||
which would make it hard to grep for it.
|
|
||||||
|
|
||||||
After creation, the chat contains no recipients and is in _unpromoted_ state;
|
|
||||||
see `create_group()` for more information on the unpromoted state.
|
|
||||||
|
|
||||||
Returns the created chat.
|
|
||||||
"""
|
|
||||||
return Chat(self, self._rpc.create_broadcast(self.id, name))
|
|
||||||
|
|
||||||
def get_chat_by_id(self, chat_id: int) -> Chat:
|
def get_chat_by_id(self, chat_id: int) -> Chat:
|
||||||
"""Return the Chat instance with the given ID."""
|
"""Return the Chat instance with the given ID."""
|
||||||
return Chat(self, chat_id)
|
return Chat(self, chat_id)
|
||||||
|
|
||||||
def secure_join(self, qrdata: str) -> Chat:
|
def secure_join(self, qrdata: str) -> Chat:
|
||||||
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on another device.
|
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on
|
||||||
|
another device.
|
||||||
|
|
||||||
The function returns immediately and the handshake runs in background, sending
|
The function returns immediately and the handshake runs in background, sending
|
||||||
and receiving several messages.
|
and receiving several messages.
|
||||||
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
Subsequent calls of `secure_join()` will abort previous, unfinished handshakes.
|
||||||
See https://securejoin.delta.chat/ for protocol details.
|
See https://securejoin.readthedocs.io/en/latest/new.html for protocol details.
|
||||||
|
|
||||||
:param qrdata: The text of the scanned QR code.
|
:param qrdata: The text of the scanned QR code.
|
||||||
"""
|
"""
|
||||||
return Chat(self, self._rpc.secure_join(self.id, qrdata))
|
return Chat(self, self._rpc.secure_join(self.id, qrdata))
|
||||||
|
|
||||||
def get_qr_code(self) -> str:
|
def get_qr_code(self) -> Tuple[str, str]:
|
||||||
"""Get Setup-Contact QR Code text.
|
"""Get Setup-Contact QR Code text and SVG data.
|
||||||
|
|
||||||
This data needs to be transferred to another Delta Chat account
|
this data needs to be transferred to another Delta Chat account
|
||||||
in a second channel, typically used by mobiles with QRcode-show + scan UX.
|
in a second channel, typically used by mobiles with QRcode-show + scan UX.
|
||||||
"""
|
"""
|
||||||
return self._rpc.get_chat_securejoin_qr_code(self.id, None)
|
|
||||||
|
|
||||||
def get_qr_code_svg(self) -> tuple[str, str]:
|
|
||||||
"""Get Setup-Contact QR code text and SVG."""
|
|
||||||
return self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
|
return self._rpc.get_chat_securejoin_qr_code_svg(self.id, None)
|
||||||
|
|
||||||
def get_message_by_id(self, msg_id: int) -> Message:
|
def get_message_by_id(self, msg_id: int) -> Message:
|
||||||
"""Return the Message instance with the given ID."""
|
"""Return the Message instance with the given ID."""
|
||||||
return Message(self, msg_id)
|
return Message(self, msg_id)
|
||||||
|
|
||||||
def mark_seen_messages(self, messages: list[Message]) -> None:
|
def mark_seen_messages(self, messages: List[Message]) -> None:
|
||||||
"""Mark the given set of messages as seen."""
|
"""Mark the given set of messages as seen."""
|
||||||
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
|
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
|
||||||
|
|
||||||
def delete_messages(self, messages: list[Message]) -> None:
|
def delete_messages(self, messages: List[Message]) -> None:
|
||||||
"""Delete messages (local and remote)."""
|
"""Delete messages (local and remote)."""
|
||||||
self._rpc.delete_messages(self.id, [msg.id for msg in messages])
|
self._rpc.delete_messages(self.id, [msg.id for msg in messages])
|
||||||
|
|
||||||
def get_fresh_messages(self) -> list[Message]:
|
def get_fresh_messages(self) -> List[Message]:
|
||||||
"""Return the list of fresh messages, newest messages first.
|
"""Return the list of fresh messages, newest messages first.
|
||||||
|
|
||||||
This call is intended for displaying notifications.
|
This call is intended for displaying notifications.
|
||||||
@@ -398,64 +261,36 @@ class Account:
|
|||||||
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
|
fresh_msg_ids = self._rpc.get_fresh_msgs(self.id)
|
||||||
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
return [Message(self, msg_id) for msg_id in fresh_msg_ids]
|
||||||
|
|
||||||
def get_next_messages(self) -> list[Message]:
|
def get_next_messages(self) -> List[Message]:
|
||||||
"""Return a list of next messages."""
|
"""Return a list of next messages."""
|
||||||
next_msg_ids = self._rpc.get_next_msgs(self.id)
|
next_msg_ids = self._rpc.get_next_msgs(self.id)
|
||||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||||
|
|
||||||
@futuremethod
|
def wait_next_messages(self) -> List[Message]:
|
||||||
def wait_next_messages(self) -> list[Message]:
|
|
||||||
"""Wait for new messages and return a list of them."""
|
"""Wait for new messages and return a list of them."""
|
||||||
next_msg_ids = yield self._rpc.wait_next_msgs.future(self.id)
|
next_msg_ids = self._rpc.wait_next_msgs(self.id)
|
||||||
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
return [Message(self, msg_id) for msg_id in next_msg_ids]
|
||||||
|
|
||||||
def wait_for_incoming_msg_event(self):
|
def wait_for_incoming_msg_event(self):
|
||||||
"""Wait for incoming message event and return it."""
|
"""Wait for incoming message event and return it."""
|
||||||
return self.wait_for_event(EventType.INCOMING_MSG)
|
while True:
|
||||||
|
event = self.wait_for_event()
|
||||||
def wait_for_msgs_changed_event(self):
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
"""Wait for messages changed event and return it."""
|
return event
|
||||||
return self.wait_for_event(EventType.MSGS_CHANGED)
|
|
||||||
|
|
||||||
def wait_for_msgs_noticed_event(self):
|
|
||||||
"""Wait for messages noticed event and return it."""
|
|
||||||
return self.wait_for_event(EventType.MSGS_NOTICED)
|
|
||||||
|
|
||||||
def wait_for_msg(self, event_type) -> Message:
|
|
||||||
"""Wait for an event about the message.
|
|
||||||
|
|
||||||
Consumes all events before the matching event.
|
|
||||||
Returns a message corresponding to the msg_id field of the event.
|
|
||||||
"""
|
|
||||||
event = self.wait_for_event(event_type)
|
|
||||||
return self.get_message_by_id(event.msg_id)
|
|
||||||
|
|
||||||
def wait_for_incoming_msg(self):
|
|
||||||
"""Wait for incoming message and return it.
|
|
||||||
|
|
||||||
Consumes all events before the next incoming message event.
|
|
||||||
"""
|
|
||||||
return self.wait_for_msg(EventType.INCOMING_MSG)
|
|
||||||
|
|
||||||
def wait_for_securejoin_inviter_success(self):
|
def wait_for_securejoin_inviter_success(self):
|
||||||
"""Wait until SecureJoin process finishes successfully on the inviter side."""
|
|
||||||
while True:
|
while True:
|
||||||
event = self.wait_for_event()
|
event = self.wait_for_event()
|
||||||
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
if event["kind"] == "SecurejoinInviterProgress" and event["progress"] == 1000:
|
||||||
break
|
break
|
||||||
|
|
||||||
def wait_for_securejoin_joiner_success(self):
|
def wait_for_securejoin_joiner_success(self):
|
||||||
"""Wait until SecureJoin process finishes successfully on the joiner side."""
|
|
||||||
while True:
|
while True:
|
||||||
event = self.wait_for_event()
|
event = self.wait_for_event()
|
||||||
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
if event["kind"] == "SecurejoinJoinerProgress" and event["progress"] == 1000:
|
||||||
break
|
break
|
||||||
|
|
||||||
def wait_for_reactions_changed(self):
|
def get_fresh_messages_in_arrival_order(self) -> List[Message]:
|
||||||
"""Wait for reaction change event."""
|
|
||||||
return self.wait_for_event(EventType.REACTIONS_CHANGED)
|
|
||||||
|
|
||||||
def get_fresh_messages_in_arrival_order(self) -> list[Message]:
|
|
||||||
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
"""Return fresh messages list sorted in the order of their arrival, with ascending IDs."""
|
||||||
warn(
|
warn(
|
||||||
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
|
"get_fresh_messages_in_arrival_order is deprecated, use get_next_messages instead.",
|
||||||
@@ -482,12 +317,3 @@ class Account:
|
|||||||
"""Import keys."""
|
"""Import keys."""
|
||||||
passphrase = "" # Importing passphrase-protected keys is currently not supported.
|
passphrase = "" # Importing passphrase-protected keys is currently not supported.
|
||||||
self._rpc.import_self_keys(self.id, str(path), passphrase)
|
self._rpc.import_self_keys(self.id, str(path), passphrase)
|
||||||
|
|
||||||
def initiate_autocrypt_key_transfer(self) -> None:
|
|
||||||
"""Send Autocrypt Setup Message."""
|
|
||||||
return self._rpc.initiate_autocrypt_key_transfer(self.id)
|
|
||||||
|
|
||||||
def ice_servers(self) -> list:
|
|
||||||
"""Return ICE servers for WebRTC configuration."""
|
|
||||||
ice_servers_json = self._rpc.ice_servers(self.id)
|
|
||||||
return json.loads(ice_servers_json)
|
|
||||||
|
|||||||
@@ -1,11 +1,6 @@
|
|||||||
"""Chat module."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import calendar
|
import calendar
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from tempfile import NamedTemporaryFile
|
from typing import TYPE_CHECKING, Dict, List, Optional, Tuple, Union
|
||||||
from typing import TYPE_CHECKING, Optional, Union
|
|
||||||
|
|
||||||
from ._utils import AttrDict
|
from ._utils import AttrDict
|
||||||
from .const import ChatVisibility, ViewType
|
from .const import ChatVisibility, ViewType
|
||||||
@@ -91,19 +86,14 @@ class Chat:
|
|||||||
def set_ephemeral_timer(self, timer: int) -> None:
|
def set_ephemeral_timer(self, timer: int) -> None:
|
||||||
"""Set ephemeral timer of this chat in seconds.
|
"""Set ephemeral timer of this chat in seconds.
|
||||||
|
|
||||||
0 means the timer is disabled, use 1 for immediate deletion.
|
0 means the timer is disabled, use 1 for immediate deletion."""
|
||||||
"""
|
|
||||||
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
||||||
|
|
||||||
def get_encryption_info(self) -> str:
|
def get_encryption_info(self) -> str:
|
||||||
"""Return encryption info for this chat."""
|
"""Return encryption info for this chat."""
|
||||||
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
||||||
|
|
||||||
def get_qr_code(self) -> str:
|
def get_qr_code(self) -> Tuple[str, str]:
|
||||||
"""Get Join-Group QR code text."""
|
|
||||||
return self._rpc.get_chat_securejoin_qr_code(self.account.id, self.id)
|
|
||||||
|
|
||||||
def get_qr_code_svg(self) -> tuple[str, str]:
|
|
||||||
"""Get Join-Group QR code text and SVG data."""
|
"""Get Join-Group QR code text and SVG data."""
|
||||||
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
|
return self._rpc.get_chat_securejoin_qr_code_svg(self.account.id, self.id)
|
||||||
|
|
||||||
@@ -127,8 +117,7 @@ class Chat:
|
|||||||
html: Optional[str] = None,
|
html: Optional[str] = None,
|
||||||
viewtype: Optional[ViewType] = None,
|
viewtype: Optional[ViewType] = None,
|
||||||
file: Optional[str] = None,
|
file: Optional[str] = None,
|
||||||
filename: Optional[str] = None,
|
location: Optional[Tuple[float, float]] = None,
|
||||||
location: Optional[tuple[float, float]] = None,
|
|
||||||
override_sender_name: Optional[str] = None,
|
override_sender_name: Optional[str] = None,
|
||||||
quoted_msg: Optional[Union[int, Message]] = None,
|
quoted_msg: Optional[Union[int, Message]] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
@@ -141,7 +130,6 @@ class Chat:
|
|||||||
"html": html,
|
"html": html,
|
||||||
"viewtype": viewtype,
|
"viewtype": viewtype,
|
||||||
"file": file,
|
"file": file,
|
||||||
"filename": filename,
|
|
||||||
"location": location,
|
"location": location,
|
||||||
"overrideSenderName": override_sender_name,
|
"overrideSenderName": override_sender_name,
|
||||||
"quotedMessageId": quoted_msg,
|
"quotedMessageId": quoted_msg,
|
||||||
@@ -154,10 +142,6 @@ class Chat:
|
|||||||
msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text)
|
msg_id = self._rpc.misc_send_text_message(self.account.id, self.id, text)
|
||||||
return Message(self.account, msg_id)
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
def send_file(self, path):
|
|
||||||
"""Send a file and return the resulting Message instance."""
|
|
||||||
return self.send_message(file=path)
|
|
||||||
|
|
||||||
def send_videochat_invitation(self) -> Message:
|
def send_videochat_invitation(self) -> Message:
|
||||||
"""Send a videochat invitation and return the resulting Message instance."""
|
"""Send a videochat invitation and return the resulting Message instance."""
|
||||||
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
|
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
|
||||||
@@ -168,12 +152,7 @@ class Chat:
|
|||||||
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
msg_id = self._rpc.send_sticker(self.account.id, self.id, path)
|
||||||
return Message(self.account, msg_id)
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
def resend_messages(self, messages: list[Message]) -> None:
|
def forward_messages(self, messages: List[Message]) -> None:
|
||||||
"""Resend a list of messages to this chat."""
|
|
||||||
msg_ids = [msg.id for msg in messages]
|
|
||||||
self._rpc.resend_messages(self.account.id, msg_ids)
|
|
||||||
|
|
||||||
def forward_messages(self, messages: list[Message]) -> None:
|
|
||||||
"""Forward a list of messages to this chat."""
|
"""Forward a list of messages to this chat."""
|
||||||
msg_ids = [msg.id for msg in messages]
|
msg_ids = [msg.id for msg in messages]
|
||||||
self._rpc.forward_messages(self.account.id, msg_ids, self.id)
|
self._rpc.forward_messages(self.account.id, msg_ids, self.id)
|
||||||
@@ -182,14 +161,13 @@ class Chat:
|
|||||||
self,
|
self,
|
||||||
text: Optional[str] = None,
|
text: Optional[str] = None,
|
||||||
file: Optional[str] = None,
|
file: Optional[str] = None,
|
||||||
filename: Optional[str] = None,
|
|
||||||
quoted_msg: Optional[int] = None,
|
quoted_msg: Optional[int] = None,
|
||||||
viewtype: Optional[str] = None,
|
viewtype: Optional[str] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Set draft message."""
|
"""Set draft message."""
|
||||||
if isinstance(quoted_msg, Message):
|
if isinstance(quoted_msg, Message):
|
||||||
quoted_msg = quoted_msg.id
|
quoted_msg = quoted_msg.id
|
||||||
self._rpc.misc_set_draft(self.account.id, self.id, text, file, filename, quoted_msg, viewtype)
|
self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg, viewtype)
|
||||||
|
|
||||||
def remove_draft(self) -> None:
|
def remove_draft(self) -> None:
|
||||||
"""Remove draft message."""
|
"""Remove draft message."""
|
||||||
@@ -206,13 +184,13 @@ class Chat:
|
|||||||
snapshot["message"] = Message(self.account, snapshot.id)
|
snapshot["message"] = Message(self.account, snapshot.id)
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> list[Message]:
|
def get_messages(self, info_only: bool = False, add_daymarker: bool = False) -> List[Message]:
|
||||||
"""Get the list of messages in this chat."""
|
"""get the list of messages in this chat."""
|
||||||
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
msgs = self._rpc.get_message_ids(self.account.id, self.id, info_only, add_daymarker)
|
||||||
return [Message(self.account, msg_id) for msg_id in msgs]
|
return [Message(self.account, msg_id) for msg_id in msgs]
|
||||||
|
|
||||||
def get_fresh_message_count(self) -> int:
|
def get_fresh_message_count(self) -> int:
|
||||||
"""Get number of fresh messages in this chat."""
|
"""Get number of fresh messages in this chat"""
|
||||||
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
||||||
|
|
||||||
def mark_noticed(self) -> None:
|
def mark_noticed(self) -> None:
|
||||||
@@ -241,7 +219,7 @@ class Chat:
|
|||||||
contact_id = cnt
|
contact_id = cnt
|
||||||
self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
|
self._rpc.remove_contact_from_chat(self.account.id, self.id, contact_id)
|
||||||
|
|
||||||
def get_contacts(self) -> list[Contact]:
|
def get_contacts(self) -> List[Contact]:
|
||||||
"""Get the contacts belonging to this chat.
|
"""Get the contacts belonging to this chat.
|
||||||
|
|
||||||
For single/direct chats self-address is not included.
|
For single/direct chats self-address is not included.
|
||||||
@@ -249,11 +227,6 @@ class Chat:
|
|||||||
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
|
contacts = self._rpc.get_chat_contacts(self.account.id, self.id)
|
||||||
return [Contact(self.account, contact_id) for contact_id in contacts]
|
return [Contact(self.account, contact_id) for contact_id in contacts]
|
||||||
|
|
||||||
def get_past_contacts(self) -> list[Contact]:
|
|
||||||
"""Get past contacts for this chat."""
|
|
||||||
past_contacts = self._rpc.get_past_chat_contacts(self.account.id, self.id)
|
|
||||||
return [Contact(self.account, contact_id) for contact_id in past_contacts]
|
|
||||||
|
|
||||||
def set_image(self, path: str) -> None:
|
def set_image(self, path: str) -> None:
|
||||||
"""Set profile image of this chat.
|
"""Set profile image of this chat.
|
||||||
|
|
||||||
@@ -270,7 +243,7 @@ class Chat:
|
|||||||
contact: Optional[Contact] = None,
|
contact: Optional[Contact] = None,
|
||||||
timestamp_from: Optional["datetime"] = None,
|
timestamp_from: Optional["datetime"] = None,
|
||||||
timestamp_to: Optional["datetime"] = None,
|
timestamp_to: Optional["datetime"] = None,
|
||||||
) -> list[AttrDict]:
|
) -> List[AttrDict]:
|
||||||
"""Get list of location snapshots for the given contact in the given timespan."""
|
"""Get list of location snapshots for the given contact in the given timespan."""
|
||||||
time_from = calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
|
time_from = calendar.timegm(timestamp_from.utctimetuple()) if timestamp_from else 0
|
||||||
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
|
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
|
||||||
@@ -278,7 +251,7 @@ class Chat:
|
|||||||
|
|
||||||
result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
|
result = self._rpc.get_locations(self.account.id, self.id, contact_id, time_from, time_to)
|
||||||
locations = []
|
locations = []
|
||||||
contacts: dict[int, Contact] = {}
|
contacts: Dict[int, Contact] = {}
|
||||||
for loc in result:
|
for loc in result:
|
||||||
location = AttrDict(loc)
|
location = AttrDict(loc)
|
||||||
location["chat"] = self
|
location["chat"] = self
|
||||||
@@ -286,16 +259,3 @@ class Chat:
|
|||||||
location["message"] = Message(self.account, location.msg_id)
|
location["message"] = Message(self.account, location.msg_id)
|
||||||
locations.append(location)
|
locations.append(location)
|
||||||
return locations
|
return locations
|
||||||
|
|
||||||
def send_contact(self, contact: Contact):
|
|
||||||
"""Send contact to the chat."""
|
|
||||||
vcard = contact.make_vcard()
|
|
||||||
with NamedTemporaryFile(suffix=".vcard") as f:
|
|
||||||
f.write(vcard.encode())
|
|
||||||
f.flush()
|
|
||||||
self._rpc.send_msg(self.account.id, self.id, {"viewtype": ViewType.VCARD, "file": f.name})
|
|
||||||
|
|
||||||
def place_outgoing_call(self, place_call_info: str) -> Message:
|
|
||||||
"""Starts an outgoing call."""
|
|
||||||
msg_id = self._rpc.place_outgoing_call(self.account.id, self.id, place_call_info)
|
|
||||||
return Message(self.account, msg_id)
|
|
||||||
|
|||||||
@@ -1,13 +1,14 @@
|
|||||||
"""Event loop implementations offering high level event handling/hooking."""
|
"""Event loop implementations offering high level event handling/hooking."""
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Callable,
|
Callable,
|
||||||
|
Dict,
|
||||||
Iterable,
|
Iterable,
|
||||||
Optional,
|
Optional,
|
||||||
|
Set,
|
||||||
|
Tuple,
|
||||||
Type,
|
Type,
|
||||||
Union,
|
Union,
|
||||||
)
|
)
|
||||||
@@ -38,17 +39,16 @@ class Client:
|
|||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
account: "Account",
|
account: "Account",
|
||||||
hooks: Optional[Iterable[tuple[Callable, Union[type, EventFilter]]]] = None,
|
hooks: Optional[Iterable[Tuple[Callable, Union[type, EventFilter]]]] = None,
|
||||||
logger: Optional[logging.Logger] = None,
|
logger: Optional[logging.Logger] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
self.account = account
|
self.account = account
|
||||||
self.logger = logger or logging
|
self.logger = logger or logging
|
||||||
self._hooks: dict[type, set[tuple]] = {}
|
self._hooks: Dict[type, Set[tuple]] = {}
|
||||||
self._should_process_messages = 0
|
self._should_process_messages = 0
|
||||||
self.add_hooks(hooks or [])
|
self.add_hooks(hooks or [])
|
||||||
|
|
||||||
def add_hooks(self, hooks: Iterable[tuple[Callable, Union[type, EventFilter]]]) -> None:
|
def add_hooks(self, hooks: Iterable[Tuple[Callable, Union[type, EventFilter]]]) -> None:
|
||||||
"""Register multiple hooks."""
|
|
||||||
for hook, event in hooks:
|
for hook, event in hooks:
|
||||||
self.add_hook(hook, event)
|
self.add_hook(hook, event)
|
||||||
|
|
||||||
@@ -78,15 +78,14 @@ class Client:
|
|||||||
self._hooks.get(type(event), set()).remove((hook, event))
|
self._hooks.get(type(event), set()).remove((hook, event))
|
||||||
|
|
||||||
def is_configured(self) -> bool:
|
def is_configured(self) -> bool:
|
||||||
"""Return True if the client is configured."""
|
|
||||||
return self.account.is_configured()
|
return self.account.is_configured()
|
||||||
|
|
||||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||||
"""Configure the client."""
|
self.account.set_config("addr", email)
|
||||||
|
self.account.set_config("mail_pw", password)
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
self.account.set_config(key, value)
|
self.account.set_config(key, value)
|
||||||
params = {"addr": email, "password": password}
|
self.account.configure()
|
||||||
self.account.add_or_update_transport(params)
|
|
||||||
self.logger.debug("Account configured")
|
self.logger.debug("Account configured")
|
||||||
|
|
||||||
def run_forever(self) -> None:
|
def run_forever(self) -> None:
|
||||||
@@ -200,6 +199,5 @@ class Bot(Client):
|
|||||||
"""Simple bot implementation that listens to events of a single account."""
|
"""Simple bot implementation that listens to events of a single account."""
|
||||||
|
|
||||||
def configure(self, email: str, password: str, **kwargs) -> None:
|
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||||
"""Configure the bot."""
|
|
||||||
kwargs.setdefault("bot", "1")
|
kwargs.setdefault("bot", "1")
|
||||||
super().configure(email, password, **kwargs)
|
super().configure(email, password, **kwargs)
|
||||||
|
|||||||
@@ -1,20 +1,14 @@
|
|||||||
"""Constants module."""
|
|
||||||
|
|
||||||
from enum import Enum, IntEnum
|
from enum import Enum, IntEnum
|
||||||
|
|
||||||
COMMAND_PREFIX = "/"
|
COMMAND_PREFIX = "/"
|
||||||
|
|
||||||
|
|
||||||
class ContactFlag(IntEnum):
|
class ContactFlag(IntEnum):
|
||||||
"""Bit flags for get_contacts() method."""
|
VERIFIED_ONLY = 0x01
|
||||||
|
|
||||||
ADD_SELF = 0x02
|
ADD_SELF = 0x02
|
||||||
ADDRESS = 0x04
|
|
||||||
|
|
||||||
|
|
||||||
class ChatlistFlag(IntEnum):
|
class ChatlistFlag(IntEnum):
|
||||||
"""Bit flags for get_chatlist() method."""
|
|
||||||
|
|
||||||
ARCHIVED_ONLY = 0x01
|
ARCHIVED_ONLY = 0x01
|
||||||
NO_SPECIALS = 0x02
|
NO_SPECIALS = 0x02
|
||||||
ADD_ALLDONE_HINT = 0x04
|
ADD_ALLDONE_HINT = 0x04
|
||||||
@@ -22,8 +16,6 @@ class ChatlistFlag(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class SpecialContactId(IntEnum):
|
class SpecialContactId(IntEnum):
|
||||||
"""Special contact IDs."""
|
|
||||||
|
|
||||||
SELF = 1
|
SELF = 1
|
||||||
INFO = 2 # centered messages as "member added", used in all chats
|
INFO = 2 # centered messages as "member added", used in all chats
|
||||||
DEVICE = 5 # messages "update info" in the device-chat
|
DEVICE = 5 # messages "update info" in the device-chat
|
||||||
@@ -31,7 +23,7 @@ class SpecialContactId(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class EventType(str, Enum):
|
class EventType(str, Enum):
|
||||||
"""Core event types."""
|
"""Core event types"""
|
||||||
|
|
||||||
INFO = "Info"
|
INFO = "Info"
|
||||||
SMTP_CONNECTED = "SmtpConnected"
|
SMTP_CONNECTED = "SmtpConnected"
|
||||||
@@ -49,14 +41,12 @@ class EventType(str, Enum):
|
|||||||
REACTIONS_CHANGED = "ReactionsChanged"
|
REACTIONS_CHANGED = "ReactionsChanged"
|
||||||
INCOMING_MSG = "IncomingMsg"
|
INCOMING_MSG = "IncomingMsg"
|
||||||
INCOMING_MSG_BUNCH = "IncomingMsgBunch"
|
INCOMING_MSG_BUNCH = "IncomingMsgBunch"
|
||||||
INCOMING_REACTION = "IncomingReaction"
|
|
||||||
MSGS_NOTICED = "MsgsNoticed"
|
MSGS_NOTICED = "MsgsNoticed"
|
||||||
MSG_DELIVERED = "MsgDelivered"
|
MSG_DELIVERED = "MsgDelivered"
|
||||||
MSG_FAILED = "MsgFailed"
|
MSG_FAILED = "MsgFailed"
|
||||||
MSG_READ = "MsgRead"
|
MSG_READ = "MsgRead"
|
||||||
MSG_DELETED = "MsgDeleted"
|
MSG_DELETED = "MsgDeleted"
|
||||||
CHAT_MODIFIED = "ChatModified"
|
CHAT_MODIFIED = "ChatModified"
|
||||||
CHAT_DELETED = "ChatDeleted"
|
|
||||||
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
|
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
|
||||||
CONTACTS_CHANGED = "ContactsChanged"
|
CONTACTS_CHANGED = "ContactsChanged"
|
||||||
LOCATION_CHANGED = "LocationChanged"
|
LOCATION_CHANGED = "LocationChanged"
|
||||||
@@ -69,22 +59,10 @@ class EventType(str, Enum):
|
|||||||
SELFAVATAR_CHANGED = "SelfavatarChanged"
|
SELFAVATAR_CHANGED = "SelfavatarChanged"
|
||||||
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
|
WEBXDC_STATUS_UPDATE = "WebxdcStatusUpdate"
|
||||||
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
|
WEBXDC_INSTANCE_DELETED = "WebxdcInstanceDeleted"
|
||||||
CHATLIST_CHANGED = "ChatlistChanged"
|
|
||||||
CHATLIST_ITEM_CHANGED = "ChatlistItemChanged"
|
|
||||||
ACCOUNTS_CHANGED = "AccountsChanged"
|
|
||||||
ACCOUNTS_ITEM_CHANGED = "AccountsItemChanged"
|
|
||||||
INCOMING_CALL = "IncomingCall"
|
|
||||||
INCOMING_CALL_ACCEPTED = "IncomingCallAccepted"
|
|
||||||
OUTGOING_CALL_ACCEPTED = "OutgoingCallAccepted"
|
|
||||||
CALL_ENDED = "CallEnded"
|
|
||||||
CONFIG_SYNCED = "ConfigSynced"
|
|
||||||
WEBXDC_REALTIME_DATA = "WebxdcRealtimeData"
|
|
||||||
WEBXDC_REALTIME_ADVERTISEMENT_RECEIVED = "WebxdcRealtimeAdvertisementReceived"
|
|
||||||
TRANSPORTS_MODIFIED = "TransportsModified"
|
|
||||||
|
|
||||||
|
|
||||||
class ChatId(IntEnum):
|
class ChatId(IntEnum):
|
||||||
"""Special chat IDs."""
|
"""Special chat ids"""
|
||||||
|
|
||||||
TRASH = 3
|
TRASH = 3
|
||||||
ARCHIVED_LINK = 6
|
ARCHIVED_LINK = 6
|
||||||
@@ -92,46 +70,18 @@ class ChatId(IntEnum):
|
|||||||
LAST_SPECIAL = 9
|
LAST_SPECIAL = 9
|
||||||
|
|
||||||
|
|
||||||
class ChatType(str, Enum):
|
class ChatType(IntEnum):
|
||||||
"""Chat type."""
|
"""Chat types"""
|
||||||
|
|
||||||
SINGLE = "Single"
|
UNDEFINED = 0
|
||||||
"""1:1 chat, i.e. a direct chat with a single contact"""
|
SINGLE = 100
|
||||||
|
GROUP = 120
|
||||||
GROUP = "Group"
|
MAILINGLIST = 140
|
||||||
|
BROADCAST = 160
|
||||||
MAILINGLIST = "Mailinglist"
|
|
||||||
|
|
||||||
OUT_BROADCAST = "OutBroadcast"
|
|
||||||
"""Outgoing broadcast channel, called "Channel" in the UI.
|
|
||||||
|
|
||||||
The user can send into this channel,
|
|
||||||
and all recipients will receive messages
|
|
||||||
in an `IN_BROADCAST`.
|
|
||||||
|
|
||||||
Called `broadcast` here rather than `channel`,
|
|
||||||
because the word "channel" already appears a lot in the code,
|
|
||||||
which would make it hard to grep for it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
IN_BROADCAST = "InBroadcast"
|
|
||||||
"""Incoming broadcast channel, called "Channel" in the UI.
|
|
||||||
|
|
||||||
This channel is read-only,
|
|
||||||
and we do not know who the other recipients are.
|
|
||||||
|
|
||||||
This is similar to a `MAILINGLIST`,
|
|
||||||
with the main difference being that
|
|
||||||
`IN_BROADCAST`s are encrypted.
|
|
||||||
|
|
||||||
Called `broadcast` here rather than `channel`,
|
|
||||||
because the word "channel" already appears a lot in the code,
|
|
||||||
which would make it hard to grep for it.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class ChatVisibility(str, Enum):
|
class ChatVisibility(str, Enum):
|
||||||
"""Chat visibility types."""
|
"""Chat visibility types"""
|
||||||
|
|
||||||
NORMAL = "Normal"
|
NORMAL = "Normal"
|
||||||
ARCHIVED = "Archived"
|
ARCHIVED = "Archived"
|
||||||
@@ -139,7 +89,7 @@ class ChatVisibility(str, Enum):
|
|||||||
|
|
||||||
|
|
||||||
class DownloadState(str, Enum):
|
class DownloadState(str, Enum):
|
||||||
"""Message download state."""
|
"""Message download state"""
|
||||||
|
|
||||||
DONE = "Done"
|
DONE = "Done"
|
||||||
AVAILABLE = "Available"
|
AVAILABLE = "Available"
|
||||||
@@ -159,8 +109,8 @@ class ViewType(str, Enum):
|
|||||||
VOICE = "Voice"
|
VOICE = "Voice"
|
||||||
VIDEO = "Video"
|
VIDEO = "Video"
|
||||||
FILE = "File"
|
FILE = "File"
|
||||||
|
VIDEOCHAT_INVITATION = "VideochatInvitation"
|
||||||
WEBXDC = "Webxdc"
|
WEBXDC = "Webxdc"
|
||||||
VCARD = "Vcard"
|
|
||||||
|
|
||||||
|
|
||||||
class SystemMessageType(str, Enum):
|
class SystemMessageType(str, Enum):
|
||||||
@@ -199,14 +149,14 @@ class MessageState(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class MessageId(IntEnum):
|
class MessageId(IntEnum):
|
||||||
"""Special message IDs."""
|
"""Special message ids"""
|
||||||
|
|
||||||
DAYMARKER = 9
|
DAYMARKER = 9
|
||||||
LAST_SPECIAL = 9
|
LAST_SPECIAL = 9
|
||||||
|
|
||||||
|
|
||||||
class CertificateChecks(IntEnum):
|
class CertificateChecks(IntEnum):
|
||||||
"""Certificate checks mode."""
|
"""Certificate checks mode"""
|
||||||
|
|
||||||
AUTOMATIC = 0
|
AUTOMATIC = 0
|
||||||
STRICT = 1
|
STRICT = 1
|
||||||
@@ -214,7 +164,7 @@ class CertificateChecks(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class Connectivity(IntEnum):
|
class Connectivity(IntEnum):
|
||||||
"""Connectivity states."""
|
"""Connectivity states"""
|
||||||
|
|
||||||
NOT_CONNECTED = 1000
|
NOT_CONNECTED = 1000
|
||||||
CONNECTING = 2000
|
CONNECTING = 2000
|
||||||
@@ -223,7 +173,7 @@ class Connectivity(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class KeyGenType(IntEnum):
|
class KeyGenType(IntEnum):
|
||||||
"""Type of the key to generate."""
|
"""Type of the key to generate"""
|
||||||
|
|
||||||
DEFAULT = 0
|
DEFAULT = 0
|
||||||
RSA2048 = 1
|
RSA2048 = 1
|
||||||
@@ -233,21 +183,21 @@ class KeyGenType(IntEnum):
|
|||||||
|
|
||||||
# "Lp" means "login parameters"
|
# "Lp" means "login parameters"
|
||||||
class LpAuthFlag(IntEnum):
|
class LpAuthFlag(IntEnum):
|
||||||
"""Authorization flags."""
|
"""Authorization flags"""
|
||||||
|
|
||||||
OAUTH2 = 0x2
|
OAUTH2 = 0x2
|
||||||
NORMAL = 0x4
|
NORMAL = 0x4
|
||||||
|
|
||||||
|
|
||||||
class MediaQuality(IntEnum):
|
class MediaQuality(IntEnum):
|
||||||
"""Media quality setting."""
|
"""Media quality setting"""
|
||||||
|
|
||||||
BALANCED = 0
|
BALANCED = 0
|
||||||
WORSE = 1
|
WORSE = 1
|
||||||
|
|
||||||
|
|
||||||
class ProviderStatus(IntEnum):
|
class ProviderStatus(IntEnum):
|
||||||
"""Provider status according to manual testing."""
|
"""Provider status according to manual testing"""
|
||||||
|
|
||||||
OK = 1
|
OK = 1
|
||||||
PREPARATION = 2
|
PREPARATION = 2
|
||||||
@@ -255,7 +205,7 @@ class ProviderStatus(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class PushNotifyState(IntEnum):
|
class PushNotifyState(IntEnum):
|
||||||
"""Push notifications state."""
|
"""Push notifications state"""
|
||||||
|
|
||||||
NOT_CONNECTED = 0
|
NOT_CONNECTED = 0
|
||||||
HEARTBEAT = 1
|
HEARTBEAT = 1
|
||||||
@@ -263,7 +213,7 @@ class PushNotifyState(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class ShowEmails(IntEnum):
|
class ShowEmails(IntEnum):
|
||||||
"""Show emails mode."""
|
"""Show emails mode"""
|
||||||
|
|
||||||
OFF = 0
|
OFF = 0
|
||||||
ACCEPTED_CONTACTS = 1
|
ACCEPTED_CONTACTS = 1
|
||||||
@@ -271,9 +221,17 @@ class ShowEmails(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class SocketSecurity(IntEnum):
|
class SocketSecurity(IntEnum):
|
||||||
"""Socket security."""
|
"""Socket security"""
|
||||||
|
|
||||||
AUTOMATIC = 0
|
AUTOMATIC = 0
|
||||||
SSL = 1
|
SSL = 1
|
||||||
STARTTLS = 2
|
STARTTLS = 2
|
||||||
PLAIN = 3
|
PLAIN = 3
|
||||||
|
|
||||||
|
|
||||||
|
class VideochatType(IntEnum):
|
||||||
|
"""Video chat URL type"""
|
||||||
|
|
||||||
|
UNKNOWN = 0
|
||||||
|
BASICWEBRTC = 1
|
||||||
|
JITSI = 2
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
"""Contact module."""
|
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
@@ -13,7 +11,8 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Contact:
|
class Contact:
|
||||||
"""Contact API.
|
"""
|
||||||
|
Contact API.
|
||||||
|
|
||||||
Essentially a wrapper for RPC, account ID and a contact ID.
|
Essentially a wrapper for RPC, account ID and a contact ID.
|
||||||
"""
|
"""
|
||||||
@@ -42,9 +41,8 @@ class Contact:
|
|||||||
self._rpc.change_contact_name(self.account.id, self.id, name)
|
self._rpc.change_contact_name(self.account.id, self.id, name)
|
||||||
|
|
||||||
def get_encryption_info(self) -> str:
|
def get_encryption_info(self) -> str:
|
||||||
"""Get a multi-line encryption info.
|
"""Get a multi-line encryption info, containing your fingerprint and
|
||||||
|
the fingerprint of the contact.
|
||||||
Encryption info contains your fingerprint and the fingerprint of the contact.
|
|
||||||
"""
|
"""
|
||||||
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
||||||
|
|
||||||
@@ -62,7 +60,3 @@ class Contact:
|
|||||||
self.account,
|
self.account,
|
||||||
self._rpc.create_chat_by_contact_id(self.account.id, self.id),
|
self._rpc.create_chat_by_contact_id(self.account.id, self.id),
|
||||||
)
|
)
|
||||||
|
|
||||||
def make_vcard(self) -> str:
|
|
||||||
"""Make a vCard for the contact."""
|
|
||||||
return self.account.make_vcard([self])
|
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
"""Account manager module."""
|
from typing import TYPE_CHECKING, Dict, List
|
||||||
|
|
||||||
from __future__ import annotations
|
from ._utils import AttrDict
|
||||||
|
|
||||||
from typing import TYPE_CHECKING
|
|
||||||
|
|
||||||
from ._utils import AttrDict, futuremethod
|
|
||||||
from .account import Account
|
from .account import Account
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -12,13 +8,12 @@ if TYPE_CHECKING:
|
|||||||
|
|
||||||
|
|
||||||
class DeltaChat:
|
class DeltaChat:
|
||||||
"""Delta Chat accounts manager.
|
"""
|
||||||
|
Delta Chat accounts manager.
|
||||||
This is the root of the object oriented API.
|
This is the root of the object oriented API.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, rpc: "Rpc") -> None:
|
def __init__(self, rpc: "Rpc") -> None:
|
||||||
"""Initialize account manager."""
|
|
||||||
self.rpc = rpc
|
self.rpc = rpc
|
||||||
|
|
||||||
def add_account(self) -> Account:
|
def add_account(self) -> Account:
|
||||||
@@ -26,7 +21,7 @@ class DeltaChat:
|
|||||||
account_id = self.rpc.add_account()
|
account_id = self.rpc.add_account()
|
||||||
return Account(self, account_id)
|
return Account(self, account_id)
|
||||||
|
|
||||||
def get_all_accounts(self) -> list[Account]:
|
def get_all_accounts(self) -> List[Account]:
|
||||||
"""Return a list of all available accounts."""
|
"""Return a list of all available accounts."""
|
||||||
account_ids = self.rpc.get_all_account_ids()
|
account_ids = self.rpc.get_all_account_ids()
|
||||||
return [Account(self, account_id) for account_id in account_ids]
|
return [Account(self, account_id) for account_id in account_ids]
|
||||||
@@ -39,23 +34,16 @@ class DeltaChat:
|
|||||||
"""Stop the I/O of all accounts."""
|
"""Stop the I/O of all accounts."""
|
||||||
self.rpc.stop_io_for_all_accounts()
|
self.rpc.stop_io_for_all_accounts()
|
||||||
|
|
||||||
@futuremethod
|
|
||||||
def background_fetch(self, timeout_in_seconds: int) -> None:
|
|
||||||
"""Run background fetch for all accounts."""
|
|
||||||
yield self.rpc.background_fetch.future(timeout_in_seconds)
|
|
||||||
|
|
||||||
def stop_background_fetch(self) -> None:
|
|
||||||
"""Stop ongoing background fetch."""
|
|
||||||
self.rpc.stop_background_fetch()
|
|
||||||
|
|
||||||
def maybe_network(self) -> None:
|
def maybe_network(self) -> None:
|
||||||
"""Indicate that the network conditions might have changed."""
|
"""Indicate that the network likely has come back or just that the network
|
||||||
|
conditions might have changed.
|
||||||
|
"""
|
||||||
self.rpc.maybe_network()
|
self.rpc.maybe_network()
|
||||||
|
|
||||||
def get_system_info(self) -> AttrDict:
|
def get_system_info(self) -> AttrDict:
|
||||||
"""Get information about the Delta Chat core in this system."""
|
"""Get information about the Delta Chat core in this system."""
|
||||||
return AttrDict(self.rpc.get_system_info())
|
return AttrDict(self.rpc.get_system_info())
|
||||||
|
|
||||||
def set_translations(self, translations: dict[str, str]) -> None:
|
def set_translations(self, translations: Dict[str, str]) -> None:
|
||||||
"""Set stock translation strings."""
|
"""Set stock translation strings."""
|
||||||
self.rpc.set_stock_strings(translations)
|
self.rpc.set_stock_strings(translations)
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
"""High-level classes for event processing and filtering."""
|
"""High-level classes for event processing and filtering."""
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
import re
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Union
|
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
|
||||||
|
|
||||||
from .const import EventType
|
from .const import EventType
|
||||||
|
|
||||||
@@ -36,7 +34,7 @@ class EventFilter(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
"""Object's unique hash."""
|
"""Object's unique hash"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def __eq__(self, other) -> bool:
|
def __eq__(self, other) -> bool:
|
||||||
@@ -52,7 +50,9 @@ class EventFilter(ABC):
|
|||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def filter(self, event):
|
def filter(self, event):
|
||||||
"""Return True-like value if the event passed the filter."""
|
"""Return True-like value if the event passed the filter and should be
|
||||||
|
used, or False-like value otherwise.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class RawEvent(EventFilter):
|
class RawEvent(EventFilter):
|
||||||
@@ -80,17 +80,31 @@ class RawEvent(EventFilter):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
"""Filter an event.
|
|
||||||
|
|
||||||
Return true if the event should be processed.
|
|
||||||
"""
|
|
||||||
if self.types and event.kind not in self.types:
|
if self.types and event.kind not in self.types:
|
||||||
return False
|
return False
|
||||||
return self._call_func(event)
|
return self._call_func(event)
|
||||||
|
|
||||||
|
|
||||||
class NewMessage(EventFilter):
|
class NewMessage(EventFilter):
|
||||||
"""Matches whenever a new message arrives."""
|
"""Matches whenever a new message arrives.
|
||||||
|
|
||||||
|
Warning: registering a handler for this event will cause the messages
|
||||||
|
to be marked as read. Its usage is mainly intended for bots.
|
||||||
|
|
||||||
|
:param pattern: if set, this Pattern will be used to filter the message by its text
|
||||||
|
content.
|
||||||
|
:param command: If set, only match messages with the given command (ex. /help).
|
||||||
|
Setting this property implies `is_info==False`.
|
||||||
|
:param is_bot: If set to True only match messages sent by bots, if set to None
|
||||||
|
match messages from bots and users. If omitted or set to False
|
||||||
|
only messages from users will be matched.
|
||||||
|
:param is_info: If set to True only match info/system messages, if set to False
|
||||||
|
only match messages that are not info/system messages. If omitted
|
||||||
|
info/system messages as well as normal messages will be matched.
|
||||||
|
:param func: A Callable function that should accept the event as input
|
||||||
|
parameter, and return a bool value indicating whether the event
|
||||||
|
should be dispatched or not.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
@@ -105,25 +119,6 @@ class NewMessage(EventFilter):
|
|||||||
is_info: Optional[bool] = None,
|
is_info: Optional[bool] = None,
|
||||||
func: Optional[Callable[["AttrDict"], bool]] = None,
|
func: Optional[Callable[["AttrDict"], bool]] = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Initialize a new message filter.
|
|
||||||
|
|
||||||
Warning: registering a handler for this event will cause the messages
|
|
||||||
to be marked as read. Its usage is mainly intended for bots.
|
|
||||||
|
|
||||||
:param pattern: if set, this Pattern will be used to filter the message by its text
|
|
||||||
content.
|
|
||||||
:param command: If set, only match messages with the given command (ex. /help).
|
|
||||||
Setting this property implies `is_info==False`.
|
|
||||||
:param is_bot: If set to True only match messages sent by bots, if set to None
|
|
||||||
match messages from bots and users. If omitted or set to False
|
|
||||||
only messages from users will be matched.
|
|
||||||
:param is_info: If set to True only match info/system messages, if set to False
|
|
||||||
only match messages that are not info/system messages. If omitted
|
|
||||||
info/system messages as well as normal messages will be matched.
|
|
||||||
:param func: A Callable function that should accept the event as input
|
|
||||||
parameter, and return a bool value indicating whether the event
|
|
||||||
should be dispatched or not.
|
|
||||||
"""
|
|
||||||
super().__init__(func=func)
|
super().__init__(func=func)
|
||||||
self.is_bot = is_bot
|
self.is_bot = is_bot
|
||||||
self.is_info = is_info
|
self.is_info = is_info
|
||||||
@@ -162,7 +157,6 @@ class NewMessage(EventFilter):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
"""Return true if if the event is a new message event."""
|
|
||||||
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
|
if self.is_bot is not None and self.is_bot != event.message_snapshot.is_bot:
|
||||||
return False
|
return False
|
||||||
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
|
if self.is_info is not None and self.is_info != event.message_snapshot.is_info:
|
||||||
@@ -203,7 +197,6 @@ class MemberListChanged(EventFilter):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
"""Return true if if the event is a member addition event."""
|
|
||||||
if self.added is not None and self.added != event.member_added:
|
if self.added is not None and self.added != event.member_added:
|
||||||
return False
|
return False
|
||||||
return self._call_func(event)
|
return self._call_func(event)
|
||||||
@@ -236,7 +229,6 @@ class GroupImageChanged(EventFilter):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
"""Return True if event is matched."""
|
|
||||||
if self.deleted is not None and self.deleted != event.image_deleted:
|
if self.deleted is not None and self.deleted != event.image_deleted:
|
||||||
return False
|
return False
|
||||||
return self._call_func(event)
|
return self._call_func(event)
|
||||||
@@ -262,17 +254,18 @@ class GroupNameChanged(EventFilter):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
"""Return True if event is matched."""
|
|
||||||
return self._call_func(event)
|
return self._call_func(event)
|
||||||
|
|
||||||
|
|
||||||
class HookCollection:
|
class HookCollection:
|
||||||
"""Helper class to collect event hooks that can later be added to a Delta Chat client."""
|
"""
|
||||||
|
Helper class to collect event hooks that can later be added to a Delta Chat client.
|
||||||
|
"""
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
self._hooks: set[tuple[Callable, Union[type, EventFilter]]] = set()
|
self._hooks: Set[Tuple[Callable, Union[type, EventFilter]]] = set()
|
||||||
|
|
||||||
def __iter__(self) -> Iterator[tuple[Callable, Union[type, EventFilter]]]:
|
def __iter__(self) -> Iterator[Tuple[Callable, Union[type, EventFilter]]]:
|
||||||
return iter(self._hooks)
|
return iter(self._hooks)
|
||||||
|
|
||||||
def on(self, event: Union[type, EventFilter]) -> Callable: # noqa
|
def on(self, event: Union[type, EventFilter]) -> Callable: # noqa
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user