mirror of
https://github.com/chatmail/core.git
synced 2026-05-08 09:26:29 +03:00
Merge branch 'master' into flub/ongoing-guard
This commit is contained in:
8
.gitattributes
vendored
8
.gitattributes
vendored
@@ -2,6 +2,14 @@
|
|||||||
# ensures this even if the user has not set core.autocrlf.
|
# ensures this even if the user has not set core.autocrlf.
|
||||||
* text=auto
|
* text=auto
|
||||||
|
|
||||||
|
# Checkout JavaScript files with LF line endings
|
||||||
|
# to prevent `prettier` from reporting errors on Windows.
|
||||||
|
*.js eol=lf
|
||||||
|
*.jsx eol=lf
|
||||||
|
*.ts eol=lf
|
||||||
|
*.tsx eol=lf
|
||||||
|
*.json eol=lf
|
||||||
|
|
||||||
# This directory contains email messages verbatim, and changing CRLF to
|
# This directory contains email messages verbatim, and changing CRLF to
|
||||||
# LF will corrupt them.
|
# LF will corrupt them.
|
||||||
test-data/** text=false
|
test-data/** text=false
|
||||||
|
|||||||
2
.github/dependabot.yml
vendored
2
.github/dependabot.yml
vendored
@@ -5,5 +5,5 @@ updates:
|
|||||||
schedule:
|
schedule:
|
||||||
interval: "monthly"
|
interval: "monthly"
|
||||||
commit-message:
|
commit-message:
|
||||||
prefix: "cargo"
|
prefix: "chore(cargo)"
|
||||||
open-pull-requests-limit: 50
|
open-pull-requests-limit: 50
|
||||||
|
|||||||
26
.github/mergeable.yml
vendored
26
.github/mergeable.yml
vendored
@@ -1,26 +0,0 @@
|
|||||||
version: 2
|
|
||||||
mergeable:
|
|
||||||
- when: pull_request.*
|
|
||||||
name: "Changelog check"
|
|
||||||
validate:
|
|
||||||
- do: or
|
|
||||||
validate:
|
|
||||||
- do: description
|
|
||||||
must_include:
|
|
||||||
regex: "#skip-changelog"
|
|
||||||
- do: and
|
|
||||||
validate:
|
|
||||||
- do: dependent
|
|
||||||
changed:
|
|
||||||
file: "src/**"
|
|
||||||
required: ["CHANGELOG.md"]
|
|
||||||
- do: dependent
|
|
||||||
changed:
|
|
||||||
file: "deltachat-ffi/src/**"
|
|
||||||
required: ["CHANGELOG.md"]
|
|
||||||
fail:
|
|
||||||
- do: checks
|
|
||||||
status: "action_required"
|
|
||||||
payload:
|
|
||||||
title: Changelog might need an update
|
|
||||||
summary: "Check if CHANGELOG.md needs an update or add #skip-changelog to the PR description."
|
|
||||||
42
.github/workflows/ci.yml
vendored
42
.github/workflows/ci.yml
vendored
@@ -1,3 +1,7 @@
|
|||||||
|
# GitHub Actions workflow to
|
||||||
|
# lint Rust and Python code
|
||||||
|
# and run Rust tests, Python tests and async Python tests.
|
||||||
|
|
||||||
name: Rust CI
|
name: Rust CI
|
||||||
|
|
||||||
# Cancel previously started workflow runs
|
# Cancel previously started workflow runs
|
||||||
@@ -11,6 +15,7 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
- stable
|
||||||
|
|
||||||
env:
|
env:
|
||||||
RUSTFLAGS: -Dwarnings
|
RUSTFLAGS: -Dwarnings
|
||||||
@@ -20,7 +25,7 @@ jobs:
|
|||||||
name: Lint Rust
|
name: Lint Rust
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
RUSTUP_TOOLCHAIN: 1.68.2
|
RUSTUP_TOOLCHAIN: 1.73.0
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
- name: Install rustfmt and clippy
|
- name: Install rustfmt and clippy
|
||||||
@@ -72,19 +77,15 @@ jobs:
|
|||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
rust: 1.68.2
|
rust: 1.73.0
|
||||||
- os: windows-latest
|
- os: windows-latest
|
||||||
rust: 1.68.2
|
rust: 1.73.0
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
rust: 1.68.2
|
rust: 1.73.0
|
||||||
|
|
||||||
# Minimum Supported Rust Version = 1.65.0
|
# Minimum Supported Rust Version = 1.67.0
|
||||||
#
|
|
||||||
# Minimum Supported Python Version = 3.7
|
|
||||||
# This is the minimum version for which manylinux Python wheels are
|
|
||||||
# built.
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
rust: 1.65.0
|
rust: 1.67.0
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
@@ -97,6 +98,8 @@ jobs:
|
|||||||
uses: swatinem/rust-cache@v2
|
uses: swatinem/rust-cache@v2
|
||||||
|
|
||||||
- name: Tests
|
- name: Tests
|
||||||
|
env:
|
||||||
|
RUST_BACKTRACE: 1
|
||||||
run: cargo test --workspace
|
run: cargo test --workspace
|
||||||
|
|
||||||
- name: Test cargo vendor
|
- name: Test cargo vendor
|
||||||
@@ -173,15 +176,15 @@ jobs:
|
|||||||
include:
|
include:
|
||||||
# Currently used Rust version.
|
# Currently used Rust version.
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: 3.11
|
python: 3.12
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: 3.11
|
python: 3.12
|
||||||
|
|
||||||
# PyPy tests
|
# PyPy tests
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: pypy3.9
|
python: pypy3.10
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: pypy3.9
|
python: pypy3.10
|
||||||
|
|
||||||
# Minimum Supported Python Version = 3.7
|
# 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
|
||||||
@@ -222,21 +225,18 @@ jobs:
|
|||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
include:
|
include:
|
||||||
# Currently used Rust version.
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: 3.11
|
python: 3.12
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: 3.11
|
python: 3.12
|
||||||
|
|
||||||
# PyPy tests
|
# PyPy tests
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: pypy3.9
|
python: pypy3.10
|
||||||
- os: macos-latest
|
- os: macos-latest
|
||||||
python: pypy3.9
|
python: pypy3.10
|
||||||
|
|
||||||
# Minimum Supported Python Version = 3.7
|
# Minimum Supported Python Version = 3.7
|
||||||
# This is the minimum version for which manylinux Python wheels are
|
|
||||||
# built. Test it with minimum supported Rust version.
|
|
||||||
- os: ubuntu-latest
|
- os: ubuntu-latest
|
||||||
python: 3.7
|
python: 3.7
|
||||||
|
|
||||||
|
|||||||
125
.github/workflows/deltachat-rpc-server.yml
vendored
125
.github/workflows/deltachat-rpc-server.yml
vendored
@@ -1,4 +1,10 @@
|
|||||||
# Manually triggered action to build deltachat-rpc-server binaries.
|
# GitHub Actions workflow
|
||||||
|
# to build `deltachat-rpc-server` binaries
|
||||||
|
# and upload them to the release.
|
||||||
|
#
|
||||||
|
# The workflow is automatically triggered on releases.
|
||||||
|
# It can also be triggered manually
|
||||||
|
# to produce binary artifacts for testing.
|
||||||
|
|
||||||
name: Build deltachat-rpc-server binaries
|
name: Build deltachat-rpc-server binaries
|
||||||
|
|
||||||
@@ -20,35 +26,17 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: Build
|
- name: Install ziglang
|
||||||
|
run: pip install wheel ziglang==0.11.0
|
||||||
|
|
||||||
|
- name: Build deltachat-rpc-server binaries
|
||||||
run: sh scripts/zig-rpc-server.sh
|
run: sh scripts/zig-rpc-server.sh
|
||||||
|
|
||||||
- name: Upload x86_64 binary
|
- name: Upload dist directory with Linux binaries
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v3
|
||||||
with:
|
with:
|
||||||
name: deltachat-rpc-server-x86_64
|
name: linux
|
||||||
path: target/x86_64-unknown-linux-musl/release/deltachat-rpc-server
|
path: dist/
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload i686 binary
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-i686
|
|
||||||
path: target/i686-unknown-linux-musl/release/deltachat-rpc-server
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload aarch64 binary
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-aarch64
|
|
||||||
path: target/aarch64-unknown-linux-musl/release/deltachat-rpc-server
|
|
||||||
if-no-files-found: error
|
|
||||||
|
|
||||||
- name: Upload armv7 binary
|
|
||||||
uses: actions/upload-artifact@v3
|
|
||||||
with:
|
|
||||||
name: deltachat-rpc-server-armv7
|
|
||||||
path: target/armv7-unknown-linux-musleabihf/release/deltachat-rpc-server
|
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
build_windows:
|
build_windows:
|
||||||
@@ -84,22 +72,89 @@ jobs:
|
|||||||
path: target/${{ matrix.target}}/release/${{ matrix.path }}
|
path: target/${{ matrix.target}}/release/${{ matrix.path }}
|
||||||
if-no-files-found: error
|
if-no-files-found: error
|
||||||
|
|
||||||
|
build_macos:
|
||||||
|
name: Build deltachat-rpc-server for macOS
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- arch: x86_64
|
||||||
|
- arch: aarch64
|
||||||
|
|
||||||
|
runs-on: macos-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Setup rust target
|
||||||
|
run: rustup target add ${{ matrix.arch }}-apple-darwin
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --release --package deltachat-rpc-server --target ${{ matrix.arch }}-apple-darwin --features vendored
|
||||||
|
|
||||||
|
- name: Upload binary
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-${{ matrix.arch }}-macos
|
||||||
|
path: target/${{ matrix.arch }}-apple-darwin/release/deltachat-rpc-server
|
||||||
|
if-no-files-found: error
|
||||||
|
|
||||||
publish:
|
publish:
|
||||||
name: Upload binaries to the release
|
name: Build wheels and upload binaries to the release
|
||||||
needs: ["build_linux", "build_windows"]
|
needs: ["build_linux", "build_windows", "build_macos"]
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
runs-on: "ubuntu-latest"
|
runs-on: "ubuntu-latest"
|
||||||
steps:
|
steps:
|
||||||
- name: Download built binaries
|
- uses: actions/checkout@v3
|
||||||
uses: "actions/download-artifact@v3"
|
|
||||||
|
|
||||||
- name: Compose dist/ directory
|
- name: Download Linux binaries
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: linux
|
||||||
|
path: dist/
|
||||||
|
|
||||||
|
- name: Download win32 binary
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-win32.exe
|
||||||
|
path: deltachat-rpc-server-win32.exe.d
|
||||||
|
|
||||||
|
- name: Download win64 binary
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-win64.exe
|
||||||
|
path: deltachat-rpc-server-win64.exe.d
|
||||||
|
|
||||||
|
- name: Download macOS binary for x86_64
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
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@v3
|
||||||
|
with:
|
||||||
|
name: deltachat-rpc-server-aarch64-macos
|
||||||
|
path: deltachat-rpc-server-aarch64-macos.d
|
||||||
|
|
||||||
|
- name: Flatten dist/ directory
|
||||||
run: |
|
run: |
|
||||||
mkdir dist
|
mv deltachat-rpc-server-win32.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win32.exe
|
||||||
for x in x86_64 i686 aarch64 armv7 win32.exe win64.exe; do
|
mv deltachat-rpc-server-win64.exe.d/deltachat-rpc-server.exe dist/deltachat-rpc-server-win64.exe
|
||||||
mv "deltachat-rpc-server-$x"/* "dist/deltachat-rpc-server-$x"
|
mv deltachat-rpc-server-x86_64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-x86_64-macos
|
||||||
done
|
mv deltachat-rpc-server-aarch64-macos.d/deltachat-rpc-server dist/deltachat-rpc-server-aarch64-macos
|
||||||
|
|
||||||
|
# Python 3.11 is needed for tomllib used in scripts/wheel-rpc-server.py
|
||||||
|
- name: Install python 3.12
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: 3.12
|
||||||
|
|
||||||
|
- name: Install wheel
|
||||||
|
run: pip install wheel
|
||||||
|
|
||||||
|
- name: Build deltachat-rpc-server Python wheels and source package
|
||||||
|
run: scripts/wheel-rpc-server.py
|
||||||
|
|
||||||
- name: List downloaded artifacts
|
- name: List downloaded artifacts
|
||||||
run: ls -l dist/
|
run: ls -l dist/
|
||||||
|
|||||||
3
.github/workflows/dependabot.yml
vendored
3
.github/workflows/dependabot.yml
vendored
@@ -1,3 +1,6 @@
|
|||||||
|
# GitHub Actions workflow
|
||||||
|
# to automatically approve PRs made by Dependabot.
|
||||||
|
|
||||||
name: Dependabot auto-approve
|
name: Dependabot auto-approve
|
||||||
on: pull_request
|
on: pull_request
|
||||||
|
|
||||||
|
|||||||
@@ -38,13 +38,12 @@ jobs:
|
|||||||
node --version
|
node --version
|
||||||
echo $DELTACHAT_JSONRPC_TAR_GZ
|
echo $DELTACHAT_JSONRPC_TAR_GZ
|
||||||
- name: Install dependencies without running scripts
|
- name: Install dependencies without running scripts
|
||||||
run: |
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
cd deltachat-jsonrpc/typescript
|
run: npm install --ignore-scripts
|
||||||
npm install --ignore-scripts
|
|
||||||
- name: Package
|
- name: Package
|
||||||
shell: bash
|
shell: bash
|
||||||
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
run: |
|
run: |
|
||||||
cd deltachat-jsonrpc/typescript
|
|
||||||
npm run build
|
npm run build
|
||||||
npm pack .
|
npm pack .
|
||||||
ls -lah
|
ls -lah
|
||||||
|
|||||||
25
.github/workflows/jsonrpc.yml
vendored
25
.github/workflows/jsonrpc.yml
vendored
@@ -22,24 +22,19 @@ jobs:
|
|||||||
- name: Add Rust cache
|
- name: Add Rust cache
|
||||||
uses: Swatinem/rust-cache@v2
|
uses: Swatinem/rust-cache@v2
|
||||||
- name: npm install
|
- name: npm install
|
||||||
run: |
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
cd deltachat-jsonrpc/typescript
|
run: npm install
|
||||||
npm install
|
|
||||||
- name: Build TypeScript, run Rust tests, generate bindings
|
- name: Build TypeScript, run Rust tests, generate bindings
|
||||||
run: |
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
cd deltachat-jsonrpc/typescript
|
run: npm run build
|
||||||
npm run build
|
|
||||||
- name: Run integration tests
|
- name: Run integration tests
|
||||||
run: |
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
cd deltachat-jsonrpc/typescript
|
run: npm run test
|
||||||
npm run test
|
|
||||||
env:
|
env:
|
||||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||||
- name: make sure websocket server version still builds
|
- name: make sure websocket server version still builds
|
||||||
run: |
|
working-directory: deltachat-jsonrpc
|
||||||
cd deltachat-jsonrpc
|
run: cargo build --bin deltachat-jsonrpc-server --features webserver
|
||||||
cargo build --bin deltachat-jsonrpc-server --features webserver
|
|
||||||
- name: Run linter
|
- name: Run linter
|
||||||
run: |
|
working-directory: deltachat-jsonrpc/typescript
|
||||||
cd deltachat-jsonrpc/typescript
|
run: npm run prettier:check
|
||||||
npm run prettier:check
|
|
||||||
|
|||||||
31
.github/workflows/node-delete-preview.yml
vendored
31
.github/workflows/node-delete-preview.yml
vendored
@@ -1,31 +0,0 @@
|
|||||||
# documentation: https://github.com/deltachat/sysadmin/tree/master/download.delta.chat
|
|
||||||
name: Delete node PR previews
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [closed]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
delete:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Get Pull Request ID
|
|
||||||
id: getid
|
|
||||||
run: |
|
|
||||||
export PULLREQUEST_ID=$(jq .number < $GITHUB_EVENT_PATH)
|
|
||||||
echo "prid=$PULLREQUEST_ID" >> $GITHUB_OUTPUT
|
|
||||||
- name: Renaming
|
|
||||||
run: |
|
|
||||||
# create empty file to copy it over the outdated deliverable on download.delta.chat
|
|
||||||
echo "This preview build is outdated and has been removed." > empty
|
|
||||||
cp empty deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz
|
|
||||||
- name: Replace builds with dummy files
|
|
||||||
uses: horochx/deploy-via-scp@v1.0.1
|
|
||||||
with:
|
|
||||||
user: ${{ secrets.USERNAME }}
|
|
||||||
key: ${{ secrets.SSH_KEY }}
|
|
||||||
host: "download.delta.chat"
|
|
||||||
port: 22
|
|
||||||
local: "deltachat-node-${{ steps.getid.outputs.prid }}.tar.gz"
|
|
||||||
remote: "/var/www/html/download/node/preview/"
|
|
||||||
7
.github/workflows/node-docs.yml
vendored
7
.github/workflows/node-docs.yml
vendored
@@ -1,3 +1,8 @@
|
|||||||
|
# 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
|
name: Generate & upload node.js documentation
|
||||||
|
|
||||||
on:
|
on:
|
||||||
@@ -17,8 +22,8 @@ jobs:
|
|||||||
node-version: 16.x
|
node-version: 16.x
|
||||||
|
|
||||||
- name: npm install and generate documentation
|
- name: npm install and generate documentation
|
||||||
|
working-directory: node
|
||||||
run: |
|
run: |
|
||||||
cd node
|
|
||||||
npm i --ignore-scripts
|
npm i --ignore-scripts
|
||||||
npx typedoc
|
npx typedoc
|
||||||
mv docs js
|
mv docs js
|
||||||
|
|||||||
92
.github/workflows/node-package.yml
vendored
92
.github/workflows/node-package.yml
vendored
@@ -12,7 +12,7 @@ jobs:
|
|||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-20.04, macos-latest, windows-latest]
|
os: [macos-latest, windows-latest]
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
@@ -46,13 +46,12 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies & build
|
- name: Install dependencies & build
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
run: |
|
working-directory: node
|
||||||
cd node
|
run: npm install --verbose
|
||||||
npm install --verbose
|
|
||||||
|
|
||||||
- name: Build Prebuild
|
- name: Build Prebuild
|
||||||
|
working-directory: node
|
||||||
run: |
|
run: |
|
||||||
cd node
|
|
||||||
npm run prebuildify
|
npm run prebuildify
|
||||||
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
|
tar -zcvf "${{ matrix.os }}.tar.gz" -C prebuilds .
|
||||||
|
|
||||||
@@ -62,10 +61,81 @@ jobs:
|
|||||||
name: ${{ matrix.os }}
|
name: ${{ matrix.os }}
|
||||||
path: node/${{ matrix.os }}.tar.gz
|
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 at the time of the writing (2023-06-04): https://packages.debian.org/buster/libc6
|
||||||
|
# Ubuntu 18.04 is at the End of Standard Support since June 2023, but it contains glibc 2.27,
|
||||||
|
# so we are using it to support Ubuntu 18.04 setups that are still not upgraded.
|
||||||
|
container: ubuntu:18.04
|
||||||
|
steps:
|
||||||
|
# Working directory is owned by 1001:1001 by default.
|
||||||
|
# Change it to our user.
|
||||||
|
- name: Change working directory owner
|
||||||
|
run: chown root:root .
|
||||||
|
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- uses: actions/setup-node@v3
|
||||||
|
with:
|
||||||
|
node-version: "16"
|
||||||
|
- 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@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
${{ env.APPDATA }}/npm-cache
|
||||||
|
~/.npm
|
||||||
|
key: ${{ matrix.os }}-node-${{ hashFiles('**/package.json') }}
|
||||||
|
|
||||||
|
- name: Cache cargo index
|
||||||
|
uses: actions/cache@v3
|
||||||
|
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@v3
|
||||||
|
with:
|
||||||
|
name: linux
|
||||||
|
path: node/linux.tar.gz
|
||||||
|
|
||||||
pack-module:
|
pack-module:
|
||||||
needs: prebuild
|
needs: [prebuild, prebuild-linux]
|
||||||
name: Package deltachat-node and upload to download.delta.chat
|
name: Package deltachat-node and upload to download.delta.chat
|
||||||
runs-on: ubuntu-20.04
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Install tree
|
- name: Install tree
|
||||||
run: sudo apt install tree
|
run: sudo apt install tree
|
||||||
@@ -96,10 +166,10 @@ jobs:
|
|||||||
npm --version
|
npm --version
|
||||||
node --version
|
node --version
|
||||||
echo $DELTACHAT_NODE_TAR_GZ
|
echo $DELTACHAT_NODE_TAR_GZ
|
||||||
- name: Download Ubuntu prebuild
|
- name: Download Linux prebuild
|
||||||
uses: actions/download-artifact@v1
|
uses: actions/download-artifact@v1
|
||||||
with:
|
with:
|
||||||
name: ubuntu-20.04
|
name: linux
|
||||||
- name: Download macOS prebuild
|
- name: Download macOS prebuild
|
||||||
uses: actions/download-artifact@v1
|
uses: actions/download-artifact@v1
|
||||||
with:
|
with:
|
||||||
@@ -111,11 +181,11 @@ jobs:
|
|||||||
- shell: bash
|
- shell: bash
|
||||||
run: |
|
run: |
|
||||||
mkdir node/prebuilds
|
mkdir node/prebuilds
|
||||||
tar -xvzf ubuntu-20.04/ubuntu-20.04.tar.gz -C node/prebuilds
|
tar -xvzf linux/linux.tar.gz -C node/prebuilds
|
||||||
tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
|
tar -xvzf macos-latest/macos-latest.tar.gz -C node/prebuilds
|
||||||
tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
|
tar -xvzf windows-latest/windows-latest.tar.gz -C node/prebuilds
|
||||||
tree node/prebuilds
|
tree node/prebuilds
|
||||||
rm -rf ubuntu-20.04 macos-latest windows-latest
|
rm -rf linux macos-latest windows-latest
|
||||||
- name: Install dependencies without running scripts
|
- name: Install dependencies without running scripts
|
||||||
run: |
|
run: |
|
||||||
npm install --ignore-scripts
|
npm install --ignore-scripts
|
||||||
|
|||||||
23
.github/workflows/node-tests.yml
vendored
23
.github/workflows/node-tests.yml
vendored
@@ -1,3 +1,6 @@
|
|||||||
|
# GitHub Actions workflow
|
||||||
|
# to test Node.js bindings.
|
||||||
|
|
||||||
name: "node.js tests"
|
name: "node.js tests"
|
||||||
|
|
||||||
# Cancel previously started workflow runs
|
# Cancel previously started workflow runs
|
||||||
@@ -52,25 +55,13 @@ jobs:
|
|||||||
|
|
||||||
- name: Install dependencies & build
|
- name: Install dependencies & build
|
||||||
if: steps.cache.outputs.cache-hit != 'true'
|
if: steps.cache.outputs.cache-hit != 'true'
|
||||||
run: |
|
working-directory: node
|
||||||
cd node
|
run: npm install --verbose
|
||||||
npm install --verbose
|
|
||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
timeout-minutes: 10
|
timeout-minutes: 10
|
||||||
if: runner.os != 'Windows'
|
working-directory: node
|
||||||
run: |
|
run: npm run test
|
||||||
cd node
|
|
||||||
npm run test
|
|
||||||
env:
|
|
||||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
|
||||||
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"
|
|
||||||
- name: Run tests on Windows, except lint
|
|
||||||
timeout-minutes: 10
|
|
||||||
if: runner.os == 'Windows'
|
|
||||||
run: |
|
|
||||||
cd node
|
|
||||||
npm run test:mocha
|
|
||||||
env:
|
env:
|
||||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
||||||
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"
|
NODE_OPTIONS: "--force-node-api-uncaught-exceptions-policy=true"
|
||||||
|
|||||||
3
.github/workflows/repl.yml
vendored
3
.github/workflows/repl.yml
vendored
@@ -1,4 +1,5 @@
|
|||||||
# Manually triggered action to build a Windows repl.exe which users can
|
# Manually triggered GitHub Actions workflow
|
||||||
|
# to build a Windows repl.exe which users can
|
||||||
# download to debug complex bugs.
|
# download to debug complex bugs.
|
||||||
|
|
||||||
name: Build Windows REPL .exe
|
name: Build Windows REPL .exe
|
||||||
|
|||||||
4
.github/workflows/upload-ffi-docs.yml
vendored
4
.github/workflows/upload-ffi-docs.yml
vendored
@@ -1,3 +1,7 @@
|
|||||||
|
# GitHub Actions workflow
|
||||||
|
# to build `deltachat_fii` crate documentation
|
||||||
|
# and upload it to <https://cffi.delta.chat/>
|
||||||
|
|
||||||
name: Build & Deploy Documentation on cffi.delta.chat
|
name: Build & Deploy Documentation on cffi.delta.chat
|
||||||
|
|
||||||
on:
|
on:
|
||||||
|
|||||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,6 +1,7 @@
|
|||||||
/target
|
/target
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
/build
|
/build
|
||||||
|
/dist
|
||||||
|
|
||||||
# ignore vi temporaries
|
# ignore vi temporaries
|
||||||
*~
|
*~
|
||||||
@@ -18,6 +19,9 @@ python/.eggs
|
|||||||
__pycache__
|
__pycache__
|
||||||
python/src/deltachat/capi*.so
|
python/src/deltachat/capi*.so
|
||||||
python/.venv/
|
python/.venv/
|
||||||
|
python/venv/
|
||||||
|
venv/
|
||||||
|
env/
|
||||||
|
|
||||||
python/liveconfig*
|
python/liveconfig*
|
||||||
|
|
||||||
|
|||||||
604
CHANGELOG.md
604
CHANGELOG.md
@@ -1,33 +1,579 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## [Unreleased]
|
## [1.126.1] - 2023-10-24
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Do not hardcode version in deltachat-rpc-server source package.
|
||||||
|
- Do not interrupt IMAP loop from `get_connectivity_html()`.
|
||||||
|
|
||||||
|
### Features / Changes
|
||||||
|
|
||||||
|
- imap: Buffer `STARTTLS` command.
|
||||||
|
|
||||||
|
### Build system
|
||||||
|
|
||||||
|
- Build `deltachat-rpc-server` binary for aarch64 macOS.
|
||||||
|
- Build `deltachat-rpc-server` wheels for macOS and Windows.
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Remove job queue.
|
||||||
|
|
||||||
|
### Miscellaneous Tasks
|
||||||
|
|
||||||
|
- cargo: Update `ahash` to make `cargo-deny` happy.
|
||||||
|
|
||||||
|
## [1.126.0] - 2023-10-22
|
||||||
|
|
||||||
|
### API-Changes
|
||||||
|
|
||||||
|
- Allow to filter by unread in `chatlist:try_load` ([#4824](https://github.com/deltachat/deltachat-core-rust/pull/4824)).
|
||||||
|
- Add `misc_send_draft()` to JSON-RPC API ([#4839](https://github.com/deltachat/deltachat-core-rust/pull/4839)).
|
||||||
|
|
||||||
|
### Features / Changes
|
||||||
|
|
||||||
|
- [**breaking**] Make broadcast lists create their own chat ([#4644](https://github.com/deltachat/deltachat-core-rust/pull/4644)).
|
||||||
|
- This means that UIs need to ask for the name when creating a broadcast list, similar to <https://github.com/deltachat/deltachat-android/pull/2653>.
|
||||||
|
- Add self-address to backup filename ([#4820](https://github.com/deltachat/deltachat-core-rust/pull/4820))
|
||||||
|
|
||||||
|
### CI
|
||||||
|
|
||||||
|
- Build Python wheels for deltachat-rpc-server.
|
||||||
|
|
||||||
|
### Build system
|
||||||
|
|
||||||
|
- Strip release binaries.
|
||||||
|
- Workaround OpenSSL crate expecting libatomic to be available.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Set `soft_heap_limit` on SQLite database.
|
||||||
|
- imap: Fallback to `STATUS` if `SELECT` did not return UIDNEXT.
|
||||||
|
|
||||||
|
## [1.125.0] - 2023-10-14
|
||||||
|
|
||||||
|
### API-Changes
|
||||||
|
|
||||||
|
- [**breaking**] deltachat-rpc-client: Replace `asyncio` with threads.
|
||||||
|
- Validate boolean values passed to `set_config`. Attempts to set values other than `0` and `1` will result in an error.
|
||||||
|
|
||||||
|
### CI
|
||||||
|
|
||||||
|
- Reduce required Python version for deltachat-rpc-client from 3.8 to 3.7.
|
||||||
|
|
||||||
|
### Features / Changes
|
||||||
|
|
||||||
|
- Add developer option to disable IDLE.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- `deltachat-rpc-client`: Run `deltachat-rpc-server` in its own process group. This prevents reception of `SIGINT` by the server when the bot is terminated with `^C`.
|
||||||
|
- python: Don't automatically set the displayname to "bot" when setting log level.
|
||||||
|
- Don't update `timestamp`, `timestamp_rcvd`, `state` when replacing partially downloaded message ([#4700](https://github.com/deltachat/deltachat-core-rust/pull/4700)).
|
||||||
|
- Assign encrypted partially downloaded group messages to 1:1 chat ([#4757](https://github.com/deltachat/deltachat-core-rust/pull/4757)).
|
||||||
|
- Return all contacts from `Contact::get_all` for bots ([#4811](https://github.com/deltachat/deltachat-core-rust/pull/4811)).
|
||||||
|
- Set connectivity status to "connected" during fake idle.
|
||||||
|
- Return verifier contacts regardless of their origin.
|
||||||
|
- Don't try to send more MDNs if there's a temporary SMTP error ([#4534](https://github.com/deltachat/deltachat-core-rust/pull/4534)).
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- deltachat-rpc-client: Close stdin instead of sending `SIGTERM`.
|
||||||
|
- deltachat-rpc-client: Remove print() calls. Standard `logging` package is for logging instead.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- deltachat-rpc-client: Enable logs in pytest.
|
||||||
|
|
||||||
|
## [1.124.1] - 2023-10-05
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Remove footer from reactions on the receiver side ([#4780](https://github.com/deltachat/deltachat-core-rust/pull/4780)).
|
||||||
|
|
||||||
|
### CI
|
||||||
|
|
||||||
|
- Pin `urllib3` version to `<2`. ([#4788](https://github.com/deltachat/deltachat-core-rust/issues/4788))
|
||||||
|
|
||||||
|
## [1.124.0] - 2023-10-04
|
||||||
|
|
||||||
|
### API-Changes
|
||||||
|
|
||||||
|
- [**breaking**] Return `DC_CONTACT_ID_SELF` from `dc_contact_get_verifier_id()` for directly verified contacts.
|
||||||
|
- Deprecate `dc_contact_get_verifier_addr`.
|
||||||
|
- python: use `dc_contact_get_verifier_id()`. `get_verifier()` returns a Contact rather than an address now.
|
||||||
|
- Deprecate `get_next_media()`.
|
||||||
|
- Ignore public key argument in `dc_preconfigure_keypair()`. Public key is extracted from the private key.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Wrap base64-encoded parts to 76 characters.
|
||||||
|
- Require valid email addresses in `dc_provider_new_from_email[_with_dns]`.
|
||||||
|
- Do not trash messages with attachments and no text when `location.kml` is attached ([#4749](https://github.com/deltachat/deltachat-core-rust/issues/4749)).
|
||||||
|
- Initialise `last_msg_id` to the highest known row id. This ensures bots migrated from older version to `dc_get_next_msgs()` API do not process all previous messages from scratch.
|
||||||
|
- Do not put the status footer into reaction MIME parts.
|
||||||
|
- Ignore special chats in `get_similar_chat_ids()`. This prevents trash chat from showing up in similar chat list ([#4756](https://github.com/deltachat/deltachat-core-rust/issues/4756)).
|
||||||
|
- Cap percentage in connectivity layout to 100% ([#4765](https://github.com/deltachat/deltachat-core-rust/pull/4765)).
|
||||||
|
- Add Let's Encrypt root certificate to `reqwest`. This should allow scanning `DCACCOUNT` QR-codes on older Android phones when the server has a Let's Encrypt certificate.
|
||||||
|
- deltachat-rpc-client: Increase stdio buffer to 64 MiB to avoid Python bots crashing when trying to load large messages via a JSON-RPC call.
|
||||||
|
- Add `protected-headers` directive to Content-Type of encrypted messages with attachments ([#2302](https://github.com/deltachat/deltachat-core-rust/issues/2302)). This makes Thunderbird show encrypted Subject for Delta Chat messages.
|
||||||
|
- webxdc: Reset `document.update` on forwarding. This fixes the test `test_forward_webxdc_instance()`.
|
||||||
|
|
||||||
|
### Features / Changes
|
||||||
|
|
||||||
|
- Remove extra members from the local list in sake of group membership consistency ([#3782](https://github.com/deltachat/deltachat-core-rust/issues/3782)).
|
||||||
|
- deltachat-rpc-client: Log exceptions when long-running tasks die.
|
||||||
|
|
||||||
|
### Build
|
||||||
|
|
||||||
|
- Build wheels for Python 3.12 and PyPy 3.10.
|
||||||
|
|
||||||
|
## [1.123.0] - 2023-09-22
|
||||||
|
|
||||||
|
### API-Changes
|
||||||
|
|
||||||
|
- Make it possible to import secret key from a file with `DC_IMEX_IMPORT_SELF_KEYS`.
|
||||||
|
- [**breaking**] Make `dc_jsonrpc_blocking_call` accept JSON-RPC request.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- `lookup_chat_by_reply()`: Skip not fully downloaded and undecipherable messages ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
|
||||||
|
- `lookup_chat_by_reply()`: Skip undecipherable parent messages created by older versions ([#4676](https://github.com/deltachat/deltachat-core-rust/pull/4676)).
|
||||||
|
- imex: Use "default" in the filename of the default key.
|
||||||
|
|
||||||
|
### Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Update OpenSSL from 3.1.2 to 3.1.3.
|
||||||
|
|
||||||
|
## [1.122.0] - 2023-09-12
|
||||||
|
|
||||||
|
### API-Changes
|
||||||
|
|
||||||
|
- jsonrpc: Return only chat IDs for similar chats.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Reopen all connections on database passpharse change.
|
||||||
|
- Do not block new group chats if 1:1 chat is blocked.
|
||||||
|
- Improve group membership consistency algorithm ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782))([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
|
||||||
|
- Forbid membership changes from possible non-members ([#3782](https://github.com/deltachat/deltachat-core-rust/pull/3782)).
|
||||||
|
- `ChatId::parent_query()`: Don't filter out OutPending and OutFailed messages.
|
||||||
|
|
||||||
|
### Build system
|
||||||
|
|
||||||
|
- Update to OpenSSL 3.0.
|
||||||
|
- Bump webpki from 0.22.0 to 0.22.1.
|
||||||
|
- python: Add link to Mastodon into projects.urls.
|
||||||
|
|
||||||
|
### Features / Changes
|
||||||
|
|
||||||
|
- Add RSA-4096 key generation support.
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- pgp: Add constants for encryption algorithm and hash.
|
||||||
|
|
||||||
|
## [1.121.0] - 2023-09-06
|
||||||
|
|
||||||
|
### API-Changes
|
||||||
|
|
||||||
|
- Add `dc_context_change_passphrase()`.
|
||||||
|
- Add `Message.set_file_from_bytes()` API.
|
||||||
|
- Add experimental API to get similar chats.
|
||||||
|
|
||||||
|
### Build system
|
||||||
|
|
||||||
|
- Build node packages on Ubuntu 18.04 instead of Debian 10.
|
||||||
|
This reduces the requirement for glibc version from 2.28 to 2.27.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Allow membership changes by a MUA if we're not in the group ([#4624](https://github.com/deltachat/deltachat-core-rust/pull/4624)).
|
||||||
|
- Save mime headers for messages not signed with a known key ([#4557](https://github.com/deltachat/deltachat-core-rust/pull/4557)).
|
||||||
|
- Return from `dc_get_chatlist(DC_GCL_FOR_FORWARDING)` only chats where we can send ([#4616](https://github.com/deltachat/deltachat-core-rust/pull/4616)).
|
||||||
|
- Do not allow dots at the end of email addresses.
|
||||||
|
- deltachat-rpc-client: Remove `aiodns` optional dependency from required dependencies.
|
||||||
|
`aiodns` depends on `pycares` which [fails to install in Termux](https://github.com/saghul/aiodns/issues/98).
|
||||||
|
|
||||||
|
## [1.120.0] - 2023-08-28
|
||||||
|
|
||||||
|
### API-Changes
|
||||||
|
|
||||||
|
- jsonrpc: Add `resend_messages`.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Update async-imap to 0.9.1 to fix memory leak.
|
||||||
|
- Delete messages from SMTP queue only on user demand ([#4579](https://github.com/deltachat/deltachat-core-rust/pull/4579)).
|
||||||
|
- Do not send images without transparency as stickers ([#4611](https://github.com/deltachat/deltachat-core-rust/pull/4611)).
|
||||||
|
- `prepare_msg_blob()`: do not use the image if it has Exif metadata but the image cannot be recoded.
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Hide accounts.rs constants from public API.
|
||||||
|
- Hide pgp module from public API.
|
||||||
|
|
||||||
|
### Build system
|
||||||
|
|
||||||
|
- Update to Zig 0.11.0.
|
||||||
|
- Update to Rust 1.72.0.
|
||||||
|
|
||||||
|
### CI
|
||||||
|
|
||||||
|
- Run on push to stable branch.
|
||||||
|
|
||||||
|
### Miscellaneous Tasks
|
||||||
|
|
||||||
|
- python: Fix lint errors.
|
||||||
|
- python: Fix `ruff` 0.0.286 warnings.
|
||||||
|
- Fix beta clippy warnings.
|
||||||
|
|
||||||
|
## [1.119.1] - 2023-08-06
|
||||||
|
|
||||||
|
Bugfix release attempting to fix the [iOS build error](https://github.com/deltachat/deltachat-core-rust/issues/4610).
|
||||||
|
|
||||||
|
### Features / Changes
|
||||||
|
|
||||||
|
- Guess message viewtype from "application/octet-stream" attachment extension ([#4378](https://github.com/deltachat/deltachat-core-rust/pull/4378)).
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Update `xattr` from 1.0.0 to 1.0.1 to fix UnsupportedPlatformError import.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- webxdc: Ensure unknown WebXDC update properties do not result in an error.
|
||||||
|
|
||||||
|
## [1.119.0] - 2023-08-03
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- imap: Avoid IMAP move loops when DeltaChat folder is aliased.
|
||||||
|
- imap: Do not resync IMAP after initial configuration.
|
||||||
|
|
||||||
|
- webxdc: Accept WebXDC updates in mailing lists.
|
||||||
|
- webxdc: Base64-encode WebXDC updates to prevent corruption of large unencrypted WebXDC updates.
|
||||||
|
- webxdc: Delete old webxdc status updates during housekeeping.
|
||||||
|
|
||||||
|
- Return valid MsgId from `receive_imf()` when the message is replaced.
|
||||||
|
- Emit MsgsChanged event with correct chat id for replaced messages.
|
||||||
|
|
||||||
|
- deltachat-rpc-server: Update tokio-tar to fix backup import.
|
||||||
|
|
||||||
|
### Features / Changes
|
||||||
|
|
||||||
|
- deltachat-rpc-client: Add `MSG_DELETED` constant.
|
||||||
|
- Make `dc_msg_get_filename()` return the original attachment filename ([#4309](https://github.com/deltachat/deltachat-core-rust/pull/4309)).
|
||||||
|
|
||||||
|
### API-Changes
|
||||||
|
|
||||||
|
- deltachat-rpc-client: Add `Account.{import,export}_backup` methods.
|
||||||
|
- deltachat-jsonrpc: Make `MessageObject.text` non-optional.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Update default value for `show_emails` in `dc_set_config()` documentation.
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Improve IMAP logs.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- Add basic import/export test for async python.
|
||||||
|
- Add `test_webxdc_download_on_demand`.
|
||||||
|
- Add tests for deletion of webxdc status-updates.
|
||||||
|
|
||||||
|
## [1.118.0] - 2023-07-07
|
||||||
|
|
||||||
|
### API-Changes
|
||||||
|
|
||||||
|
- [**breaking**] Remove `Contact::load_from_db()` in favor of `Contact::get_by_id()`.
|
||||||
|
- Add `Contact::get_by_id_optional()` API.
|
||||||
|
- [**breaking**] Make `Message.text` non-optional.
|
||||||
|
- [**breaking**] Replace `message::get_msg_info()` with `MsgId.get_info()`.
|
||||||
|
- Move `handle_mdn` and `handle_ndn` to mimeparser and make them private.
|
||||||
|
Previously `handle_mdn` was erroneously exposed in the public API.
|
||||||
|
- python: flatten the API of `deltachat` module.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Use different member added/removal messages locally and on the network.
|
||||||
|
- Update tokio to 1.29.1 to fix core panic after sending 29 offline messages ([#4414](https://github.com/deltachat/deltachat-core-rust/issues/4414)).
|
||||||
|
- Make SVG avatar image work on more platforms (use `xlink:href`).
|
||||||
|
- Preserve indentation when converting plaintext to HTML.
|
||||||
|
- Do not run simplify() on dehtml() output.
|
||||||
|
- Rewrite member added/removed messages even if the change is not allowed PR ([#4529](https://github.com/deltachat/deltachat-core-rust/pull/4529)).
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Document how to regenerate Node.js constants before the release.
|
||||||
|
|
||||||
|
### Build system
|
||||||
|
|
||||||
|
- git-cliff: Do not fail if commit.footers is undefined.
|
||||||
|
|
||||||
|
### Other
|
||||||
|
|
||||||
|
- Dependency updates.
|
||||||
|
- Update MPL 2.0 license text.
|
||||||
|
- Add LICENSE file to deltachat-rpc-client.
|
||||||
|
- deltachat-rpc-client: Add Trove classifiers.
|
||||||
|
- python: Change bindings status to production/stable.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- Add `make-python-testenv.sh` script.
|
||||||
|
|
||||||
|
## [1.117.0] - 2023-06-15
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- New group membership update algorithm.
|
||||||
|
|
||||||
|
New algorithm improves group consistency
|
||||||
|
in cases of missing messages,
|
||||||
|
restored old backups and replies from classic MUAs.
|
||||||
|
|
||||||
|
- Add `DC_EVENT_MSG_DELETED` event.
|
||||||
|
|
||||||
|
This event notifies the UI about the message
|
||||||
|
being deleted from the messagelist, e.g. when the message expires
|
||||||
|
or the user deletes it.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Emit `DC_EVENT_MSGS_CHANGED` without IDs when the message expires.
|
||||||
|
|
||||||
|
Specifying msg IDs that cannot be loaded in the event payload
|
||||||
|
results in an error when the UI tries to load the message.
|
||||||
|
Instead, emit an event without IDs
|
||||||
|
to make the UI reload the whole messagelist.
|
||||||
|
|
||||||
|
- Ignore address case when comparing the `To:` field to `Autocrypt-Gossip:`.
|
||||||
|
|
||||||
|
This bug resulted in failure to propagate verification
|
||||||
|
if the contact list already contained a new verified group member
|
||||||
|
with a non-lowercase address.
|
||||||
|
|
||||||
|
- dehtml: skip links with empty text.
|
||||||
|
|
||||||
|
Links like `<a href="https://delta.chat/"></a>` in HTML mails are now skipped
|
||||||
|
instead of being converted to a link without a label like `[](https://delta.chat/)`.
|
||||||
|
|
||||||
|
- dehtml: Do not insert unnecessary newlines when parsing `<p>` tags.
|
||||||
|
|
||||||
|
- Update from yanked `libc` 0.2.145 to 0.2.146.
|
||||||
|
- Update to async-imap 0.9.0 to remove deprecated `ouroboros` dependency.
|
||||||
|
|
||||||
|
### API-Changes
|
||||||
|
|
||||||
|
- Emit `DC_EVENT_MSGS_CHANGED` per chat when messages are deleted.
|
||||||
|
|
||||||
|
Previously a single event with zero chat ID was emitted.
|
||||||
|
|
||||||
|
- python: make `Contact.is_verified()` return bool.
|
||||||
|
|
||||||
|
- rust: add API endpoint `get_status_update` ([#4468](https://github.com/deltachat/deltachat-core-rust/pull/4468)).
|
||||||
|
|
||||||
|
- rust: make `WebxdcManifest` type public.
|
||||||
|
|
||||||
|
### Build system
|
||||||
|
|
||||||
|
- Use Rust 1.70.0 to compile deltachat-rpc-server releases.
|
||||||
|
- Disable unused `brotli` feature `ffi-api` and use 1 codegen-units for release builds to reduce the size of the binaries.
|
||||||
|
|
||||||
|
### CI
|
||||||
|
|
||||||
|
- Run `cargo check` with musl libc.
|
||||||
|
- concourse: Install devpi in a virtual environment.
|
||||||
|
- Remove [mergeable](https://mergeable.us/) configuration.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- README: mark napi.rs bindings as experimental. CFFI bindings are not legacy and are the recommended Node.js bindings currently.
|
||||||
|
- CONTRIBUTING: document how conventional commits interact with squash merges.
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Rename `MimeMessage.header` into `MimeMessage.headers`.
|
||||||
|
|
||||||
|
- Derive `Default` trait for `WebxdcManifest`.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- Regression test for case-sensitive comparison of gossip header to contact address.
|
||||||
|
- Multiple new group consistency tests in Rust.
|
||||||
|
- python: Replace legacy `tmpdir` fixture with `tmp_path`.
|
||||||
|
|
||||||
|
## [1.116.0] - 2023-06-05
|
||||||
|
|
||||||
|
### API-Changes
|
||||||
|
|
||||||
|
- Add `dc_jsonrpc_blocking_call()`.
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Generate OpenRPC definitions for JSON-RPC.
|
||||||
|
- Add more context to message loading errors.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Build deltachat-node prebuilds on Debian 10.
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
|
||||||
|
- Document release process in `RELEASE.md`.
|
||||||
|
- Add contributing guidelines `CONTRIBUTING.md`.
|
||||||
|
- Update instructions for python devenv.
|
||||||
|
- python: Document pytest fixtures.
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
|
||||||
|
- python: Make `test_mdn_asymmetric` less flaky.
|
||||||
|
- Make `test_group_with_removed_message_id` less flaky.
|
||||||
|
- Add golden tests infrastructure ([#4395](https://github.com/deltachat/deltachat-core-rust/pull/4395)).
|
||||||
|
|
||||||
|
### Build system
|
||||||
|
|
||||||
|
- git-cliff: Changelog generation improvements.
|
||||||
|
- `set_core_version.py`: Expect release date in the changelog.
|
||||||
|
|
||||||
|
### CI
|
||||||
|
|
||||||
|
- Require Python 3.8 for deltachat-rpc-client.
|
||||||
|
- mergeable: Allow PR titles to start with "ci" and "build".
|
||||||
|
- Remove incorrect comment.
|
||||||
|
- dependabot: Use `chore` prefix for dependency updates.
|
||||||
|
- Remove broken `node-delete-preview.yml` workflow.
|
||||||
|
- Add top comments to GH Actions workflows.
|
||||||
|
- Run node.js lint on Windows.
|
||||||
|
- Update clippy to 1.70.0.
|
||||||
|
|
||||||
|
### Miscellaneous Tasks
|
||||||
|
|
||||||
|
- Remove release.toml.
|
||||||
|
- gitattributes: Configure LF line endings for JavaScript files.
|
||||||
|
- Update dependencies
|
||||||
|
|
||||||
|
## [1.112.10] - 2023-06-01
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Disable `fetch_existing_msgs` setting by default.
|
||||||
|
- Update `h2` to fix RUSTSEC-2023-0034.
|
||||||
|
|
||||||
|
## [1.115.0] - 2023-05-12
|
||||||
|
|
||||||
|
### JSON-RPC API Changes
|
||||||
|
|
||||||
|
- Sort reactions in descending order ([#4388](https://github.com/deltachat/deltachat-core-rust/pull/4388)).
|
||||||
|
- Add API to get reactions outside the message snapshot.
|
||||||
|
- `get_chatlist_items_by_entries` now takes only chatids instead of `ChatListEntries`.
|
||||||
|
- `get_chatlist_entries` now returns `Vec<u32>` of chatids instead of `ChatListEntries`.
|
||||||
|
- `JSONRPCReactions.reactions` is now a `Vec<JSONRPCReaction>` with unique reactions and their count, sorted in descending order.
|
||||||
|
- `Event`: `context_id` property is now called `contextId`.
|
||||||
|
- Expand `MessageSearchResult`:
|
||||||
|
- Always include `chat_name`(not an option anymore).
|
||||||
|
- Add `author_id`, `chat_type`, `chat_color`, `is_chat_protected`, `is_chat_contact_request`, `is_chat_archived`.
|
||||||
|
- `author_name` now contains the overridden sender name.
|
||||||
|
- `ChatListItemFetchResult` gets new properties: `summary_preview_image`, `last_message_type` and `last_message_id`
|
||||||
|
- New `MessageReadReceipt` type and `get_message_read_receipts(account_id, message_id)` jsonrpc method.
|
||||||
|
|
||||||
|
### API Changes
|
||||||
|
|
||||||
|
- New rust API `send_webxdc_status_update_struct` to send a `StatusUpdateItem`.
|
||||||
|
- Add `get_msg_read_receipts(context, msg_id)` - get the contacts that send read receipts for a message.
|
||||||
|
|
||||||
|
### Features / Changes
|
||||||
|
|
||||||
|
- Build deltachat-rpc-server releases for x86\_64 macOS.
|
||||||
|
- Generate changelogs using git-cliff ([#4393](https://github.com/deltachat/deltachat-core-rust/pull/4393), [#4396](https://github.com/deltachat/deltachat-core-rust/pull/4396)).
|
||||||
|
- Improve SMTP logging.
|
||||||
|
- Do not cut incoming text if "bot" config is set.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- JSON-RPC: typescript client: fix types of events in event emitter ([#4373](https://github.com/deltachat/deltachat-core-rust/pull/4373)).
|
||||||
|
- Fetch at most 100 existing messages even if EXISTS was not received ([#4383](https://github.com/deltachat/deltachat-core-rust/pull/4383)).
|
||||||
|
- Don't put a double dot at the end of error messages ([#4398](https://github.com/deltachat/deltachat-core-rust/pull/4398)).
|
||||||
|
- Recreate `smtp` table with AUTOINCREMENT `id` ([#4390](https://github.com/deltachat/deltachat-core-rust/pull/4390)).
|
||||||
|
- Do not return an error from `send_msg_to_smtp` if retry limit is exceeded.
|
||||||
|
- Make the bots automatically accept group chat contact requests ([#4377](https://github.com/deltachat/deltachat-core-rust/pull/4377)).
|
||||||
|
- Delete `smtp` rows when message sending is cancelled ([#4391](https://github.com/deltachat/deltachat-core-rust/pull/4391)).
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
|
||||||
|
- Iterate over `msg_ids` without .iter().
|
||||||
|
|
||||||
|
## [1.112.9] - 2023-05-12
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Fetch at most 100 existing messages even if EXISTS was not received.
|
||||||
|
- Delete `smtp` rows when message sending is cancelled.
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
|
||||||
|
- Improve SMTP logging.
|
||||||
|
|
||||||
|
## [1.114.0] - 2023-04-24
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- JSON-RPC: Use long polling instead of server-sent notifications to retrieve events.
|
||||||
|
This better corresponds to JSON-RPC 2.0 server-client distinction
|
||||||
|
and is expected to simplify writing new bindings
|
||||||
|
because dispatching events can be done on higher level.
|
||||||
|
- JSON-RPC: TS: Client now has a mandatory argument whether you want to start listening for events.
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- JSON-RPC: do not print to stdout on failure to find an account.
|
||||||
|
|
||||||
|
|
||||||
|
## [1.113.0] - 2023-04-18
|
||||||
|
|
||||||
|
### Added
|
||||||
|
- New JSON-RPC API `can_send()`.
|
||||||
|
- New `dc_get_next_msgs()` and `dc_wait_next_msgs()` C APIs.
|
||||||
|
New `get_next_msgs()` and `wait_next_msgs()` JSON-RPC API.
|
||||||
|
These APIs can be used by bots to get all unprocessed messages
|
||||||
|
in the order of their arrival and wait for them without relying on events.
|
||||||
|
- New Python bindings API `Account.wait_next_incoming_message()`.
|
||||||
|
- New Python bindings APIs `Message.is_from_self()` and `Message.is_from_device()`.
|
||||||
|
|
||||||
### Changes
|
### Changes
|
||||||
- Increase MSRV to 1.65.0. #4236
|
- Increase MSRV to 1.65.0. #4236
|
||||||
- Remove upper limit on the attachment size. #4253
|
- Remove upper limit on the attachment size. #4253
|
||||||
- Update rPGP to 0.10.1. #4236
|
- Update rPGP to 0.10.1. #4236
|
||||||
- Compress `mime_headers` column with HTML emails stored in database
|
- Compress HTML emails stored in the `mime_headers` column of the database.
|
||||||
- Strip BIDI characters in system messages, files, group names and contact names #3479
|
- Strip BIDI characters in system messages, files, group names and contact names. #3479
|
||||||
- maybe_add_time_based_warnings(): Use release date instead of the provider DB update one
|
- Use release date instead of the provider database update date in `maybe_add_time_based_warnings()`.
|
||||||
- Remove confusing log line "ignoring unsolicited response Recent(…)" #3934
|
- Gracefully terminate `deltachat-rpc-server` on Ctrl+C (`SIGINT`), `SIGTERM` and EOF.
|
||||||
- Cleanly terminate deltachat-rpc-server.
|
|
||||||
Also terminate on ctrl-c.
|
|
||||||
- Refactorings #4317
|
|
||||||
- Add JSON-RPC API `can_send()`.
|
|
||||||
- New `dc_get_next_msgs()` and `dc_wait_next_msgs()` C APIs.
|
|
||||||
New `get_next_msgs()` and `wait_next_msgs()` JSON-RPC API.
|
|
||||||
These APIs can be used by bots to get all unprocessed messages
|
|
||||||
in the order of their arrival and wait for them without relying on events.
|
|
||||||
- Async Python API `get_fresh_messages_in_arrival_order()` is deprecated
|
- Async Python API `get_fresh_messages_in_arrival_order()` is deprecated
|
||||||
in favor of `get_next_msgs()` and `wait_next_msgs()`.
|
in favor of `get_next_msgs()` and `wait_next_msgs()`.
|
||||||
- New Python bindings API `Account.wait_next_incoming_message()`.
|
- Remove metadata from avatars and JPEG images before sending. #4037
|
||||||
- New Python bindings APIs `Message.is_from_self()` and `Message.is_from_device()`.
|
- Recode PNG and other supported image formats to JPEG if they are > 500K in size. #4037
|
||||||
|
|
||||||
### Fixes
|
### Fixes
|
||||||
|
- Don't let blocking be bypassed using groups. #4316
|
||||||
|
- Show a warning if quota list is empty. #4261
|
||||||
|
- Do not reset status on other devices when sending signed reaction messages. #3692
|
||||||
|
- Update `accounts.toml` atomically.
|
||||||
- Fix python bindings README documentation on installing the bindings from source.
|
- Fix python bindings README documentation on installing the bindings from source.
|
||||||
- Show a warning if quota list is empty #4261
|
- Remove confusing log line "ignoring unsolicited response Recent(…)". #3934
|
||||||
- Update "accounts.toml" atomically
|
|
||||||
- Don't let blocking be bypassed using groups #4316
|
## [1.112.8] - 2023-04-20
|
||||||
|
|
||||||
|
### Changes
|
||||||
|
- Add `get_http_response` JSON-RPC API.
|
||||||
|
- Add C API to get HTTP responses.
|
||||||
|
|
||||||
|
## [1.112.7] - 2023-04-17
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
|
||||||
|
- Updated `async-imap` to v0.8.0 to fix erroneous EOF detection in long IMAP responses.
|
||||||
|
|
||||||
## [1.112.6] - 2023-04-04
|
## [1.112.6] - 2023-04-04
|
||||||
|
|
||||||
@@ -2393,7 +2939,6 @@ For a full list of changes, please see our closed Pull Requests:
|
|||||||
|
|
||||||
https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
||||||
|
|
||||||
[unreleased]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.5...HEAD
|
|
||||||
[1.111.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.110.0...v1.111.0
|
[1.111.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.110.0...v1.111.0
|
||||||
[1.112.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.111.0...v1.112.0
|
[1.112.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.111.0...v1.112.0
|
||||||
[1.112.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.0...v1.112.1
|
[1.112.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.0...v1.112.1
|
||||||
@@ -2401,3 +2946,24 @@ https://github.com/deltachat/deltachat-core-rust/pulls?q=is%3Apr+is%3Aclosed
|
|||||||
[1.112.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.2...v1.112.3
|
[1.112.3]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.2...v1.112.3
|
||||||
[1.112.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.3...v1.112.4
|
[1.112.4]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.3...v1.112.4
|
||||||
[1.112.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.4...v1.112.5
|
[1.112.5]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.4...v1.112.5
|
||||||
|
[1.112.6]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.5...v1.112.6
|
||||||
|
[1.112.7]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.6...v1.112.7
|
||||||
|
[1.112.8]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.7...v1.112.8
|
||||||
|
[1.112.9]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.8...v1.112.9
|
||||||
|
[1.112.10]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.112.10
|
||||||
|
[1.113.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.112.9...v1.113.0
|
||||||
|
[1.114.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.113.0...v1.114.0
|
||||||
|
[1.115.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.114.0...v1.115.0
|
||||||
|
[1.116.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.115.0...v1.116.0
|
||||||
|
[1.117.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.116.0...v1.117.0
|
||||||
|
[1.118.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.117.0...v1.118.0
|
||||||
|
[1.119.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.118.0...v1.119.0
|
||||||
|
[1.119.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.0...v1.119.1
|
||||||
|
[1.120.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.119.1...v1.120.0
|
||||||
|
[1.121.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.120.0...v1.121.0
|
||||||
|
[1.122.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.121.0...v1.122.0
|
||||||
|
[1.123.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.122.0...v1.123.0
|
||||||
|
[1.124.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.123.0...v1.124.0
|
||||||
|
[1.124.1]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.0...v1.124.1
|
||||||
|
[1.125.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.124.1...v1.125.0
|
||||||
|
[1.126.0]: https://github.com/deltachat/deltachat-core-rust/compare/v1.125.0...v1.126.0
|
||||||
|
|||||||
115
CONTRIBUTING.md
Normal file
115
CONTRIBUTING.md
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
# Contributing guidelines
|
||||||
|
|
||||||
|
## Reporting bugs
|
||||||
|
|
||||||
|
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
|
||||||
|
[Android](https://github.com/deltachat/deltachat-android/issues),
|
||||||
|
[iOS](https://github.com/deltachat/deltachat-ios/issues) or
|
||||||
|
[Desktop](https://github.com/deltachat/deltachat-desktop/issues),
|
||||||
|
report it to the corresponding repository.
|
||||||
|
|
||||||
|
## Proposing features
|
||||||
|
|
||||||
|
If you have a feature request, create a new topic on the [forum](https://support.delta.chat/).
|
||||||
|
|
||||||
|
## Contributing code
|
||||||
|
|
||||||
|
If you want to contribute a code, [open a Pull Request](https://github.com/deltachat/deltachat-core-rust/pulls).
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
You can find the list of good first issues
|
||||||
|
and a link to this guide
|
||||||
|
on the contributing page: <https://github.com/deltachat/deltachat-core-rust/contribute>
|
||||||
|
|
||||||
|
### 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].
|
||||||
|
|
||||||
|
Commit messages follow the [Conventional Commits] notation.
|
||||||
|
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.
|
||||||
|
|
||||||
|
The following prefix types are used:
|
||||||
|
- `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 cancelled"
|
||||||
|
- `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 vX.Y.Z".
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
#### Breaking Changes
|
||||||
|
|
||||||
|
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.:
|
||||||
|
|
||||||
|
```
|
||||||
|
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
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
[Clippy]: https://doc.rust-lang.org/clippy/
|
||||||
|
[Conventional Commits]: https://www.conventionalcommits.org/
|
||||||
|
[git-cliff]: https://git-cliff.org/
|
||||||
|
|
||||||
|
### 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}"))
|
||||||
|
```
|
||||||
|
|
||||||
|
### 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:#}.");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reviewing
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
For other ways to contribute, refer to the [website](https://delta.chat/en/contribute).
|
||||||
1944
Cargo.lock
generated
1944
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
58
Cargo.toml
58
Cargo.toml
@@ -1,9 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "1.112.6"
|
version = "1.126.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
rust-version = "1.65"
|
rust-version = "1.67"
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
debug = 0
|
debug = 0
|
||||||
@@ -23,6 +23,8 @@ opt-level = "z"
|
|||||||
lto = true
|
lto = true
|
||||||
panic = 'abort'
|
panic = 'abort'
|
||||||
opt-level = "z"
|
opt-level = "z"
|
||||||
|
codegen-units = 1
|
||||||
|
strip = true
|
||||||
|
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
|
quinn-udp = { git = "https://github.com/quinn-rs/quinn", branch="main" }
|
||||||
@@ -35,74 +37,77 @@ ratelimit = { path = "./deltachat-ratelimit" }
|
|||||||
|
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
async-channel = "1.8.0"
|
async-channel = "1.8.0"
|
||||||
async-imap = { version = "0.7.0", default-features = false, features = ["runtime-tokio"] }
|
async-imap = { version = "0.9.1", default-features = false, features = ["runtime-tokio"] }
|
||||||
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.9", default-features = false, features = ["runtime-tokio"] }
|
async-smtp = { version = "0.9", default-features = false, features = ["runtime-tokio"] }
|
||||||
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
|
async_zip = { version = "0.0.12", default-features = false, features = ["deflate", "fs"] }
|
||||||
backtrace = "0.3"
|
backtrace = "0.3"
|
||||||
base64 = "0.21"
|
base64 = "0.21"
|
||||||
brotli = "3.3"
|
brotli = { version = "3.4", default-features=false, features = ["std"] }
|
||||||
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
|
chrono = { version = "0.4", default-features=false, features = ["clock", "std"] }
|
||||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
encoded-words = { git = "https://github.com/async-email/encoded-words", branch = "master" }
|
||||||
escaper = "0.1"
|
escaper = "0.1"
|
||||||
fast-socks5 = "0.8"
|
fast-socks5 = "0.8"
|
||||||
|
fd-lock = "3.0.11"
|
||||||
futures = "0.3"
|
futures = "0.3"
|
||||||
futures-lite = "1.12.0"
|
futures-lite = "1.13.0"
|
||||||
hex = "0.4.0"
|
hex = "0.4.0"
|
||||||
|
hickory-resolver = "0.24"
|
||||||
humansize = "2"
|
humansize = "2"
|
||||||
image = { version = "0.24.6", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
image = { version = "0.24.7", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
||||||
iroh = { version = "0.4.1", default-features = false }
|
iroh = { version = "0.4.1", default-features = false }
|
||||||
kamadak-exif = "0.5"
|
kamadak-exif = "0.5"
|
||||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
mailparse = "0.14"
|
mailparse = "0.14"
|
||||||
num_cpus = "1.15"
|
mime = "0.3.17"
|
||||||
num-derive = "0.3"
|
num_cpus = "1.16"
|
||||||
|
num-derive = "0.4"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
once_cell = "1.17.0"
|
once_cell = "1.18.0"
|
||||||
percent-encoding = "2.2"
|
percent-encoding = "2.3"
|
||||||
parking_lot = "0.12"
|
parking_lot = "0.12"
|
||||||
pgp = { version = "0.10", default-features = false }
|
pgp = { version = "0.10", default-features = false }
|
||||||
pretty_env_logger = { version = "0.4", optional = true }
|
pretty_env_logger = { version = "0.5", optional = true }
|
||||||
qrcodegen = "1.7.0"
|
qrcodegen = "1.7.0"
|
||||||
quick-xml = "0.28"
|
quick-xml = "0.30"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
regex = "1.7"
|
regex = "1.9"
|
||||||
reqwest = { version = "0.11.16", features = ["json"] }
|
reqwest = { version = "0.11.20", features = ["json"] }
|
||||||
rusqlite = { version = "0.29", features = ["sqlcipher"] }
|
rusqlite = { version = "0.29", features = ["sqlcipher"] }
|
||||||
rust-hsluv = "0.1"
|
rust-hsluv = "0.1"
|
||||||
sanitize-filename = "0.4"
|
sanitize-filename = "0.5"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
sha-1 = "0.10"
|
sha-1 = "0.10"
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
smallvec = "1"
|
smallvec = "1"
|
||||||
strum = "0.24"
|
strum = "0.25"
|
||||||
strum_macros = "0.24"
|
strum_macros = "0.25"
|
||||||
tagger = "4.3.4"
|
tagger = "4.3.4"
|
||||||
textwrap = "0.16.0"
|
textwrap = "0.16.0"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||||
tokio-io-timeout = "1.2.0"
|
tokio-io-timeout = "1.2.0"
|
||||||
tokio-stream = { version = "0.1.11", features = ["fs"] }
|
tokio-stream = { version = "0.1.14", features = ["fs"] }
|
||||||
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
tokio-tar = { version = "0.3" } # TODO: integrate tokio into async-tar
|
||||||
tokio-util = "0.7.7"
|
tokio-util = "0.7.9"
|
||||||
toml = "0.7"
|
toml = "0.7"
|
||||||
trust-dns-resolver = "0.22"
|
|
||||||
url = "2"
|
url = "2"
|
||||||
uuid = { version = "1", features = ["serde", "v4"] }
|
uuid = { version = "1", features = ["serde", "v4"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
ansi_term = "0.12.0"
|
ansi_term = "0.12.0"
|
||||||
criterion = { version = "0.4.0", features = ["async_tokio"] }
|
criterion = { version = "0.5.1", features = ["async_tokio"] }
|
||||||
futures-lite = "1.12"
|
futures-lite = "1.13"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.5"
|
||||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
proptest = { version = "1", default-features = false, features = ["std"] }
|
||||||
tempfile = "3"
|
tempfile = "3"
|
||||||
testdir = "0.7.3"
|
testdir = "0.8.0"
|
||||||
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
tokio = { version = "1", features = ["parking_lot", "rt-multi-thread", "macros"] }
|
||||||
|
pretty_assertions = "1.3.0"
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
@@ -115,11 +120,6 @@ members = [
|
|||||||
"format-flowed",
|
"format-flowed",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "simple"
|
|
||||||
path = "examples/simple.rs"
|
|
||||||
|
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "create_account"
|
name = "create_account"
|
||||||
harness = false
|
harness = false
|
||||||
|
|||||||
2
LICENSE
2
LICENSE
@@ -361,7 +361,7 @@ Exhibit A - Source Code Form License Notice
|
|||||||
|
|
||||||
This Source Code Form is subject to the terms of the Mozilla Public
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
If it is not possible or desirable to put the notice in a particular
|
If it is not possible or desirable to put the notice in a particular
|
||||||
file, then You may include the notice in a location (such as a LICENSE
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
|||||||
18
README.md
18
README.md
@@ -1,8 +1,16 @@
|
|||||||
# Delta Chat Rust
|
<p align="center">
|
||||||
|
<img alt="Delta Chat Logo" height="200px" src="https://raw.githubusercontent.com/deltachat/deltachat-pages/master/assets/blog/rust-delta.png">
|
||||||
|
</p>
|
||||||
|
|
||||||
> Deltachat-core written in Rust
|
<p align="center">
|
||||||
|
<a href="https://github.com/yoav-lavi/melody/actions/workflows/rust.yml">
|
||||||
|
<img alt="Rust CI" src="https://github.com/yoav-lavi/melody/actions/workflows/rust.yml/badge.svg">
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
[](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml)
|
<p align="center">
|
||||||
|
The core library for Delta Chat, written in Rust
|
||||||
|
</p>
|
||||||
|
|
||||||
## Installing Rust and Cargo
|
## Installing Rust and Cargo
|
||||||
|
|
||||||
@@ -167,8 +175,8 @@ Language bindings are available for:
|
|||||||
|
|
||||||
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
||||||
- **Node.js**
|
- **Node.js**
|
||||||
- over cffi (legacy): \[[📂 source](./node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.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: \[[📂 source](https://github.com/deltachat/napi-jsonrpc) | [📦 npm](https://www.npmjs.com/package/@deltachat/napi-jsonrpc)\]
|
- 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**
|
- **Go**
|
||||||
- over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
|
- over jsonrpc: \[[📂 source](https://github.com/deltachat/deltachat-rpc-client-go/)\]
|
||||||
|
|||||||
21
RELEASE.md
Normal file
21
RELEASE.md
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
# Releasing a new version of DeltaChat core
|
||||||
|
|
||||||
|
For example, to release version 1.116.0 of the core, do the following steps.
|
||||||
|
|
||||||
|
1. Resolve all [blocker issues](https://github.com/deltachat/deltachat-core-rust/labels/blocker).
|
||||||
|
|
||||||
|
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. Update the changelog: `git cliff --unreleased --tag 1.116.0 --prepend CHANGELOG.md` or `git cliff -u -t 1.116.0 -p CHANGELOG.md`.
|
||||||
|
|
||||||
|
4. Update the version by running `scripts/set_core_version.py 1.116.0`.
|
||||||
|
|
||||||
|
5. 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.
|
||||||
|
|
||||||
|
6. Tag the release: `git tag -a v1.116.0`.
|
||||||
|
|
||||||
|
7. Push the release tag: `git push origin v1.116.0`.
|
||||||
|
|
||||||
|
8. Create a GitHub release: `gh release create v1.116.0 -n ''`.
|
||||||
@@ -8,7 +8,8 @@ async fn create_accounts(n: u32) {
|
|||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let p: PathBuf = dir.path().join("accounts");
|
let p: PathBuf = dir.path().join("accounts");
|
||||||
|
|
||||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
let writable = true;
|
||||||
|
let mut accounts = Accounts::new(p.clone(), writable).await.unwrap();
|
||||||
|
|
||||||
for expected_id in 2..n {
|
for expected_id in 2..n {
|
||||||
let id = accounts.add_account().await.unwrap();
|
let id = accounts.add_account().await.unwrap();
|
||||||
|
|||||||
79
cliff.toml
Normal file
79
cliff.toml
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# configuration file for git-cliff
|
||||||
|
# see https://git-cliff.org/docs/configuration/
|
||||||
|
|
||||||
|
|
||||||
|
[git]
|
||||||
|
# parse the commits based on https://www.conventionalcommits.org
|
||||||
|
conventional_commits = true
|
||||||
|
# filter out the commits that are not conventional
|
||||||
|
filter_unconventional = false
|
||||||
|
# process each line of a commit as an individual commit
|
||||||
|
split_commits = false
|
||||||
|
# regex for preprocessing the commit messages
|
||||||
|
commit_preprocessors = [
|
||||||
|
{ 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
|
||||||
|
commit_parsers = [
|
||||||
|
{ message = "^feat", group = "Features / Changes"},
|
||||||
|
{ message = "^fix", group = "Fixes"},
|
||||||
|
{ message = "^api", group = "API-Changes" },
|
||||||
|
{ message = "^refactor", group = "Refactor"},
|
||||||
|
{ message = "^perf", group = "Performance"},
|
||||||
|
{ message = "^test", group = "Tests"},
|
||||||
|
{ message = "^style", group = "Styling"},
|
||||||
|
{ message = "^chore\\(release\\): prepare for", skip = true},
|
||||||
|
{ message = "^chore", group = "Miscellaneous Tasks"},
|
||||||
|
{ message = "^build", group = "Build system"},
|
||||||
|
{ message = "^docs", group = "Documentation"},
|
||||||
|
{ message = "^ci", group = "CI"},
|
||||||
|
{ message = ".*", group = "Other"},
|
||||||
|
# { body = ".*security", group = "Security"},
|
||||||
|
]
|
||||||
|
# protect breaking changes from being skipped due to matching a skipping commit_parser
|
||||||
|
protect_breaking_commits = true
|
||||||
|
# filter out the commits that are not matched by commit parsers
|
||||||
|
filter_commits = true
|
||||||
|
# glob pattern for matching git tags
|
||||||
|
tag_pattern = "v[0-9]*"
|
||||||
|
# regex for skipping tags
|
||||||
|
#skip_tags = "v0.1.0-beta.1"
|
||||||
|
# regex for ignoring tags
|
||||||
|
ignore_tags = ""
|
||||||
|
# sort the tags topologically
|
||||||
|
topo_order = false
|
||||||
|
# sort the commits inside sections by oldest/newest order
|
||||||
|
sort_commits = "oldest"
|
||||||
|
# limit the number of commits included in the changelog.
|
||||||
|
# limit_commits = 42
|
||||||
|
|
||||||
|
|
||||||
|
[changelog]
|
||||||
|
# changelog header
|
||||||
|
header = """
|
||||||
|
# Changelog\n
|
||||||
|
"""
|
||||||
|
# template for the changelog body
|
||||||
|
# https://keats.github.io/tera/docs/#templates
|
||||||
|
body = """
|
||||||
|
{% if version %}\
|
||||||
|
## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }}
|
||||||
|
{% else %}\
|
||||||
|
## [unreleased]
|
||||||
|
{% endif %}\
|
||||||
|
{% for group, commits in commits | group_by(attribute="group") %}
|
||||||
|
### {{ group | upper_first }}
|
||||||
|
{% for commit in commits %}
|
||||||
|
- {% if commit.breaking %}[**breaking**] {% endif %}\
|
||||||
|
{% if commit.scope %}{{ commit.scope }}: {% endif %}\
|
||||||
|
{{ commit.message | upper_first }}.\
|
||||||
|
{% if commit.footers is defined %}\
|
||||||
|
{% for footer in commit.footers %}{% if 'BREAKING CHANGE' in footer.token %}
|
||||||
|
{% raw %} {% endraw %}- {{ footer.value }}\
|
||||||
|
{% endif %}{% endfor %}\
|
||||||
|
{% endif%}\
|
||||||
|
{% endfor %}
|
||||||
|
{% endfor %}\n
|
||||||
|
"""
|
||||||
|
# remove the leading and trailing whitespace from the template
|
||||||
|
trim = true
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat_ffi"
|
name = "deltachat_ffi"
|
||||||
version = "1.112.6"
|
version = "1.126.1"
|
||||||
description = "Deltachat FFI"
|
description = "Deltachat FFI"
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -24,7 +24,8 @@ tokio = { version = "1", features = ["rt-multi-thread"] }
|
|||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
thiserror = "1"
|
thiserror = "1"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
once_cell = "1.17.0"
|
once_cell = "1.18.0"
|
||||||
|
yerpc = { version = "0.5.1", features = ["anyhow_expose"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
|
|||||||
@@ -846,7 +846,7 @@ EXCLUDE_PATTERNS =
|
|||||||
# exclude all test directories use the pattern */test/*
|
# exclude all test directories use the pattern */test/*
|
||||||
|
|
||||||
######################################################
|
######################################################
|
||||||
EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_keyring_t dc_loginparam_t dc_mime*_t
|
EXCLUDE_SYMBOLS = dc_aheader_t dc_apeerstate_t dc_e2ee_helper_t dc_imap_t dc_job*_t dc_key_t dc_loginparam_t dc_mime*_t
|
||||||
EXCLUDE_SYMBOLS += dc_saxparser_t dc_simplify_t dc_smtp_t dc_sqlite3_t dc_strbuilder_t dc_param_t dc_hash_t dc_hashelem_t
|
EXCLUDE_SYMBOLS += dc_saxparser_t dc_simplify_t dc_smtp_t dc_sqlite3_t dc_strbuilder_t dc_param_t dc_hash_t dc_hashelem_t
|
||||||
EXCLUDE_SYMBOLS += _dc_* jsmn*
|
EXCLUDE_SYMBOLS += _dc_* jsmn*
|
||||||
######################################################
|
######################################################
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ typedef struct _dc_event dc_event_t;
|
|||||||
typedef struct _dc_event_emitter dc_event_emitter_t;
|
typedef struct _dc_event_emitter dc_event_emitter_t;
|
||||||
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
|
typedef struct _dc_jsonrpc_instance dc_jsonrpc_instance_t;
|
||||||
typedef struct _dc_backup_provider dc_backup_provider_t;
|
typedef struct _dc_backup_provider dc_backup_provider_t;
|
||||||
|
typedef struct _dc_http_response dc_http_response_t;
|
||||||
|
|
||||||
// Alias for backwards compatibility, use dc_event_emitter_t instead.
|
// Alias for backwards compatibility, use dc_event_emitter_t instead.
|
||||||
typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
|
typedef struct _dc_event_emitter dc_accounts_event_emitter_t;
|
||||||
@@ -300,6 +301,19 @@ dc_context_t* dc_context_new_closed (const char* dbfile);
|
|||||||
int dc_context_open (dc_context_t *context, const char* passphrase);
|
int dc_context_open (dc_context_t *context, const char* passphrase);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the passphrase on the open database.
|
||||||
|
* Existing database must already be encrypted and the passphrase cannot be NULL or empty.
|
||||||
|
* It is impossible to encrypt unencrypted database with this method and vice versa.
|
||||||
|
*
|
||||||
|
* @memberof dc_context_t
|
||||||
|
* @param context The context object.
|
||||||
|
* @param passphrase The new passphrase.
|
||||||
|
* @return 1 on success, 0 on error.
|
||||||
|
*/
|
||||||
|
int dc_context_change_passphrase (dc_context_t* context, const char* passphrase);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns 1 if database is open.
|
* Returns 1 if database is open.
|
||||||
*
|
*
|
||||||
@@ -370,7 +384,12 @@ char* dc_get_blobdir (const dc_context_t* context);
|
|||||||
/**
|
/**
|
||||||
* Configure the context. The configuration is handled by key=value pairs as:
|
* Configure the context. The configuration is handled by key=value pairs as:
|
||||||
*
|
*
|
||||||
* - `addr` = address to display (always needed)
|
* - `addr` = Email address to use for configuration.
|
||||||
|
* If dc_configure() fails this is not the email address actually in use.
|
||||||
|
* Use `configured_addr` to find out the email address actually in use.
|
||||||
|
* - `configured_addr` = Email address actually in use.
|
||||||
|
* Unless for testing, do not set this value using dc_set_config().
|
||||||
|
* Instead, set `addr` and call dc_configure().
|
||||||
* - `mail_server` = IMAP-server, guessed if left out
|
* - `mail_server` = IMAP-server, guessed if left out
|
||||||
* - `mail_user` = IMAP-username, guessed if left out
|
* - `mail_user` = IMAP-username, guessed if left out
|
||||||
* - `mail_pw` = IMAP-password (always needed)
|
* - `mail_pw` = IMAP-password (always needed)
|
||||||
@@ -419,17 +438,19 @@ char* dc_get_blobdir (const dc_context_t* context);
|
|||||||
* 0=watch all folders normally (default)
|
* 0=watch all folders normally (default)
|
||||||
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
* changes require restarting IO by calling dc_stop_io() and then dc_start_io().
|
||||||
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
|
* - `show_emails` = DC_SHOW_EMAILS_OFF (0)=
|
||||||
* show direct replies to chats only (default),
|
* show direct replies to chats only,
|
||||||
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
|
* DC_SHOW_EMAILS_ACCEPTED_CONTACTS (1)=
|
||||||
* also show all mails of confirmed contacts,
|
* also show all mails of confirmed contacts,
|
||||||
* DC_SHOW_EMAILS_ALL (2)=
|
* DC_SHOW_EMAILS_ALL (2)=
|
||||||
* also show mails of unconfirmed contacts.
|
* also show mails of unconfirmed contacts (default).
|
||||||
* - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)=
|
* - `key_gen_type` = DC_KEY_GEN_DEFAULT (0)=
|
||||||
* generate recommended key type (default),
|
* generate recommended key type (default),
|
||||||
* DC_KEY_GEN_RSA2048 (1)=
|
* DC_KEY_GEN_RSA2048 (1)=
|
||||||
* generate RSA 2048 keypair
|
* generate RSA 2048 keypair
|
||||||
* DC_KEY_GEN_ED25519 (2)=
|
* DC_KEY_GEN_ED25519 (2)=
|
||||||
* generate Ed25519 keypair
|
* generate Curve25519 keypair
|
||||||
|
* DC_KEY_GEN_RSA4096 (3)=
|
||||||
|
* generate RSA 4096 keypair
|
||||||
* - `save_mime_headers` = 1=save mime headers
|
* - `save_mime_headers` = 1=save mime headers
|
||||||
* and make dc_get_mime_headers() work for subsequent calls,
|
* and make dc_get_mime_headers() work for subsequent calls,
|
||||||
* 0=do not save mime headers (default)
|
* 0=do not save mime headers (default)
|
||||||
@@ -460,8 +481,9 @@ char* dc_get_blobdir (const dc_context_t* context);
|
|||||||
* If no type is prefixed, the videochat is handled completely in a browser.
|
* If no type is prefixed, the videochat is handled completely in a browser.
|
||||||
* - `bot` = Set to "1" if this is a bot.
|
* - `bot` = Set to "1" if this is a bot.
|
||||||
* Prevents adding the "Device messages" and "Saved messages" chats,
|
* Prevents adding the "Device messages" and "Saved messages" chats,
|
||||||
* adds Auto-Submitted header to outgoing messages
|
* adds Auto-Submitted header to outgoing messages,
|
||||||
* and accepts contact requests automatically (calling dc_accept_chat() is not needed for bots).
|
* accepts contact requests automatically (calling dc_accept_chat() is not needed for bots)
|
||||||
|
* and does not cut large incoming text messages.
|
||||||
* - `last_msg_id` = database ID of the last message processed by the bot.
|
* - `last_msg_id` = database ID of the last message processed by the bot.
|
||||||
* This ID and IDs below it are guaranteed not to be returned
|
* This ID and IDs below it are guaranteed not to be returned
|
||||||
* by dc_get_next_msgs() and dc_wait_next_msgs().
|
* by dc_get_next_msgs() and dc_wait_next_msgs().
|
||||||
@@ -475,6 +497,9 @@ char* dc_get_blobdir (const dc_context_t* context);
|
|||||||
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
|
* - `fetch_existing_msgs` = 1=fetch most recent existing messages on configure (default),
|
||||||
* 0=do not fetch existing messages on configure.
|
* 0=do not fetch existing messages on configure.
|
||||||
* In both cases, existing recipients are added to the contact database.
|
* In both cases, existing recipients are added to the contact database.
|
||||||
|
* - `disable_idle` = 1=disable IMAP IDLE even if the server supports it,
|
||||||
|
* 0=use IMAP IDLE if the server supports it.
|
||||||
|
* This is a developer option used for testing polling used as an IDLE fallback.
|
||||||
* - `download_limit` = Messages up to this number of bytes are downloaded automatically.
|
* - `download_limit` = Messages up to this number of bytes are downloaded automatically.
|
||||||
* For larger messages, only the header is downloaded and a placeholder is shown.
|
* For larger messages, only the header is downloaded and a placeholder is shown.
|
||||||
* These messages can be downloaded fully using dc_download_full_msg() later.
|
* These messages can be downloaded fully using dc_download_full_msg() later.
|
||||||
@@ -483,6 +508,16 @@ char* dc_get_blobdir (const dc_context_t* context);
|
|||||||
* to not mess up with non-delivery-reports or read-receipts.
|
* to not mess up with non-delivery-reports or read-receipts.
|
||||||
* 0=no limit (default).
|
* 0=no limit (default).
|
||||||
* Changes affect future messages only.
|
* Changes affect future messages only.
|
||||||
|
* - `gossip_period` = How often to gossip Autocrypt keys in chats with multiple recipients, in
|
||||||
|
* seconds. 2 days by default.
|
||||||
|
* This is not supposed to be changed by UIs and only used for testing.
|
||||||
|
* - `verified_one_on_one_chats` = Feature flag for verified 1:1 chats; the UI should set it
|
||||||
|
* to 1 if it supports verified 1:1 chats.
|
||||||
|
* Regardless of this setting, `dc_chat_is_protected()` returns true while the key is verified,
|
||||||
|
* and when the key changes, an info message is posted into the chat.
|
||||||
|
* 0=Nothing else happens when the key changes.
|
||||||
|
* 1=After the key changed, `dc_chat_can_send()` returns false and `dc_chat_is_protection_broken()` returns true
|
||||||
|
* until `dc_accept_chat()` is called.
|
||||||
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
|
* - `ui.*` = All keys prefixed by `ui.` can be used by the user-interfaces for system-specific purposes.
|
||||||
* The prefix should be followed by the system and maybe subsystem,
|
* The prefix should be followed by the system and maybe subsystem,
|
||||||
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
|
* e.g. `ui.desktop.foo`, `ui.desktop.linux.bar`, `ui.android.foo`, `ui.dc40.bar`, `ui.bot.simplebot.baz`.
|
||||||
@@ -807,7 +842,7 @@ void dc_maybe_network (dc_context_t* context);
|
|||||||
* @param context The context as created by dc_context_new().
|
* @param context The context as created by dc_context_new().
|
||||||
* @param addr The e-mail address of the user. This must match the
|
* @param addr The e-mail address of the user. This must match the
|
||||||
* configured_addr setting of the context as well as the UID of the key.
|
* configured_addr setting of the context as well as the UID of the key.
|
||||||
* @param public_data ASCII armored public key.
|
* @param public_data Ignored, actual public key is extracted from secret_data.
|
||||||
* @param secret_data ASCII armored secret key.
|
* @param secret_data ASCII armored secret key.
|
||||||
* @return 1 on success, 0 on failure.
|
* @return 1 on success, 0 on failure.
|
||||||
*/
|
*/
|
||||||
@@ -861,7 +896,8 @@ int dc_preconfigure_keypair (dc_context_t* context, const cha
|
|||||||
* - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
|
* - if the flag DC_GCL_ADD_ALLDONE_HINT is set, DC_CHAT_ID_ALLDONE_HINT
|
||||||
* is added as needed.
|
* is added as needed.
|
||||||
* @param query_str An optional query for filtering the list. Only chats matching this query
|
* @param query_str An optional query for filtering the list. Only chats matching this query
|
||||||
* are returned. Give NULL for no filtering.
|
* are returned. Give NULL for no filtering. When `is:unread` is contained in the query,
|
||||||
|
* the chatlist is filtered such that only chats with unread messages show up.
|
||||||
* @param query_id An optional contact ID for filtering the list. Only chats including this contact ID
|
* @param query_id An optional contact ID for filtering the list. Only chats including this contact ID
|
||||||
* are returned. Give 0 for no filtering.
|
* are returned. Give 0 for no filtering.
|
||||||
* @return A chatlist as an dc_chatlist_t object.
|
* @return A chatlist as an dc_chatlist_t object.
|
||||||
@@ -1101,7 +1137,7 @@ dc_reactions_t* dc_get_msg_reactions (dc_context_t *context, int msg_id);
|
|||||||
*
|
*
|
||||||
* In JS land, that would be mapped to something as:
|
* In JS land, that would be mapped to something as:
|
||||||
* ```
|
* ```
|
||||||
* success = window.webxdc.sendUpdate('{"action":"move","src":"A3","dest":"B4"}', 'move A3 B4');
|
* success = window.webxdc.sendUpdate('{payload: {"action":"move","src":"A3","dest":"B4"}}', 'move A3 B4');
|
||||||
* ```
|
* ```
|
||||||
* `context` and `msg_id` are not needed in JS as those are unique within a webxdc instance.
|
* `context` and `msg_id` are not needed in JS as those are unique within a webxdc instance.
|
||||||
* See dc_get_webxdc_status_updates() for the receiving counterpart.
|
* See dc_get_webxdc_status_updates() for the receiving counterpart.
|
||||||
@@ -1319,6 +1355,20 @@ int dc_get_msg_cnt (dc_context_t* context, uint32_t ch
|
|||||||
int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t chat_id);
|
int dc_get_fresh_msg_cnt (dc_context_t* context, uint32_t chat_id);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of similar chats.
|
||||||
|
*
|
||||||
|
* @warning This is an experimental API which may change or be removed in the future.
|
||||||
|
*
|
||||||
|
* @memberof dc_context_t
|
||||||
|
* @param context The context object as returned from dc_context_new().
|
||||||
|
* @param chat_id The ID of the chat for which to find similar chats.
|
||||||
|
* @return The list of similar chats.
|
||||||
|
* On errors, NULL is returned.
|
||||||
|
* Must be freed using dc_chatlist_unref() when no longer used.
|
||||||
|
*/
|
||||||
|
dc_chatlist_t* dc_get_similar_chatlist (dc_context_t* context, uint32_t chat_id);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Estimate the number of messages that will be deleted
|
* Estimate the number of messages that will be deleted
|
||||||
@@ -1450,6 +1500,7 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
|
|||||||
* Typically used to implement the "next" and "previous" buttons
|
* Typically used to implement the "next" and "previous" buttons
|
||||||
* in a gallery or in a media player.
|
* in a gallery or in a media player.
|
||||||
*
|
*
|
||||||
|
* @deprecated Deprecated 2023-10-03, use dc_get_chat_media() and navigate the returned array instead.
|
||||||
* @memberof dc_context_t
|
* @memberof dc_context_t
|
||||||
* @param context The context object as returned from dc_context_new().
|
* @param context The context object as returned from dc_context_new().
|
||||||
* @param msg_id The ID of the current message from which the next or previous message should be searched.
|
* @param msg_id The ID of the current message from which the next or previous message should be searched.
|
||||||
@@ -1468,24 +1519,6 @@ dc_array_t* dc_get_chat_media (dc_context_t* context, uint32_t ch
|
|||||||
uint32_t dc_get_next_media (dc_context_t* context, uint32_t msg_id, int dir, int msg_type, int msg_type2, int msg_type3);
|
uint32_t dc_get_next_media (dc_context_t* context, uint32_t msg_id, int dir, int msg_type, int msg_type2, int msg_type3);
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Enable or disable protection against active attacks.
|
|
||||||
* To enable protection, it is needed that all members are verified;
|
|
||||||
* if this condition is met, end-to-end-encryption is always enabled
|
|
||||||
* and only the verified keys are used.
|
|
||||||
*
|
|
||||||
* Sends out #DC_EVENT_CHAT_MODIFIED on changes
|
|
||||||
* and #DC_EVENT_MSGS_CHANGED if a status message was sent.
|
|
||||||
*
|
|
||||||
* @memberof dc_context_t
|
|
||||||
* @param context The context object as returned from dc_context_new().
|
|
||||||
* @param chat_id The ID of the chat to change the protection for.
|
|
||||||
* @param protect 1=protect chat, 0=unprotect chat
|
|
||||||
* @return 1=success, 0=error, e.g. some members may be unverified
|
|
||||||
*/
|
|
||||||
int dc_set_chat_protection (dc_context_t* context, uint32_t chat_id, int protect);
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Set chat visibility to pinned, archived or normal.
|
* Set chat visibility to pinned, archived or normal.
|
||||||
*
|
*
|
||||||
@@ -1679,24 +1712,12 @@ uint32_t dc_create_group_chat (dc_context_t* context, int protect
|
|||||||
* Create a new broadcast list.
|
* Create a new broadcast list.
|
||||||
*
|
*
|
||||||
* Broadcast lists are similar to groups on the sending device,
|
* Broadcast lists are similar to groups on the sending device,
|
||||||
* however, recipients get the messages in normal one-to-one chats
|
* however, recipients get the messages in a read-only chat
|
||||||
* and will not be aware of other members.
|
* and will see who the other members are.
|
||||||
*
|
*
|
||||||
* Replies to broadcasts go only to the sender
|
* For historical reasons, this function does not take a name directly,
|
||||||
* and not to all broadcast recipients.
|
* instead you have to set the name using dc_set_chat_name()
|
||||||
* Moreover, replies will not appear in the broadcast list
|
* after creating the broadcast list.
|
||||||
* but in the one-to-one chat with the person answering.
|
|
||||||
*
|
|
||||||
* The name and the image of the broadcast list is set automatically
|
|
||||||
* and is visible to the sender only.
|
|
||||||
* Not asking for these data allows more focused creation
|
|
||||||
* and we bypass the question who will get which data.
|
|
||||||
* Also, many users will have at most one broadcast list
|
|
||||||
* so, a generic name and image is sufficient at the first place.
|
|
||||||
*
|
|
||||||
* Later on, however, the name can be changed using dc_set_chat_name().
|
|
||||||
* The image cannot be changed to have a unique, recognizable icon in the chat lists.
|
|
||||||
* All in all, this is also what other messengers are doing here.
|
|
||||||
*
|
*
|
||||||
* @memberof dc_context_t
|
* @memberof dc_context_t
|
||||||
* @param context The context object.
|
* @param context The context object.
|
||||||
@@ -2239,8 +2260,7 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
|
|||||||
* the backup is not encrypted.
|
* the backup is not encrypted.
|
||||||
* The backup contains all contacts, chats, images and other data and device independent settings.
|
* The backup contains all contacts, chats, images and other data and device independent settings.
|
||||||
* The backup does not contain device dependent settings as ringtones or LED notification settings.
|
* The backup does not contain device dependent settings as ringtones or LED notification settings.
|
||||||
* The name of the backup is typically `delta-chat-<day>.tar`, if more than one backup is create on a day,
|
* The name of the backup is `delta-chat-backup-<day>-<number>-<addr>.tar`.
|
||||||
* the format is `delta-chat-<day>-<number>.tar`
|
|
||||||
*
|
*
|
||||||
* - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. `param2` is the passphrase.
|
* - **DC_IMEX_IMPORT_BACKUP** (12) - `param1` is the file (not: directory) to import. `param2` is the passphrase.
|
||||||
* The file is normally created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
|
* The file is normally created by DC_IMEX_EXPORT_BACKUP and detected by dc_imex_has_backup(). Importing a backup
|
||||||
@@ -2253,6 +2273,7 @@ dc_contact_t* dc_get_contact (dc_context_t* context, uint32_t co
|
|||||||
*
|
*
|
||||||
* - **DC_IMEX_IMPORT_SELF_KEYS** (2) - Import private keys found in the directory given as `param1`.
|
* - **DC_IMEX_IMPORT_SELF_KEYS** (2) - Import private keys found in the directory given as `param1`.
|
||||||
* The last imported key is made the default keys unless its name contains the string `legacy`. Public keys are not imported.
|
* The last imported key is made the default keys unless its name contains the string `legacy`. Public keys are not imported.
|
||||||
|
* If `param1` is a filename, import the private key from the file and make it the default.
|
||||||
*
|
*
|
||||||
* While dc_imex() returns immediately, the started job may take a while,
|
* While dc_imex() returns immediately, the started job may take a while,
|
||||||
* you can stop it using dc_stop_ongoing_process(). During execution of the job,
|
* you can stop it using dc_stop_ongoing_process(). During execution of the job,
|
||||||
@@ -2924,12 +2945,15 @@ int dc_receive_backup (dc_context_t* context, const char* qr);
|
|||||||
* @param dir The directory to create the context-databases in.
|
* @param dir The directory to create the context-databases in.
|
||||||
* If the directory does not exist,
|
* If the directory does not exist,
|
||||||
* dc_accounts_new() will try to create it.
|
* dc_accounts_new() will try to create it.
|
||||||
|
* @param writable Whether the returned account manager is writable, i.e. calling these functions on
|
||||||
|
* it is possible: dc_accounts_add_account(), dc_accounts_add_closed_account(),
|
||||||
|
* dc_accounts_migrate_account(), dc_accounts_remove_account(), dc_accounts_select_account().
|
||||||
* @return An account manager object.
|
* @return An account manager object.
|
||||||
* The object must be passed to the other account manager functions
|
* The object must be passed to the other account manager functions
|
||||||
* and must be freed using dc_accounts_unref() after usage.
|
* and must be freed using dc_accounts_unref() after usage.
|
||||||
* On errors, NULL is returned.
|
* On errors, NULL is returned.
|
||||||
*/
|
*/
|
||||||
dc_accounts_t* dc_accounts_new (const char* os_name, const char* dir);
|
dc_accounts_t* dc_accounts_new (const char* dir, int writable);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -3710,7 +3734,6 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
|||||||
* Check if a chat is protected.
|
* Check if a chat is protected.
|
||||||
* Protected chats contain only verified members and encryption is always enabled.
|
* Protected chats contain only verified members and encryption is always enabled.
|
||||||
* Protected chats are created using dc_create_group_chat() by setting the 'protect' parameter to 1.
|
* Protected chats are created using dc_create_group_chat() by setting the 'protect' parameter to 1.
|
||||||
* The status can be changed using dc_set_chat_protection().
|
|
||||||
*
|
*
|
||||||
* @memberof dc_chat_t
|
* @memberof dc_chat_t
|
||||||
* @param chat The chat object.
|
* @param chat The chat object.
|
||||||
@@ -3719,6 +3742,26 @@ int dc_chat_can_send (const dc_chat_t* chat);
|
|||||||
int dc_chat_is_protected (const dc_chat_t* chat);
|
int dc_chat_is_protected (const dc_chat_t* chat);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the chat was protected, and then an incoming message broke this protection.
|
||||||
|
*
|
||||||
|
* This function is only useful if the UI enabled the `verified_one_on_one_chats` feature flag,
|
||||||
|
* otherwise it will return false for all chats.
|
||||||
|
*
|
||||||
|
* 1:1 chats are automatically set as protected when a contact is verified.
|
||||||
|
* When a message comes in that is not encrypted / signed correctly,
|
||||||
|
* the chat is automatically set as unprotected again.
|
||||||
|
* dc_chat_is_protection_broken() will return true until dc_accept_chat() is called.
|
||||||
|
*
|
||||||
|
* The UI should let the user confirm that this is OK with a message like
|
||||||
|
* `Bob sent a message from another device. Tap to learn more` and then call dc_accept_chat().
|
||||||
|
* @memberof dc_chat_t
|
||||||
|
* @param chat The chat object.
|
||||||
|
* @return 1=chat protection broken, 0=otherwise.
|
||||||
|
*/
|
||||||
|
int dc_chat_is_protection_broken (const dc_chat_t* chat);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if locations are sent to the chat
|
* Check if locations are sent to the chat
|
||||||
* at the time the object was created using dc_get_chat().
|
* at the time the object was created using dc_get_chat().
|
||||||
@@ -3923,7 +3966,7 @@ int64_t dc_msg_get_received_timestamp (const dc_msg_t* msg);
|
|||||||
* Get the message time used for sorting.
|
* Get the message time used for sorting.
|
||||||
* This function returns the timestamp that is used for sorting the message
|
* This function returns the timestamp that is used for sorting the message
|
||||||
* into lists as returned e.g. by dc_get_chat_msgs().
|
* into lists as returned e.g. by dc_get_chat_msgs().
|
||||||
* This may be the reveived time, the sending time or another time.
|
* This may be the received time, the sending time or another time.
|
||||||
*
|
*
|
||||||
* To get the receiving time, use dc_msg_get_received_timestamp().
|
* To get the receiving time, use dc_msg_get_received_timestamp().
|
||||||
* To get the sending time, use dc_msg_get_timestamp().
|
* To get the sending time, use dc_msg_get_timestamp().
|
||||||
@@ -3976,16 +4019,17 @@ char* dc_msg_get_text (const dc_msg_t* msg);
|
|||||||
*/
|
*/
|
||||||
char* dc_msg_get_subject (const dc_msg_t* msg);
|
char* dc_msg_get_subject (const dc_msg_t* msg);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Find out full path, file name and extension of the file associated with a
|
* Find out full path of the file associated with a message.
|
||||||
* message.
|
|
||||||
*
|
*
|
||||||
* Typically files are associated with images, videos, audios, documents.
|
* Typically files are associated with images, videos, audios, documents.
|
||||||
* Plain text messages do not have a file.
|
* Plain text messages do not have a file.
|
||||||
|
* File name may be mangled. To obtain the original attachment filename use dc_msg_get_filename().
|
||||||
*
|
*
|
||||||
* @memberof dc_msg_t
|
* @memberof dc_msg_t
|
||||||
* @param msg The message object.
|
* @param msg The message object.
|
||||||
* @return The full path, the file name, and the extension of the file associated with the message.
|
* @return The full path (with file name and extension) of the file associated with the message.
|
||||||
* If there is no file associated with the message, an empty string is returned.
|
* If there is no file associated with the message, an empty string is returned.
|
||||||
* NULL is never returned and the returned value must be released using dc_str_unref().
|
* NULL is never returned and the returned value must be released using dc_str_unref().
|
||||||
*/
|
*/
|
||||||
@@ -3993,14 +4037,13 @@ char* dc_msg_get_file (const dc_msg_t* msg);
|
|||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a base file name without the path. The base file name includes the extension; the path
|
* Get an original attachment filename, with extension but without the path. To get the full path,
|
||||||
* is not returned. To get the full path, use dc_msg_get_file().
|
* use dc_msg_get_file().
|
||||||
*
|
*
|
||||||
* @memberof dc_msg_t
|
* @memberof dc_msg_t
|
||||||
* @param msg The message object.
|
* @param msg The message object.
|
||||||
* @return The base file name plus the extension without part. If there is no file
|
* @return The attachment filename. If there is no file associated with the message, an empty string
|
||||||
* associated with the message, an empty string is returned. The returned
|
* is returned. The returned value must be released using dc_str_unref().
|
||||||
* value must be released using dc_str_unref().
|
|
||||||
*/
|
*/
|
||||||
char* dc_msg_get_filename (const dc_msg_t* msg);
|
char* dc_msg_get_filename (const dc_msg_t* msg);
|
||||||
|
|
||||||
@@ -4313,7 +4356,7 @@ int dc_msg_is_forwarded (const dc_msg_t* msg);
|
|||||||
* Check if the message is an informational message, created by the
|
* Check if the message is an informational message, created by the
|
||||||
* device or by another users. Such messages are not "typed" by the user but
|
* device or by another users. Such messages are not "typed" by the user but
|
||||||
* created due to other actions,
|
* created due to other actions,
|
||||||
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(), dc_set_chat_protection()
|
* e.g. dc_set_chat_name(), dc_set_chat_profile_image(),
|
||||||
* or dc_add_contact_to_chat().
|
* or dc_add_contact_to_chat().
|
||||||
*
|
*
|
||||||
* These messages are typically shown in the center of the chat view,
|
* These messages are typically shown in the center of the chat view,
|
||||||
@@ -5007,7 +5050,12 @@ int dc_contact_is_verified (dc_contact_t* contact);
|
|||||||
/**
|
/**
|
||||||
* Return the address that verified a contact
|
* Return the address that verified a contact
|
||||||
*
|
*
|
||||||
* The UI may use this in addition to a checkmark showing the verification status
|
* The UI may use this in addition to a checkmark showing the verification status.
|
||||||
|
* In case of verification chains,
|
||||||
|
* the last contact in the chain is shown.
|
||||||
|
* This is because of privacy reasons, but also as it would not help the user
|
||||||
|
* to see a unknown name here - where one can mostly always ask the shown name
|
||||||
|
* as it is directly known.
|
||||||
*
|
*
|
||||||
* @memberof dc_contact_t
|
* @memberof dc_contact_t
|
||||||
* @param contact The contact object.
|
* @param contact The contact object.
|
||||||
@@ -5015,6 +5063,7 @@ int dc_contact_is_verified (dc_contact_t* contact);
|
|||||||
* A string containing the verifiers address. If it is the same address as the contact itself,
|
* A string containing the verifiers address. If it is the same address as the contact itself,
|
||||||
* we verified the contact ourself. If it is an empty string, we don't have verifier
|
* we verified the contact ourself. If it is an empty string, we don't have verifier
|
||||||
* information or the contact is not verified.
|
* information or the contact is not verified.
|
||||||
|
* @deprecated 2023-09-28, use dc_contact_get_verifier_id instead
|
||||||
*/
|
*/
|
||||||
char* dc_contact_get_verifier_addr (dc_contact_t* contact);
|
char* dc_contact_get_verifier_addr (dc_contact_t* contact);
|
||||||
|
|
||||||
@@ -5027,7 +5076,7 @@ char* dc_contact_get_verifier_addr (dc_contact_t* contact);
|
|||||||
* @memberof dc_contact_t
|
* @memberof dc_contact_t
|
||||||
* @param contact The contact object.
|
* @param contact The contact object.
|
||||||
* @return
|
* @return
|
||||||
* The `ContactId` of the verifiers address. If it is the same address as the contact itself,
|
* The contact ID of the verifier. If it is DC_CONTACT_ID_SELF,
|
||||||
* we verified the contact ourself. If it is 0, we don't have verifier information or
|
* we verified the contact ourself. If it is 0, we don't have verifier information or
|
||||||
* the contact is not verified.
|
* the contact is not verified.
|
||||||
*/
|
*/
|
||||||
@@ -5127,6 +5176,72 @@ int dc_provider_get_status (const dc_provider_t* prov
|
|||||||
void dc_provider_unref (dc_provider_t* provider);
|
void dc_provider_unref (dc_provider_t* provider);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an HTTP(S) GET response.
|
||||||
|
* This function can be used to download remote content for HTML emails.
|
||||||
|
*
|
||||||
|
* @memberof dc_context_t
|
||||||
|
* @param context The context object to take proxy settings from.
|
||||||
|
* @param url HTTP or HTTPS URL.
|
||||||
|
* @return The response must be released using dc_http_response_unref() after usage.
|
||||||
|
* NULL is returned on errors.
|
||||||
|
*/
|
||||||
|
dc_http_response_t* dc_get_http_response (const dc_context_t* context, const char* url);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @class dc_http_response_t
|
||||||
|
*
|
||||||
|
* An object containing an HTTP(S) GET response.
|
||||||
|
* Created by dc_get_http_response().
|
||||||
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTP response MIME type as a string, e.g. "text/plain" or "text/html".
|
||||||
|
*
|
||||||
|
* @memberof dc_http_response_t
|
||||||
|
* @param response HTTP response as returned by dc_get_http_response().
|
||||||
|
* @return The string which must be released using dc_str_unref() after usage. May be NULL.
|
||||||
|
*/
|
||||||
|
char* dc_http_response_get_mimetype (const dc_http_response_t* response);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTP response encoding, e.g. "utf-8".
|
||||||
|
*
|
||||||
|
* @memberof dc_http_response_t
|
||||||
|
* @param response HTTP response as returned by dc_get_http_response().
|
||||||
|
* @return The string which must be released using dc_str_unref() after usage. May be NULL.
|
||||||
|
*/
|
||||||
|
char* dc_http_response_get_encoding (const dc_http_response_t* response);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTP response contents.
|
||||||
|
*
|
||||||
|
* @memberof dc_http_response_t
|
||||||
|
* @param response HTTP response as returned by dc_get_http_response().
|
||||||
|
* @return The blob which must be released using dc_str_unref() after usage. NULL is never returned.
|
||||||
|
*/
|
||||||
|
uint8_t* dc_http_response_get_blob (const dc_http_response_t* response);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns HTTP response content size.
|
||||||
|
*
|
||||||
|
* @memberof dc_http_response_t
|
||||||
|
* @param response HTTP response as returned by dc_get_http_response().
|
||||||
|
* @return The blob size.
|
||||||
|
*/
|
||||||
|
size_t dc_http_response_get_size (const dc_http_response_t* response);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Free an HTTP response object.
|
||||||
|
*
|
||||||
|
* @memberof dc_http_response_t
|
||||||
|
* @param response HTTP response as returned by dc_get_http_response().
|
||||||
|
*/
|
||||||
|
void dc_http_response_unref (const dc_http_response_t* response);
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class dc_lot_t
|
* @class dc_lot_t
|
||||||
*
|
*
|
||||||
@@ -5604,7 +5719,6 @@ void dc_reactions_unref (dc_reactions_t* reactions);
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class dc_jsonrpc_instance_t
|
* @class dc_jsonrpc_instance_t
|
||||||
*
|
*
|
||||||
@@ -5653,6 +5767,17 @@ void dc_jsonrpc_request(dc_jsonrpc_instance_t* jsonrpc_instance, const char* req
|
|||||||
*/
|
*/
|
||||||
char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
|
char* dc_jsonrpc_next_response(dc_jsonrpc_instance_t* jsonrpc_instance);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Make a JSON-RPC call and return a response.
|
||||||
|
*
|
||||||
|
* @memberof dc_jsonrpc_instance_t
|
||||||
|
* @param jsonrpc_instance jsonrpc instance as returned from dc_jsonrpc_init().
|
||||||
|
* @param input JSON-RPC request.
|
||||||
|
* @return JSON-RPC response as string, must be freed using dc_str_unref() after usage.
|
||||||
|
* If there is no response, NULL is returned.
|
||||||
|
*/
|
||||||
|
char* dc_jsonrpc_blocking_call(dc_jsonrpc_instance_t* jsonrpc_instance, const char *input);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @class dc_event_emitter_t
|
* @class dc_event_emitter_t
|
||||||
*
|
*
|
||||||
@@ -6000,6 +6125,15 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
#define DC_EVENT_MSG_READ 2015
|
#define DC_EVENT_MSG_READ 2015
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A single message is deleted.
|
||||||
|
*
|
||||||
|
* @param data1 (int) chat_id
|
||||||
|
* @param data2 (int) msg_id
|
||||||
|
*/
|
||||||
|
#define DC_EVENT_MSG_DELETED 2016
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 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.
|
* Or the verify state of a chat has changed.
|
||||||
@@ -6178,6 +6312,7 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
#define DC_KEY_GEN_DEFAULT 0
|
#define DC_KEY_GEN_DEFAULT 0
|
||||||
#define DC_KEY_GEN_RSA2048 1
|
#define DC_KEY_GEN_RSA2048 1
|
||||||
#define DC_KEY_GEN_ED25519 2
|
#define DC_KEY_GEN_ED25519 2
|
||||||
|
#define DC_KEY_GEN_RSA4096 3
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -6661,15 +6796,6 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/// Used in error strings.
|
/// Used in error strings.
|
||||||
#define DC_STR_ERROR_NO_NETWORK 87
|
#define DC_STR_ERROR_NO_NETWORK 87
|
||||||
|
|
||||||
/// "Chat protection enabled."
|
|
||||||
///
|
|
||||||
|
|
||||||
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_ENABLED_PROTECTION and DC_STR_MSG_PROTECTION_ENABLED_BY.
|
|
||||||
#define DC_STR_PROTECTION_ENABLED 88
|
|
||||||
|
|
||||||
/// @deprecated Deprecated, replaced by DC_STR_MSG_YOU_DISABLED_PROTECTION and DC_STR_MSG_PROTECTION_DISABLED_BY.
|
|
||||||
#define DC_STR_PROTECTION_DISABLED 89
|
|
||||||
|
|
||||||
/// "Reply"
|
/// "Reply"
|
||||||
///
|
///
|
||||||
/// Used in summaries.
|
/// Used in summaries.
|
||||||
@@ -7114,26 +7240,6 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/// `%2$s` will be replaced by name and address of the contact.
|
/// `%2$s` will be replaced by name and address of the contact.
|
||||||
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
|
#define DC_STR_EPHEMERAL_TIMER_WEEKS_BY_OTHER 157
|
||||||
|
|
||||||
/// "You enabled chat protection."
|
|
||||||
///
|
|
||||||
/// Used in status messages.
|
|
||||||
#define DC_STR_PROTECTION_ENABLED_BY_YOU 158
|
|
||||||
|
|
||||||
/// "Chat protection enabled by %1$s."
|
|
||||||
///
|
|
||||||
/// `%1$s` will be replaced by name and address of the contact.
|
|
||||||
///
|
|
||||||
/// Used in status messages.
|
|
||||||
#define DC_STR_PROTECTION_ENABLED_BY_OTHER 159
|
|
||||||
|
|
||||||
/// "You disabled chat protection."
|
|
||||||
#define DC_STR_PROTECTION_DISABLED_BY_YOU 160
|
|
||||||
|
|
||||||
/// "Chat protection disabled by %1$s."
|
|
||||||
///
|
|
||||||
/// `%1$s` will be replaced by name and address of the contact.
|
|
||||||
#define DC_STR_PROTECTION_DISABLED_BY_OTHER 161
|
|
||||||
|
|
||||||
/// "Scan to set up second device for %1$s"
|
/// "Scan to set up second device for %1$s"
|
||||||
///
|
///
|
||||||
/// `%1$s` will be replaced by name and address of the account.
|
/// `%1$s` will be replaced by name and address of the account.
|
||||||
@@ -7144,6 +7250,16 @@ void dc_event_unref(dc_event_t* event);
|
|||||||
/// Used as a device message after a successful backup transfer.
|
/// Used as a device message after a successful backup transfer.
|
||||||
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
|
#define DC_STR_BACKUP_TRANSFER_MSG_BODY 163
|
||||||
|
|
||||||
|
/// "Messages are guaranteed to be end-to-end encrypted from now on."
|
||||||
|
///
|
||||||
|
/// Used in info messages.
|
||||||
|
#define DC_STR_CHAT_PROTECTION_ENABLED 170
|
||||||
|
|
||||||
|
/// "%1$s sent a message from another device."
|
||||||
|
///
|
||||||
|
/// Used in info messages.
|
||||||
|
#define DC_STR_CHAT_PROTECTION_DISABLED 171
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @}
|
* @}
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -29,8 +29,9 @@ use deltachat::contact::{Contact, ContactId, Origin};
|
|||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::ephemeral::Timer as EphemeralTimer;
|
use deltachat::ephemeral::Timer as EphemeralTimer;
|
||||||
use deltachat::imex::BackupProvider;
|
use deltachat::imex::BackupProvider;
|
||||||
use deltachat::key::DcKey;
|
use deltachat::key::preconfigure_keypair;
|
||||||
use deltachat::message::MsgId;
|
use deltachat::message::MsgId;
|
||||||
|
use deltachat::net::read_url_blob;
|
||||||
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||||
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
|
use deltachat::reaction::{get_msg_reactions, send_reaction, Reactions};
|
||||||
use deltachat::stock_str::StockMessage;
|
use deltachat::stock_str::StockMessage;
|
||||||
@@ -166,6 +167,24 @@ pub unsafe extern "C" fn dc_context_open(
|
|||||||
.unwrap_or(0)
|
.unwrap_or(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_context_change_passphrase(
|
||||||
|
context: *mut dc_context_t,
|
||||||
|
passphrase: *const libc::c_char,
|
||||||
|
) -> libc::c_int {
|
||||||
|
if context.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_context_change_passphrase()");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let ctx = &*context;
|
||||||
|
let passphrase = to_string_lossy(passphrase);
|
||||||
|
block_on(ctx.change_passphrase(passphrase))
|
||||||
|
.context("dc_context_change_passphrase() failed")
|
||||||
|
.log_err(ctx)
|
||||||
|
.is_ok() as libc::c_int
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int {
|
pub unsafe extern "C" fn dc_context_is_open(context: *mut dc_context_t) -> libc::c_int {
|
||||||
if context.is_null() {
|
if context.is_null() {
|
||||||
@@ -526,6 +545,7 @@ pub unsafe extern "C" fn dc_event_get_id(event: *mut dc_event_t) -> libc::c_int
|
|||||||
EventType::MsgDelivered { .. } => 2010,
|
EventType::MsgDelivered { .. } => 2010,
|
||||||
EventType::MsgFailed { .. } => 2012,
|
EventType::MsgFailed { .. } => 2012,
|
||||||
EventType::MsgRead { .. } => 2015,
|
EventType::MsgRead { .. } => 2015,
|
||||||
|
EventType::MsgDeleted { .. } => 2016,
|
||||||
EventType::ChatModified(_) => 2020,
|
EventType::ChatModified(_) => 2020,
|
||||||
EventType::ChatEphemeralTimerModified { .. } => 2021,
|
EventType::ChatEphemeralTimerModified { .. } => 2021,
|
||||||
EventType::ContactsChanged(_) => 2030,
|
EventType::ContactsChanged(_) => 2030,
|
||||||
@@ -573,6 +593,7 @@ pub unsafe extern "C" fn dc_event_get_data1_int(event: *mut dc_event_t) -> libc:
|
|||||||
| EventType::MsgDelivered { chat_id, .. }
|
| EventType::MsgDelivered { chat_id, .. }
|
||||||
| EventType::MsgFailed { chat_id, .. }
|
| EventType::MsgFailed { chat_id, .. }
|
||||||
| EventType::MsgRead { chat_id, .. }
|
| EventType::MsgRead { chat_id, .. }
|
||||||
|
| EventType::MsgDeleted { chat_id, .. }
|
||||||
| EventType::ChatModified(chat_id)
|
| EventType::ChatModified(chat_id)
|
||||||
| EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
|
| EventType::ChatEphemeralTimerModified { chat_id, .. } => chat_id.to_u32() as libc::c_int,
|
||||||
EventType::ContactsChanged(id) | EventType::LocationChanged(id) => {
|
EventType::ContactsChanged(id) | EventType::LocationChanged(id) => {
|
||||||
@@ -630,7 +651,8 @@ pub unsafe extern "C" fn dc_event_get_data2_int(event: *mut dc_event_t) -> libc:
|
|||||||
| EventType::IncomingMsg { msg_id, .. }
|
| EventType::IncomingMsg { msg_id, .. }
|
||||||
| EventType::MsgDelivered { msg_id, .. }
|
| EventType::MsgDelivered { msg_id, .. }
|
||||||
| EventType::MsgFailed { msg_id, .. }
|
| EventType::MsgFailed { msg_id, .. }
|
||||||
| EventType::MsgRead { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
| EventType::MsgRead { msg_id, .. }
|
||||||
|
| EventType::MsgDeleted { msg_id, .. } => msg_id.to_u32() as libc::c_int,
|
||||||
EventType::SecurejoinInviterProgress { progress, .. }
|
EventType::SecurejoinInviterProgress { progress, .. }
|
||||||
| EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
|
| EventType::SecurejoinJoinerProgress { progress, .. } => *progress as libc::c_int,
|
||||||
EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
|
EventType::ChatEphemeralTimerModified { timer, .. } => timer.to_u32() as libc::c_int,
|
||||||
@@ -673,6 +695,7 @@ pub unsafe extern "C" fn dc_event_get_data2_str(event: *mut dc_event_t) -> *mut
|
|||||||
| EventType::MsgDelivered { .. }
|
| EventType::MsgDelivered { .. }
|
||||||
| EventType::MsgFailed { .. }
|
| EventType::MsgFailed { .. }
|
||||||
| EventType::MsgRead { .. }
|
| EventType::MsgRead { .. }
|
||||||
|
| EventType::MsgDeleted { .. }
|
||||||
| EventType::ChatModified(_)
|
| EventType::ChatModified(_)
|
||||||
| EventType::ContactsChanged(_)
|
| EventType::ContactsChanged(_)
|
||||||
| EventType::LocationChanged(_)
|
| EventType::LocationChanged(_)
|
||||||
@@ -782,7 +805,7 @@ pub unsafe extern "C" fn dc_maybe_network(context: *mut dc_context_t) {
|
|||||||
pub unsafe extern "C" fn dc_preconfigure_keypair(
|
pub unsafe extern "C" fn dc_preconfigure_keypair(
|
||||||
context: *mut dc_context_t,
|
context: *mut dc_context_t,
|
||||||
addr: *const libc::c_char,
|
addr: *const libc::c_char,
|
||||||
public_data: *const libc::c_char,
|
_public_data: *const libc::c_char,
|
||||||
secret_data: *const libc::c_char,
|
secret_data: *const libc::c_char,
|
||||||
) -> i32 {
|
) -> i32 {
|
||||||
if context.is_null() {
|
if context.is_null() {
|
||||||
@@ -790,21 +813,12 @@ pub unsafe extern "C" fn dc_preconfigure_keypair(
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
block_on(async move {
|
let addr = to_string_lossy(addr);
|
||||||
let addr = tools::EmailAddress::new(&to_string_lossy(addr))?;
|
let secret_data = to_string_lossy(secret_data);
|
||||||
let public = key::SignedPublicKey::from_asc(&to_string_lossy(public_data))?.0;
|
block_on(preconfigure_keypair(ctx, &addr, &secret_data))
|
||||||
let secret = key::SignedSecretKey::from_asc(&to_string_lossy(secret_data))?.0;
|
.context("Failed to save keypair")
|
||||||
let keypair = key::KeyPair {
|
.log_err(ctx)
|
||||||
addr,
|
.is_ok() as libc::c_int
|
||||||
public,
|
|
||||||
secret,
|
|
||||||
};
|
|
||||||
key::store_self_keypair(ctx, &keypair, key::KeyPairUse::Default).await?;
|
|
||||||
Ok::<_, anyhow::Error>(1)
|
|
||||||
})
|
|
||||||
.context("Failed to save keypair")
|
|
||||||
.log_err(ctx)
|
|
||||||
.unwrap_or(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -1237,6 +1251,30 @@ pub unsafe extern "C" fn dc_get_fresh_msg_cnt(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_get_similar_chatlist(
|
||||||
|
context: *mut dc_context_t,
|
||||||
|
chat_id: u32,
|
||||||
|
) -> *mut dc_chatlist_t {
|
||||||
|
if context.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_get_similar_chatlist()");
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
let ctx = &*context;
|
||||||
|
|
||||||
|
let chat_id = ChatId::new(chat_id);
|
||||||
|
match block_on(chat_id.get_similar_chatlist(ctx))
|
||||||
|
.context("failed to get similar chatlist")
|
||||||
|
.log_err(ctx)
|
||||||
|
{
|
||||||
|
Ok(list) => {
|
||||||
|
let ffi_list = ChatlistWrapper { context, list };
|
||||||
|
Box::into_raw(Box::new(ffi_list))
|
||||||
|
}
|
||||||
|
Err(_) => ptr::null_mut(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_estimate_deletion_cnt(
|
pub unsafe extern "C" fn dc_estimate_deletion_cnt(
|
||||||
context: *mut dc_context_t,
|
context: *mut dc_context_t,
|
||||||
@@ -1384,6 +1422,7 @@ pub unsafe extern "C" fn dc_get_chat_media(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
|
#[allow(deprecated)]
|
||||||
pub unsafe extern "C" fn dc_get_next_media(
|
pub unsafe extern "C" fn dc_get_next_media(
|
||||||
context: *mut dc_context_t,
|
context: *mut dc_context_t,
|
||||||
msg_id: u32,
|
msg_id: u32,
|
||||||
@@ -1424,32 +1463,6 @@ pub unsafe extern "C" fn dc_get_next_media(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
|
||||||
pub unsafe extern "C" fn dc_set_chat_protection(
|
|
||||||
context: *mut dc_context_t,
|
|
||||||
chat_id: u32,
|
|
||||||
protect: libc::c_int,
|
|
||||||
) -> libc::c_int {
|
|
||||||
if context.is_null() {
|
|
||||||
eprintln!("ignoring careless call to dc_set_chat_protection()");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
let ctx = &*context;
|
|
||||||
let protect = if let Some(s) = ProtectionStatus::from_i32(protect) {
|
|
||||||
s
|
|
||||||
} else {
|
|
||||||
warn!(ctx, "bad protect-value for dc_set_chat_protection()");
|
|
||||||
return 0;
|
|
||||||
};
|
|
||||||
|
|
||||||
block_on(async move {
|
|
||||||
match ChatId::new(chat_id).set_protection(ctx, protect).await {
|
|
||||||
Ok(()) => 1,
|
|
||||||
Err(_) => 0,
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_set_chat_visibility(
|
pub unsafe extern "C" fn dc_set_chat_visibility(
|
||||||
context: *mut dc_context_t,
|
context: *mut dc_context_t,
|
||||||
@@ -1872,13 +1885,10 @@ pub unsafe extern "C" fn dc_get_msg_info(
|
|||||||
return "".strdup();
|
return "".strdup();
|
||||||
}
|
}
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
|
let msg_id = MsgId::new(msg_id);
|
||||||
block_on(async move {
|
block_on(msg_id.get_info(ctx))
|
||||||
message::get_msg_info(ctx, MsgId::new(msg_id))
|
.unwrap_or_log_default(ctx, "failed to get msg id")
|
||||||
.await
|
.strdup()
|
||||||
.unwrap_or_log_default(ctx, "failed to get msg id")
|
|
||||||
.strdup()
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -2523,7 +2533,12 @@ pub unsafe extern "C" fn dc_set_location(
|
|||||||
}
|
}
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
|
|
||||||
block_on(location::set(ctx, latitude, longitude, accuracy)) as _
|
block_on(async move {
|
||||||
|
location::set(ctx, latitude, longitude, accuracy)
|
||||||
|
.await
|
||||||
|
.log_err(ctx)
|
||||||
|
.unwrap_or_default()
|
||||||
|
}) as libc::c_int
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -3082,6 +3097,16 @@ pub unsafe extern "C" fn dc_chat_is_protected(chat: *mut dc_chat_t) -> libc::c_i
|
|||||||
ffi_chat.chat.is_protected() as libc::c_int
|
ffi_chat.chat.is_protected() as libc::c_int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_chat_is_protection_broken(chat: *mut dc_chat_t) -> libc::c_int {
|
||||||
|
if chat.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_chat_is_protection_broken()");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
let ffi_chat = &*chat;
|
||||||
|
ffi_chat.chat.is_protection_broken() as libc::c_int
|
||||||
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int {
|
pub unsafe extern "C" fn dc_chat_is_sending_locations(chat: *mut dc_chat_t) -> libc::c_int {
|
||||||
if chat.is_null() {
|
if chat.is_null() {
|
||||||
@@ -3303,7 +3328,7 @@ pub unsafe extern "C" fn dc_msg_get_text(msg: *mut dc_msg_t) -> *mut libc::c_cha
|
|||||||
return "".strdup();
|
return "".strdup();
|
||||||
}
|
}
|
||||||
let ffi_msg = &*msg;
|
let ffi_msg = &*msg;
|
||||||
ffi_msg.message.get_text().unwrap_or_default().strdup()
|
ffi_msg.message.get_text().strdup()
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -3688,7 +3713,7 @@ pub unsafe extern "C" fn dc_msg_set_text(msg: *mut dc_msg_t, text: *const libc::
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let ffi_msg = &mut *msg;
|
let ffi_msg = &mut *msg;
|
||||||
ffi_msg.message.set_text(to_opt_string_lossy(text))
|
ffi_msg.message.set_text(to_string_lossy(text))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -4487,7 +4512,14 @@ pub unsafe extern "C" fn dc_provider_new_from_email(
|
|||||||
|
|
||||||
let ctx = &*context;
|
let ctx = &*context;
|
||||||
|
|
||||||
match block_on(provider::get_provider_info(ctx, addr.as_str(), true)) {
|
match block_on(provider::get_provider_info_by_addr(
|
||||||
|
ctx,
|
||||||
|
addr.as_str(),
|
||||||
|
true,
|
||||||
|
))
|
||||||
|
.log_err(ctx)
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
Some(provider) => provider,
|
Some(provider) => provider,
|
||||||
None => ptr::null_mut(),
|
None => ptr::null_mut(),
|
||||||
}
|
}
|
||||||
@@ -4514,11 +4546,14 @@ pub unsafe extern "C" fn dc_provider_new_from_email_with_dns(
|
|||||||
|
|
||||||
match socks5_enabled {
|
match socks5_enabled {
|
||||||
Ok(socks5_enabled) => {
|
Ok(socks5_enabled) => {
|
||||||
match block_on(provider::get_provider_info(
|
match block_on(provider::get_provider_info_by_addr(
|
||||||
ctx,
|
ctx,
|
||||||
addr.as_str(),
|
addr.as_str(),
|
||||||
socks5_enabled,
|
socks5_enabled,
|
||||||
)) {
|
))
|
||||||
|
.log_err(ctx)
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
Some(provider) => provider,
|
Some(provider) => provider,
|
||||||
None => ptr::null_mut(),
|
None => ptr::null_mut(),
|
||||||
}
|
}
|
||||||
@@ -4572,6 +4607,96 @@ pub unsafe extern "C" fn dc_provider_unref(provider: *mut dc_provider_t) {
|
|||||||
// this may change once we start localizing string.
|
// this may change once we start localizing string.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// dc_http_response_t
|
||||||
|
|
||||||
|
pub type dc_http_response_t = net::HttpResponse;
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_get_http_response(
|
||||||
|
context: *const dc_context_t,
|
||||||
|
url: *const libc::c_char,
|
||||||
|
) -> *mut dc_http_response_t {
|
||||||
|
if context.is_null() || url.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_get_http_response()");
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
let context = &*context;
|
||||||
|
let url = to_string_lossy(url);
|
||||||
|
if let Ok(response) = block_on(read_url_blob(context, &url))
|
||||||
|
.context("read_url_blob")
|
||||||
|
.log_err(context)
|
||||||
|
{
|
||||||
|
Box::into_raw(Box::new(response))
|
||||||
|
} else {
|
||||||
|
ptr::null_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_http_response_get_mimetype(
|
||||||
|
response: *const dc_http_response_t,
|
||||||
|
) -> *mut libc::c_char {
|
||||||
|
if response.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_http_response_get_mimetype()");
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = &*response;
|
||||||
|
response.mimetype.strdup()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_http_response_get_encoding(
|
||||||
|
response: *const dc_http_response_t,
|
||||||
|
) -> *mut libc::c_char {
|
||||||
|
if response.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_http_response_get_encoding()");
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = &*response;
|
||||||
|
response.encoding.strdup()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_http_response_get_blob(
|
||||||
|
response: *const dc_http_response_t,
|
||||||
|
) -> *mut libc::c_char {
|
||||||
|
if response.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_http_response_get_blob()");
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = &*response;
|
||||||
|
let blob_len = response.blob.len();
|
||||||
|
let ptr = libc::malloc(blob_len);
|
||||||
|
libc::memcpy(ptr, response.blob.as_ptr() as *mut libc::c_void, blob_len);
|
||||||
|
ptr as *mut libc::c_char
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_http_response_get_size(
|
||||||
|
response: *const dc_http_response_t,
|
||||||
|
) -> libc::size_t {
|
||||||
|
if response.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_http_response_get_size()");
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
let response = &*response;
|
||||||
|
response.blob.len()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_http_response_unref(response: *mut dc_http_response_t) {
|
||||||
|
if response.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_http_response_unref()");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
drop(Box::from_raw(response));
|
||||||
|
}
|
||||||
|
|
||||||
// -- Accounts
|
// -- Accounts
|
||||||
|
|
||||||
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
|
/// Reader-writer lock wrapper for accounts manager to guarantee thread safety when using
|
||||||
@@ -4600,17 +4725,17 @@ pub type dc_accounts_t = AccountsWrapper;
|
|||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
pub unsafe extern "C" fn dc_accounts_new(
|
pub unsafe extern "C" fn dc_accounts_new(
|
||||||
_os_name: *const libc::c_char,
|
dir: *const libc::c_char,
|
||||||
dbfile: *const libc::c_char,
|
writable: libc::c_int,
|
||||||
) -> *mut dc_accounts_t {
|
) -> *mut dc_accounts_t {
|
||||||
setup_panic!();
|
setup_panic!();
|
||||||
|
|
||||||
if dbfile.is_null() {
|
if dir.is_null() {
|
||||||
eprintln!("ignoring careless call to dc_accounts_new()");
|
eprintln!("ignoring careless call to dc_accounts_new()");
|
||||||
return ptr::null_mut();
|
return ptr::null_mut();
|
||||||
}
|
}
|
||||||
|
|
||||||
let accs = block_on(Accounts::new(as_path(dbfile).into()));
|
let accs = block_on(Accounts::new(as_path(dir).into(), writable != 0));
|
||||||
|
|
||||||
match accs {
|
match accs {
|
||||||
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
|
Ok(accs) => Box::into_raw(Box::new(AccountsWrapper::new(accs))),
|
||||||
@@ -4876,7 +5001,6 @@ pub unsafe extern "C" fn dc_accounts_get_event_emitter(
|
|||||||
#[cfg(feature = "jsonrpc")]
|
#[cfg(feature = "jsonrpc")]
|
||||||
mod jsonrpc {
|
mod jsonrpc {
|
||||||
use deltachat_jsonrpc::api::CommandApi;
|
use deltachat_jsonrpc::api::CommandApi;
|
||||||
use deltachat_jsonrpc::events::event_to_json_rpc_notification;
|
|
||||||
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
use deltachat_jsonrpc::yerpc::{OutReceiver, RpcClient, RpcSession};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -4884,7 +5008,6 @@ mod jsonrpc {
|
|||||||
pub struct dc_jsonrpc_instance_t {
|
pub struct dc_jsonrpc_instance_t {
|
||||||
receiver: OutReceiver,
|
receiver: OutReceiver,
|
||||||
handle: RpcSession<CommandApi>,
|
handle: RpcSession<CommandApi>,
|
||||||
event_thread: JoinHandle<Result<(), anyhow::Error>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[no_mangle]
|
#[no_mangle]
|
||||||
@@ -4897,28 +5020,12 @@ mod jsonrpc {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let account_manager = &*account_manager;
|
let account_manager = &*account_manager;
|
||||||
let events = block_on(account_manager.read()).get_event_emitter();
|
|
||||||
let cmd_api = deltachat_jsonrpc::api::CommandApi::from_arc(account_manager.inner.clone());
|
let cmd_api = deltachat_jsonrpc::api::CommandApi::from_arc(account_manager.inner.clone());
|
||||||
|
|
||||||
let (request_handle, receiver) = RpcClient::new();
|
let (request_handle, receiver) = RpcClient::new();
|
||||||
let handle = RpcSession::new(request_handle.clone(), cmd_api);
|
let handle = RpcSession::new(request_handle, cmd_api);
|
||||||
|
|
||||||
let event_thread = spawn(async move {
|
let instance = dc_jsonrpc_instance_t { receiver, handle };
|
||||||
while let Some(event) = events.recv().await {
|
|
||||||
let event = event_to_json_rpc_notification(event);
|
|
||||||
request_handle
|
|
||||||
.send_notification("event", Some(event))
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
let res: Result<(), anyhow::Error> = Ok(());
|
|
||||||
res
|
|
||||||
});
|
|
||||||
|
|
||||||
let instance = dc_jsonrpc_instance_t {
|
|
||||||
receiver,
|
|
||||||
handle,
|
|
||||||
event_thread,
|
|
||||||
};
|
|
||||||
|
|
||||||
Box::into_raw(Box::new(instance))
|
Box::into_raw(Box::new(instance))
|
||||||
}
|
}
|
||||||
@@ -4929,7 +5036,6 @@ mod jsonrpc {
|
|||||||
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
|
eprintln!("ignoring careless call to dc_jsonrpc_unref()");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
(*jsonrpc_instance).event_thread.abort();
|
|
||||||
drop(Box::from_raw(jsonrpc_instance));
|
drop(Box::from_raw(jsonrpc_instance));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4967,4 +5073,28 @@ mod jsonrpc {
|
|||||||
.map(|result| serde_json::to_string(&result).unwrap_or_default().strdup())
|
.map(|result| serde_json::to_string(&result).unwrap_or_default().strdup())
|
||||||
.unwrap_or(ptr::null_mut())
|
.unwrap_or(ptr::null_mut())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[no_mangle]
|
||||||
|
pub unsafe extern "C" fn dc_jsonrpc_blocking_call(
|
||||||
|
jsonrpc_instance: *mut dc_jsonrpc_instance_t,
|
||||||
|
input: *const libc::c_char,
|
||||||
|
) -> *mut libc::c_char {
|
||||||
|
if jsonrpc_instance.is_null() {
|
||||||
|
eprintln!("ignoring careless call to dc_jsonrpc_blocking_call()");
|
||||||
|
return ptr::null_mut();
|
||||||
|
}
|
||||||
|
let api = &*jsonrpc_instance;
|
||||||
|
let input = to_string_lossy(input);
|
||||||
|
let res = block_on(api.handle.process_incoming(&input));
|
||||||
|
match res {
|
||||||
|
Some(message) => {
|
||||||
|
if let Ok(message) = serde_json::to_string(&message) {
|
||||||
|
message.strdup()
|
||||||
|
} else {
|
||||||
|
ptr::null_mut()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => ptr::null_mut(),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
1
deltachat-jsonrpc/.gitignore
vendored
1
deltachat-jsonrpc/.gitignore
vendored
@@ -1,3 +1,4 @@
|
|||||||
|
openrpc/openrpc.json
|
||||||
accounts/
|
accounts/
|
||||||
|
|
||||||
.cargo
|
.cargo
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-jsonrpc"
|
name = "deltachat-jsonrpc"
|
||||||
version = "1.112.6"
|
version = "1.126.1"
|
||||||
description = "DeltaChat JSON-RPC API"
|
description = "DeltaChat JSON-RPC API"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
default-run = "deltachat-jsonrpc-server"
|
default-run = "deltachat-jsonrpc-server"
|
||||||
@@ -15,25 +15,26 @@ required-features = ["webserver"]
|
|||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
deltachat = { path = ".." }
|
deltachat = { path = ".." }
|
||||||
num-traits = "0.2"
|
num-traits = "0.2"
|
||||||
|
schemars = "0.8.13"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tempfile = "3.3.0"
|
tempfile = "3.8.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
async-channel = { version = "1.8.0" }
|
async-channel = { version = "1.8.0" }
|
||||||
futures = { version = "0.3.28" }
|
futures = { version = "0.3.28" }
|
||||||
serde_json = "1.0.95"
|
serde_json = "1.0.105"
|
||||||
yerpc = { version = "0.4.3", features = ["anyhow_expose"] }
|
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||||
typescript-type-def = { version = "0.5.5", features = ["json_value"] }
|
typescript-type-def = { version = "0.5.8", features = ["json_value"] }
|
||||||
tokio = { version = "1.27.0" }
|
tokio = { version = "1.32.0" }
|
||||||
sanitize-filename = "0.4"
|
sanitize-filename = "0.5"
|
||||||
walkdir = "2.3.3"
|
walkdir = "2.3.3"
|
||||||
base64 = "0.21"
|
base64 = "0.21"
|
||||||
|
|
||||||
# optional dependencies
|
# optional dependencies
|
||||||
axum = { version = "0.6.12", optional = true, features = ["ws"] }
|
axum = { version = "0.6.20", optional = true, features = ["ws"] }
|
||||||
env_logger = { version = "0.10.0", optional = true }
|
env_logger = { version = "0.10.0", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
tokio = { version = "1.27.0", features = ["full", "rt-multi-thread"] }
|
tokio = { version = "1.32.0", features = ["full", "rt-multi-thread"] }
|
||||||
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -4,48 +4,47 @@ use std::{collections::HashMap, str::FromStr};
|
|||||||
|
|
||||||
use anyhow::{anyhow, bail, ensure, Context, Result};
|
use anyhow::{anyhow, bail, ensure, Context, Result};
|
||||||
pub use deltachat::accounts::Accounts;
|
pub use deltachat::accounts::Accounts;
|
||||||
use deltachat::qr::Qr;
|
use deltachat::chat::{
|
||||||
use deltachat::{
|
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
|
||||||
chat::{
|
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
||||||
self, add_contact_to_chat, forward_msgs, get_chat_media, get_chat_msgs, get_chat_msgs_ex,
|
ProtectionStatus,
|
||||||
marknoticed_chat, remove_contact_from_chat, Chat, ChatId, ChatItem, MessageListOptions,
|
|
||||||
ProtectionStatus,
|
|
||||||
},
|
|
||||||
chatlist::Chatlist,
|
|
||||||
config::Config,
|
|
||||||
constants::DC_MSG_ID_DAYMARKER,
|
|
||||||
contact::{may_be_valid_addr, Contact, ContactId, Origin},
|
|
||||||
context::get_info,
|
|
||||||
ephemeral::Timer,
|
|
||||||
imex, location,
|
|
||||||
message::{
|
|
||||||
self, delete_msgs, get_msg_info, markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
|
||||||
},
|
|
||||||
provider::get_provider_info,
|
|
||||||
qr,
|
|
||||||
qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg},
|
|
||||||
reaction::send_reaction,
|
|
||||||
securejoin,
|
|
||||||
stock_str::StockMessage,
|
|
||||||
webxdc::StatusUpdateSerial,
|
|
||||||
};
|
};
|
||||||
|
use deltachat::chatlist::Chatlist;
|
||||||
|
use deltachat::config::Config;
|
||||||
|
use deltachat::constants::DC_MSG_ID_DAYMARKER;
|
||||||
|
use deltachat::contact::{may_be_valid_addr, Contact, ContactId, Origin};
|
||||||
|
use deltachat::context::get_info;
|
||||||
|
use deltachat::ephemeral::Timer;
|
||||||
|
use deltachat::imex;
|
||||||
|
use deltachat::location;
|
||||||
|
use deltachat::message::get_msg_read_receipts;
|
||||||
|
use deltachat::message::{
|
||||||
|
self, delete_msgs, markseen_msgs, Message, MessageState, MsgId, Viewtype,
|
||||||
|
};
|
||||||
|
use deltachat::provider::get_provider_info;
|
||||||
|
use deltachat::qr::{self, Qr};
|
||||||
|
use deltachat::qr_code_generator::{generate_backup_qr, get_securejoin_qr_svg};
|
||||||
|
use deltachat::reaction::{get_msg_reactions, send_reaction};
|
||||||
|
use deltachat::securejoin;
|
||||||
|
use deltachat::stock_str::StockMessage;
|
||||||
|
use deltachat::webxdc::StatusUpdateSerial;
|
||||||
use sanitize_filename::is_sanitized;
|
use sanitize_filename::is_sanitized;
|
||||||
use tokio::fs;
|
use tokio::fs;
|
||||||
use tokio::sync::{watch, Mutex, RwLock};
|
use tokio::sync::{watch, Mutex, RwLock};
|
||||||
use walkdir::WalkDir;
|
use walkdir::WalkDir;
|
||||||
use yerpc::rpc;
|
use yerpc::rpc;
|
||||||
|
|
||||||
pub mod events;
|
|
||||||
pub mod types;
|
pub mod types;
|
||||||
|
|
||||||
use num_traits::FromPrimitive;
|
use num_traits::FromPrimitive;
|
||||||
use types::account::Account;
|
use types::account::Account;
|
||||||
use types::chat::FullChat;
|
use types::chat::FullChat;
|
||||||
use types::chat_list::ChatListEntry;
|
|
||||||
use types::contact::ContactObject;
|
use types::contact::ContactObject;
|
||||||
use types::message::MessageData;
|
use types::events::Event;
|
||||||
use types::message::MessageObject;
|
use types::http::HttpResponse;
|
||||||
|
use types::message::{MessageData, MessageObject, MessageReadReceipt};
|
||||||
use types::provider_info::ProviderInfo;
|
use types::provider_info::ProviderInfo;
|
||||||
|
use types::reactions::JSONRPCReactions;
|
||||||
use types::webxdc::WebxdcMessageInfo;
|
use types::webxdc::WebxdcMessageInfo;
|
||||||
|
|
||||||
use self::types::message::MessageLoadResult;
|
use self::types::message::MessageLoadResult;
|
||||||
@@ -154,16 +153,26 @@ impl CommandApi {
|
|||||||
// Misc top level functions
|
// Misc top level functions
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
|
|
||||||
/// Check if an email address is valid.
|
/// Checks if an email address is valid.
|
||||||
async fn check_email_validity(&self, email: String) -> bool {
|
async fn check_email_validity(&self, email: String) -> bool {
|
||||||
may_be_valid_addr(&email)
|
may_be_valid_addr(&email)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get general system info.
|
/// Returns general system info.
|
||||||
async fn get_system_info(&self) -> BTreeMap<&'static str, String> {
|
async fn get_system_info(&self) -> BTreeMap<&'static str, String> {
|
||||||
get_info()
|
get_info()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the next event.
|
||||||
|
async fn get_next_event(&self) -> Result<Event> {
|
||||||
|
let event_emitter = self.accounts.read().await.get_event_emitter();
|
||||||
|
event_emitter
|
||||||
|
.recv()
|
||||||
|
.await
|
||||||
|
.map(|event| event.into())
|
||||||
|
.context("event channel is closed")
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
// Account Management
|
// Account Management
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
@@ -205,18 +214,18 @@ impl CommandApi {
|
|||||||
let context_option = self.accounts.read().await.get_account(id);
|
let context_option = self.accounts.read().await.get_account(id);
|
||||||
if let Some(ctx) = context_option {
|
if let Some(ctx) = context_option {
|
||||||
accounts.push(Account::from_context(&ctx, id).await?)
|
accounts.push(Account::from_context(&ctx, id).await?)
|
||||||
} else {
|
|
||||||
println!("account with id {id} doesn't exist anymore");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(accounts)
|
Ok(accounts)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Starts background tasks for all accounts.
|
||||||
async fn start_io_for_all_accounts(&self) -> Result<()> {
|
async fn start_io_for_all_accounts(&self) -> Result<()> {
|
||||||
self.accounts.read().await.start_io().await;
|
self.accounts.read().await.start_io().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Stops background tasks for all accounts.
|
||||||
async fn stop_io_for_all_accounts(&self) -> Result<()> {
|
async fn stop_io_for_all_accounts(&self) -> Result<()> {
|
||||||
self.accounts.read().await.stop_io().await;
|
self.accounts.read().await.stop_io().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -226,14 +235,16 @@ impl CommandApi {
|
|||||||
// Methods that work on individual accounts
|
// Methods that work on individual accounts
|
||||||
// ---------------------------------------------
|
// ---------------------------------------------
|
||||||
|
|
||||||
async fn start_io(&self, id: u32) -> Result<()> {
|
/// Starts background tasks for a single account.
|
||||||
let ctx = self.get_context(id).await?;
|
async fn start_io(&self, account_id: u32) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
ctx.start_io().await;
|
ctx.start_io().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn stop_io(&self, id: u32) -> Result<()> {
|
/// Stops background tasks for a single account.
|
||||||
let ctx = self.get_context(id).await?;
|
async fn stop_io(&self, account_id: u32) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
ctx.stop_io().await;
|
ctx.stop_io().await;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -300,11 +311,13 @@ impl CommandApi {
|
|||||||
ctx.get_info().await
|
ctx.get_info().await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Sets the given configuration key.
|
||||||
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
|
async fn set_config(&self, account_id: u32, key: String, value: Option<String>) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
set_config(&ctx, &key, value.as_deref()).await
|
set_config(&ctx, &key, value.as_deref()).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Updates a batch of configuration values.
|
||||||
async fn batch_set_config(
|
async fn batch_set_config(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -336,6 +349,7 @@ impl CommandApi {
|
|||||||
Ok(qr_object)
|
Ok(qr_object)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns configuration value for the given key.
|
||||||
async fn get_config(&self, account_id: u32, key: String) -> Result<Option<String>> {
|
async fn get_config(&self, account_id: u32, key: String) -> Result<Option<String>> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
get_config(&ctx, &key).await
|
get_config(&ctx, &key).await
|
||||||
@@ -539,7 +553,7 @@ impl CommandApi {
|
|||||||
list_flags: Option<u32>,
|
list_flags: Option<u32>,
|
||||||
query_string: Option<String>,
|
query_string: Option<String>,
|
||||||
query_contact_id: Option<u32>,
|
query_contact_id: Option<u32>,
|
||||||
) -> Result<Vec<ChatListEntry>> {
|
) -> Result<Vec<u32>> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let list = Chatlist::try_load(
|
let list = Chatlist::try_load(
|
||||||
&ctx,
|
&ctx,
|
||||||
@@ -548,32 +562,43 @@ impl CommandApi {
|
|||||||
query_contact_id.map(ContactId::new),
|
query_contact_id.map(ContactId::new),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
let mut l: Vec<ChatListEntry> = Vec::with_capacity(list.len());
|
let mut l: Vec<u32> = Vec::with_capacity(list.len());
|
||||||
for i in 0..list.len() {
|
for i in 0..list.len() {
|
||||||
l.push(ChatListEntry(
|
l.push(list.get_chat_id(i)?.to_u32());
|
||||||
list.get_chat_id(i)?.to_u32(),
|
|
||||||
list.get_msg_id(i)?.unwrap_or_default().to_u32(),
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
Ok(l)
|
Ok(l)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns chats similar to the given one.
|
||||||
|
///
|
||||||
|
/// Experimental API, subject to change without notice.
|
||||||
|
async fn get_similar_chat_ids(&self, account_id: u32, chat_id: u32) -> Result<Vec<u32>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let chat_id = ChatId::new(chat_id);
|
||||||
|
let list = chat_id
|
||||||
|
.get_similar_chat_ids(&ctx)
|
||||||
|
.await?
|
||||||
|
.into_iter()
|
||||||
|
.map(|(chat_id, _metric)| chat_id.to_u32())
|
||||||
|
.collect();
|
||||||
|
Ok(list)
|
||||||
|
}
|
||||||
|
|
||||||
async fn get_chatlist_items_by_entries(
|
async fn get_chatlist_items_by_entries(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
entries: Vec<ChatListEntry>,
|
entries: Vec<u32>,
|
||||||
) -> Result<HashMap<u32, ChatListItemFetchResult>> {
|
) -> Result<HashMap<u32, ChatListItemFetchResult>> {
|
||||||
// todo custom json deserializer for ChatListEntry?
|
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let mut result: HashMap<u32, ChatListItemFetchResult> =
|
let mut result: HashMap<u32, ChatListItemFetchResult> =
|
||||||
HashMap::with_capacity(entries.len());
|
HashMap::with_capacity(entries.len());
|
||||||
for entry in entries.iter() {
|
for &entry in entries.iter() {
|
||||||
result.insert(
|
result.insert(
|
||||||
entry.0,
|
entry,
|
||||||
match get_chat_list_item_by_id(&ctx, entry).await {
|
match get_chat_list_item_by_id(&ctx, entry).await {
|
||||||
Ok(res) => res,
|
Ok(res) => res,
|
||||||
Err(err) => ChatListItemFetchResult::Error {
|
Err(err) => ChatListItemFetchResult::Error {
|
||||||
id: entry.0,
|
id: entry,
|
||||||
error: format!("{err:#}"),
|
error: format!("{err:#}"),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -790,24 +815,12 @@ impl CommandApi {
|
|||||||
/// Create a new broadcast list.
|
/// Create a new broadcast list.
|
||||||
///
|
///
|
||||||
/// Broadcast lists are similar to groups on the sending device,
|
/// Broadcast lists are similar to groups on the sending device,
|
||||||
/// however, recipients get the messages in normal one-to-one chats
|
/// however, recipients get the messages in a read-only chat
|
||||||
/// and will not be aware of other members.
|
/// and will see who the other members are.
|
||||||
///
|
///
|
||||||
/// Replies to broadcasts go only to the sender
|
/// For historical reasons, this function does not take a name directly,
|
||||||
/// and not to all broadcast recipients.
|
/// instead you have to set the name using dc_set_chat_name()
|
||||||
/// Moreover, replies will not appear in the broadcast list
|
/// after creating the broadcast list.
|
||||||
/// but in the one-to-one chat with the person answering.
|
|
||||||
///
|
|
||||||
/// The name and the image of the broadcast list is set automatically
|
|
||||||
/// and is visible to the sender only.
|
|
||||||
/// Not asking for these data allows more focused creation
|
|
||||||
/// and we bypass the question who will get which data.
|
|
||||||
/// Also, many users will have at most one broadcast list
|
|
||||||
/// so, a generic name and image is sufficient at the first place.
|
|
||||||
///
|
|
||||||
/// Later on, however, the name can be changed using dc_set_chat_name().
|
|
||||||
/// The image cannot be changed to have a unique, recognizable icon in the chat lists.
|
|
||||||
/// All in all, this is also what other messengers are doing here.
|
|
||||||
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
|
async fn create_broadcast_list(&self, account_id: u32) -> Result<u32> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
chat::create_broadcast_list(&ctx)
|
chat::create_broadcast_list(&ctx)
|
||||||
@@ -892,7 +905,7 @@ impl CommandApi {
|
|||||||
) -> Result<u32> {
|
) -> Result<u32> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let mut msg = Message::new(Viewtype::Text);
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
msg.set_text(Some(text));
|
msg.set_text(text);
|
||||||
let message_id =
|
let message_id =
|
||||||
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?;
|
deltachat::chat::add_device_msg(&ctx, Some(&label), Some(&mut msg)).await?;
|
||||||
Ok(message_id.to_u32())
|
Ok(message_id.to_u32())
|
||||||
@@ -1110,7 +1123,25 @@ impl CommandApi {
|
|||||||
/// max. text returned by dc_msg_get_text() (about 30000 characters).
|
/// max. text returned by dc_msg_get_text() (about 30000 characters).
|
||||||
async fn get_message_info(&self, account_id: u32, message_id: u32) -> Result<String> {
|
async fn get_message_info(&self, account_id: u32, message_id: u32) -> Result<String> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
get_msg_info(&ctx, MsgId::new(message_id)).await
|
MsgId::new(message_id).get_info(&ctx).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns contacts that sent read receipts and the time of reading.
|
||||||
|
async fn get_message_read_receipts(
|
||||||
|
&self,
|
||||||
|
account_id: u32,
|
||||||
|
message_id: u32,
|
||||||
|
) -> Result<Vec<MessageReadReceipt>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let receipts = get_msg_read_receipts(&ctx, MsgId::new(message_id))
|
||||||
|
.await?
|
||||||
|
.iter()
|
||||||
|
.map(|(contact_id, ts)| MessageReadReceipt {
|
||||||
|
contact_id: contact_id.to_u32(),
|
||||||
|
timestamp: *ts,
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
Ok(receipts)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Asks the core to start downloading a message fully.
|
/// Asks the core to start downloading a message fully.
|
||||||
@@ -1313,7 +1344,7 @@ impl CommandApi {
|
|||||||
) -> Result<()> {
|
) -> Result<()> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let contact_id = ContactId::new(contact_id);
|
let contact_id = ContactId::new(contact_id);
|
||||||
let contact = Contact::load_from_db(&ctx, contact_id).await?;
|
let contact = Contact::get_by_id(&ctx, contact_id).await?;
|
||||||
let addr = contact.get_addr();
|
let addr = contact.get_addr();
|
||||||
Contact::create(&ctx, &name, addr).await?;
|
Contact::create(&ctx, &name, addr).await?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -1387,6 +1418,10 @@ impl CommandApi {
|
|||||||
///
|
///
|
||||||
/// one combined call for getting chat::get_next_media for both directions
|
/// one combined call for getting chat::get_next_media for both directions
|
||||||
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
|
/// the manual chat::get_next_media in only one direction is not exposed by the jsonrpc yet
|
||||||
|
///
|
||||||
|
/// Deprecated 2023-10-03, use `get_chat_media` method
|
||||||
|
/// and navigate the returned array instead.
|
||||||
|
#[allow(deprecated)]
|
||||||
async fn get_neighboring_chat_media(
|
async fn get_neighboring_chat_media(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -1658,6 +1693,15 @@ impl CommandApi {
|
|||||||
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
|
Ok(general_purpose::STANDARD_NO_PAD.encode(blob))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Makes an HTTP GET request and returns a response.
|
||||||
|
///
|
||||||
|
/// `url` is the HTTP or HTTPS URL.
|
||||||
|
async fn get_http_response(&self, account_id: u32, url: String) -> Result<HttpResponse> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let response = deltachat::net::read_url_blob(&ctx, &url).await?.into();
|
||||||
|
Ok(response)
|
||||||
|
}
|
||||||
|
|
||||||
/// Forward messages to another chat.
|
/// Forward messages to another chat.
|
||||||
///
|
///
|
||||||
/// All types of messages can be forwarded,
|
/// All types of messages can be forwarded,
|
||||||
@@ -1675,6 +1719,20 @@ impl CommandApi {
|
|||||||
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
|
forward_msgs(&ctx, &message_ids, ChatId::new(chat_id)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resend messages and make information available for newly added chat members.
|
||||||
|
/// Resending sends out the original message, however, recipients and webxdc-status may differ.
|
||||||
|
/// Clients that already have the original message can still ignore the resent message as
|
||||||
|
/// they have tracked the state by dedicated updates.
|
||||||
|
///
|
||||||
|
/// Some messages cannot be resent, eg. info-messages, drafts, already pending messages or messages that are not sent by SELF.
|
||||||
|
///
|
||||||
|
/// message_ids all message IDs that should be resend. All messages must belong to the same chat.
|
||||||
|
async fn resend_messages(&self, account_id: u32, message_ids: Vec<u32>) -> Result<()> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let message_ids: Vec<MsgId> = message_ids.into_iter().map(MsgId::new).collect();
|
||||||
|
chat::resend_msgs(&ctx, &message_ids).await
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_sticker(
|
async fn send_sticker(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -1686,6 +1744,9 @@ impl CommandApi {
|
|||||||
let mut msg = Message::new(Viewtype::Sticker);
|
let mut msg = Message::new(Viewtype::Sticker);
|
||||||
msg.set_file(&sticker_path, None);
|
msg.set_file(&sticker_path, None);
|
||||||
|
|
||||||
|
// JSON-rpc does not need heuristics to turn [Viewtype::Sticker] into [Viewtype::Image]
|
||||||
|
msg.force_sticker();
|
||||||
|
|
||||||
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
|
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
|
||||||
Ok(message_id.to_u32())
|
Ok(message_id.to_u32())
|
||||||
}
|
}
|
||||||
@@ -1707,6 +1768,21 @@ impl CommandApi {
|
|||||||
Ok(message_id.to_u32())
|
Ok(message_id.to_u32())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns reactions to the message.
|
||||||
|
async fn get_message_reactions(
|
||||||
|
&self,
|
||||||
|
account_id: u32,
|
||||||
|
message_id: u32,
|
||||||
|
) -> Result<Option<JSONRPCReactions>> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
let reactions = get_msg_reactions(&ctx, MsgId::new(message_id)).await?;
|
||||||
|
if reactions.is_empty() {
|
||||||
|
Ok(None)
|
||||||
|
} else {
|
||||||
|
Ok(Some(reactions.into()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
|
async fn send_msg(&self, account_id: u32, chat_id: u32, data: MessageData) -> Result<u32> {
|
||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
let mut message = Message::new(if let Some(viewtype) = data.viewtype {
|
let mut message = Message::new(if let Some(viewtype) = data.viewtype {
|
||||||
@@ -1716,9 +1792,7 @@ impl CommandApi {
|
|||||||
} else {
|
} else {
|
||||||
Viewtype::Text
|
Viewtype::Text
|
||||||
});
|
});
|
||||||
if data.text.is_some() {
|
message.set_text(data.text.unwrap_or_default());
|
||||||
message.set_text(data.text);
|
|
||||||
}
|
|
||||||
if data.html.is_some() {
|
if data.html.is_some() {
|
||||||
message.set_html(data.html);
|
message.set_html(data.html);
|
||||||
}
|
}
|
||||||
@@ -1806,7 +1880,7 @@ impl CommandApi {
|
|||||||
.context("path conversion to string failed")
|
.context("path conversion to string failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// save a sticker to a collection/folder in the account's sticker folder
|
/// Saves a sticker to a collection/folder in the account's sticker folder.
|
||||||
async fn misc_save_sticker(
|
async fn misc_save_sticker(
|
||||||
&self,
|
&self,
|
||||||
account_id: u32,
|
account_id: u32,
|
||||||
@@ -1899,7 +1973,7 @@ impl CommandApi {
|
|||||||
let ctx = self.get_context(account_id).await?;
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
|
||||||
let mut msg = Message::new(Viewtype::Text);
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
msg.set_text(Some(text));
|
msg.set_text(text);
|
||||||
|
|
||||||
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
|
let message_id = deltachat::chat::send_msg(&ctx, ChatId::new(chat_id), &mut msg).await?;
|
||||||
Ok(message_id.to_u32())
|
Ok(message_id.to_u32())
|
||||||
@@ -1922,9 +1996,7 @@ impl CommandApi {
|
|||||||
} else {
|
} else {
|
||||||
Viewtype::Text
|
Viewtype::Text
|
||||||
});
|
});
|
||||||
if text.is_some() {
|
message.set_text(text.unwrap_or_default());
|
||||||
message.set_text(text);
|
|
||||||
}
|
|
||||||
if let Some(file) = file {
|
if let Some(file) = file {
|
||||||
message.set_file(file, None);
|
message.set_file(file, None);
|
||||||
}
|
}
|
||||||
@@ -1968,9 +2040,7 @@ impl CommandApi {
|
|||||||
} else {
|
} else {
|
||||||
Viewtype::Text
|
Viewtype::Text
|
||||||
});
|
});
|
||||||
if text.is_some() {
|
draft.set_text(text.unwrap_or_default());
|
||||||
draft.set_text(text);
|
|
||||||
}
|
|
||||||
if let Some(file) = file {
|
if let Some(file) = file {
|
||||||
draft.set_file(file, None);
|
draft.set_file(file, None);
|
||||||
}
|
}
|
||||||
@@ -1989,6 +2059,23 @@ impl CommandApi {
|
|||||||
|
|
||||||
ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await
|
ChatId::new(chat_id).set_draft(&ctx, Some(&mut draft)).await
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// send the chat's current set draft
|
||||||
|
async fn misc_send_draft(&self, account_id: u32, chat_id: u32) -> Result<u32> {
|
||||||
|
let ctx = self.get_context(account_id).await?;
|
||||||
|
if let Some(draft) = ChatId::new(chat_id).get_draft(&ctx).await? {
|
||||||
|
let mut draft = draft;
|
||||||
|
let msg_id = chat::send_msg(&ctx, ChatId::new(chat_id), &mut draft)
|
||||||
|
.await?
|
||||||
|
.to_u32();
|
||||||
|
Ok(msg_id)
|
||||||
|
} else {
|
||||||
|
Err(anyhow!(
|
||||||
|
"chat with id {} doesn't have draft message",
|
||||||
|
chat_id
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper functions (to prevent code duplication)
|
// Helper functions (to prevent code duplication)
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ use typescript_type_def::TypeDef;
|
|||||||
|
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "kind")]
|
||||||
pub enum Account {
|
pub enum Account {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
Configured {
|
Configured {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::time::{Duration, SystemTime};
|
use std::time::{Duration, SystemTime};
|
||||||
|
|
||||||
use anyhow::{anyhow, bail, Result};
|
use anyhow::{bail, Context as _, Result};
|
||||||
use deltachat::chat::{self, get_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;
|
||||||
@@ -13,7 +13,7 @@ use typescript_type_def::TypeDef;
|
|||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
use super::contact::ContactObject;
|
use super::contact::ContactObject;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct FullChat {
|
pub struct FullChat {
|
||||||
id: u32,
|
id: u32,
|
||||||
@@ -53,7 +53,9 @@ impl FullChat {
|
|||||||
contacts.push(
|
contacts.push(
|
||||||
ContactObject::try_from_dc_contact(
|
ContactObject::try_from_dc_contact(
|
||||||
context,
|
context,
|
||||||
Contact::load_from_db(context, *contact_id).await?,
|
Contact::get_by_id(context, *contact_id)
|
||||||
|
.await
|
||||||
|
.context("failed to load contact")?,
|
||||||
)
|
)
|
||||||
.await?,
|
.await?,
|
||||||
)
|
)
|
||||||
@@ -72,8 +74,9 @@ impl FullChat {
|
|||||||
|
|
||||||
let was_seen_recently = if chat.get_type() == Chattype::Single {
|
let was_seen_recently = if chat.get_type() == Chattype::Single {
|
||||||
match contact_ids.get(0) {
|
match contact_ids.get(0) {
|
||||||
Some(contact) => Contact::load_from_db(context, *contact)
|
Some(contact) => Contact::get_by_id(context, *contact)
|
||||||
.await?
|
.await
|
||||||
|
.context("failed to load contact for was_seen_recently")?
|
||||||
.was_seen_recently(),
|
.was_seen_recently(),
|
||||||
None => false,
|
None => false,
|
||||||
}
|
}
|
||||||
@@ -89,10 +92,7 @@ impl FullChat {
|
|||||||
is_protected: chat.is_protected(),
|
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,
|
||||||
chat_type: chat
|
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||||
.get_type()
|
|
||||||
.to_u32()
|
|
||||||
.ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap?
|
|
||||||
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,
|
||||||
@@ -121,7 +121,7 @@ impl FullChat {
|
|||||||
/// - can_send
|
/// - can_send
|
||||||
///
|
///
|
||||||
/// used when you only need the basic metadata of a chat like type, name, profile picture
|
/// used when you only need the basic metadata of a chat like type, name, profile picture
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct BasicChat {
|
pub struct BasicChat {
|
||||||
id: u32,
|
id: u32,
|
||||||
@@ -155,10 +155,7 @@ impl BasicChat {
|
|||||||
is_protected: chat.is_protected(),
|
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,
|
||||||
chat_type: chat
|
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||||
.get_type()
|
|
||||||
.to_u32()
|
|
||||||
.ok_or_else(|| anyhow!("unknown chat type id"))?, // TODO get rid of this unwrap?
|
|
||||||
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,
|
||||||
@@ -169,11 +166,12 @@ impl BasicChat {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, TypeDef)]
|
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(tag = "kind")]
|
||||||
pub enum MuteDuration {
|
pub enum MuteDuration {
|
||||||
NotMuted,
|
NotMuted,
|
||||||
Forever,
|
Forever,
|
||||||
Until(i64),
|
Until { duration: i64 },
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MuteDuration {
|
impl MuteDuration {
|
||||||
@@ -181,20 +179,20 @@ impl MuteDuration {
|
|||||||
match self {
|
match self {
|
||||||
MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted),
|
MuteDuration::NotMuted => Ok(chat::MuteDuration::NotMuted),
|
||||||
MuteDuration::Forever => Ok(chat::MuteDuration::Forever),
|
MuteDuration::Forever => Ok(chat::MuteDuration::Forever),
|
||||||
MuteDuration::Until(n) => {
|
MuteDuration::Until { duration } => {
|
||||||
if n <= 0 {
|
if duration <= 0 {
|
||||||
bail!("failed to read mute duration")
|
bail!("failed to read mute duration")
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(SystemTime::now()
|
Ok(SystemTime::now()
|
||||||
.checked_add(Duration::from_secs(n as u64))
|
.checked_add(Duration::from_secs(duration as u64))
|
||||||
.map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until))
|
.map_or(chat::MuteDuration::Forever, chat::MuteDuration::Until))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Deserialize, TypeDef)]
|
#[derive(Clone, Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "ChatVisibility")]
|
#[serde(rename = "ChatVisibility")]
|
||||||
pub enum JSONRPCChatVisibility {
|
pub enum JSONRPCChatVisibility {
|
||||||
Normal,
|
Normal,
|
||||||
|
|||||||
@@ -1,25 +1,21 @@
|
|||||||
use anyhow::Result;
|
use anyhow::{Context, Result};
|
||||||
|
use deltachat::chat::{Chat, ChatId};
|
||||||
|
use deltachat::chatlist::get_last_message_for_chat;
|
||||||
use deltachat::constants::*;
|
use deltachat::constants::*;
|
||||||
use deltachat::contact::{Contact, ContactId};
|
use deltachat::contact::{Contact, ContactId};
|
||||||
use deltachat::{
|
use deltachat::{
|
||||||
chat::{get_chat_contacts, ChatVisibility},
|
chat::{get_chat_contacts, ChatVisibility},
|
||||||
chatlist::Chatlist,
|
chatlist::Chatlist,
|
||||||
};
|
};
|
||||||
use deltachat::{
|
|
||||||
chat::{Chat, ChatId},
|
|
||||||
message::MsgId,
|
|
||||||
};
|
|
||||||
use num_traits::cast::ToPrimitive;
|
use num_traits::cast::ToPrimitive;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
|
use super::message::MessageViewtype;
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
pub struct ChatListEntry(pub u32, pub u32);
|
#[serde(tag = "kind")]
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
|
||||||
#[serde(tag = "type")]
|
|
||||||
pub enum ChatListItemFetchResult {
|
pub enum ChatListItemFetchResult {
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ChatListItem {
|
ChatListItem {
|
||||||
@@ -31,6 +27,8 @@ pub enum ChatListItemFetchResult {
|
|||||||
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
|
||||||
|
summary_preview_image: Option<String>,
|
||||||
is_protected: bool,
|
is_protected: bool,
|
||||||
is_group: bool,
|
is_group: bool,
|
||||||
fresh_message_counter: usize,
|
fresh_message_counter: usize,
|
||||||
@@ -47,6 +45,8 @@ pub enum ChatListItemFetchResult {
|
|||||||
/// 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,
|
||||||
|
last_message_type: Option<MessageViewtype>,
|
||||||
|
last_message_id: Option<u32>,
|
||||||
},
|
},
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
ArchiveLink { fresh_message_counter: usize },
|
ArchiveLink { fresh_message_counter: usize },
|
||||||
@@ -56,14 +56,9 @@ pub enum ChatListItemFetchResult {
|
|||||||
|
|
||||||
pub(crate) async fn get_chat_list_item_by_id(
|
pub(crate) async fn get_chat_list_item_by_id(
|
||||||
ctx: &deltachat::context::Context,
|
ctx: &deltachat::context::Context,
|
||||||
entry: &ChatListEntry,
|
entry: u32,
|
||||||
) -> Result<ChatListItemFetchResult> {
|
) -> Result<ChatListItemFetchResult> {
|
||||||
let chat_id = ChatId::new(entry.0);
|
let chat_id = ChatId::new(entry);
|
||||||
let last_msgid = match entry.1 {
|
|
||||||
0 => None,
|
|
||||||
_ => Some(MsgId::new(entry.1)),
|
|
||||||
};
|
|
||||||
|
|
||||||
let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?;
|
let fresh_message_counter = chat_id.get_fresh_msg_cnt(ctx).await?;
|
||||||
|
|
||||||
if chat_id.is_archived_link() {
|
if chat_id.is_archived_link() {
|
||||||
@@ -72,12 +67,18 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let chat = Chat::load_from_db(ctx, chat_id).await?;
|
let last_msgid = get_last_message_for_chat(ctx, chat_id).await?;
|
||||||
let summary = Chatlist::get_summary2(ctx, chat_id, last_msgid, Some(&chat)).await?;
|
|
||||||
|
let chat = Chat::load_from_db(ctx, chat_id).await.context("chat")?;
|
||||||
|
let summary = Chatlist::get_summary2(ctx, chat_id, last_msgid, Some(&chat))
|
||||||
|
.await
|
||||||
|
.context("summary")?;
|
||||||
|
|
||||||
let summary_text1 = summary.prefix.map_or_else(String::new, |s| s.to_string());
|
let summary_text1 = summary.prefix.map_or_else(String::new, |s| s.to_string());
|
||||||
let summary_text2 = summary.text.to_owned();
|
let summary_text2 = summary.text.to_owned();
|
||||||
|
|
||||||
|
let summary_preview_image = summary.thumbnail_path;
|
||||||
|
|
||||||
let visibility = chat.get_visibility();
|
let visibility = chat.get_visibility();
|
||||||
|
|
||||||
let avatar_path = chat
|
let avatar_path = chat
|
||||||
@@ -85,12 +86,15 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
.await?
|
.await?
|
||||||
.map(|path| path.to_str().unwrap_or("invalid/path").to_owned());
|
.map(|path| path.to_str().unwrap_or("invalid/path").to_owned());
|
||||||
|
|
||||||
let last_updated = match last_msgid {
|
let (last_updated, message_type) = match last_msgid {
|
||||||
Some(id) => {
|
Some(id) => {
|
||||||
let last_message = deltachat::message::Message::load_from_db(ctx, id).await?;
|
let last_message = deltachat::message::Message::load_from_db(ctx, id).await?;
|
||||||
Some(last_message.get_timestamp() * 1000)
|
(
|
||||||
|
Some(last_message.get_timestamp() * 1000),
|
||||||
|
Some(last_message.get_viewtype().into()),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
None => None,
|
None => (None, None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
let chat_contacts = get_chat_contacts(ctx, chat_id).await?;
|
||||||
@@ -100,8 +104,9 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
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 contact = chat_contacts.get(0);
|
let contact = chat_contacts.get(0);
|
||||||
let was_seen_recently = match contact {
|
let was_seen_recently = match contact {
|
||||||
Some(contact) => Contact::load_from_db(ctx, *contact)
|
Some(contact) => Contact::get_by_id(ctx, *contact)
|
||||||
.await?
|
.await
|
||||||
|
.context("contact")?
|
||||||
.was_seen_recently(),
|
.was_seen_recently(),
|
||||||
None => false,
|
None => false,
|
||||||
};
|
};
|
||||||
@@ -124,6 +129,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
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,
|
||||||
is_protected: chat.is_protected(),
|
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,
|
||||||
@@ -138,5 +144,7 @@ pub(crate) async fn get_chat_list_item_by_id(
|
|||||||
is_broadcast: chat.get_type() == Chattype::Broadcast,
|
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_id: last_msgid.map(|id| id.to_u32()),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use typescript_type_def::TypeDef;
|
|||||||
|
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "Contact", rename_all = "camelCase")]
|
#[serde(rename = "Contact", rename_all = "camelCase")]
|
||||||
pub struct ContactObject {
|
pub struct ContactObject {
|
||||||
address: String,
|
address: String,
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
use deltachat::{Event, EventType};
|
use deltachat::{Event as CoreEvent, EventType as CoreEventType};
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use serde_json::{json, Value};
|
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
pub fn event_to_json_rpc_notification(event: Event) -> Value {
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
let id: JSONRPCEventType = event.typ.into();
|
#[serde(rename_all = "camelCase")]
|
||||||
json!({
|
pub struct Event {
|
||||||
"event": id,
|
/// Event payload.
|
||||||
"contextId": event.id,
|
event: EventType,
|
||||||
})
|
|
||||||
|
/// Account ID.
|
||||||
|
context_id: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
impl From<CoreEvent> for Event {
|
||||||
#[serde(tag = "type", rename = "Event")]
|
fn from(event: CoreEvent) -> Self {
|
||||||
pub enum JSONRPCEventType {
|
Event {
|
||||||
|
event: event.typ.into(),
|
||||||
|
context_id: event.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(tag = "kind")]
|
||||||
|
pub enum EventType {
|
||||||
/// The library-user may write an informational string to the log.
|
/// The library-user may write an informational string to the log.
|
||||||
///
|
///
|
||||||
/// This event should *not* be reported to the end-user using a popup or something like
|
/// This event should *not* be reported to the end-user using a popup or something like
|
||||||
@@ -164,6 +174,13 @@ pub enum JSONRPCEventType {
|
|||||||
msg_id: u32,
|
msg_id: u32,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/// A single message is deleted.
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
MsgDeleted {
|
||||||
|
chat_id: u32,
|
||||||
|
msg_id: u32,
|
||||||
|
},
|
||||||
|
|
||||||
/// Chat changed. The name or the image of a chat group was changed or members were added or removed.
|
/// 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.
|
/// Or the verify state of a chat has changed.
|
||||||
/// See setChatName(), setChatProfileImage(), addContactToChat()
|
/// See setChatName(), setChatProfileImage(), addContactToChat()
|
||||||
@@ -286,27 +303,27 @@ pub enum JSONRPCEventType {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<EventType> for JSONRPCEventType {
|
impl From<CoreEventType> for EventType {
|
||||||
fn from(event: EventType) -> Self {
|
fn from(event: CoreEventType) -> Self {
|
||||||
use JSONRPCEventType::*;
|
use EventType::*;
|
||||||
match event {
|
match event {
|
||||||
EventType::Info(msg) => Info { msg },
|
CoreEventType::Info(msg) => Info { msg },
|
||||||
EventType::SmtpConnected(msg) => SmtpConnected { msg },
|
CoreEventType::SmtpConnected(msg) => SmtpConnected { msg },
|
||||||
EventType::ImapConnected(msg) => ImapConnected { msg },
|
CoreEventType::ImapConnected(msg) => ImapConnected { msg },
|
||||||
EventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
|
CoreEventType::SmtpMessageSent(msg) => SmtpMessageSent { msg },
|
||||||
EventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
|
CoreEventType::ImapMessageDeleted(msg) => ImapMessageDeleted { msg },
|
||||||
EventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
|
CoreEventType::ImapMessageMoved(msg) => ImapMessageMoved { msg },
|
||||||
EventType::ImapInboxIdle => ImapInboxIdle,
|
CoreEventType::ImapInboxIdle => ImapInboxIdle,
|
||||||
EventType::NewBlobFile(file) => NewBlobFile { file },
|
CoreEventType::NewBlobFile(file) => NewBlobFile { file },
|
||||||
EventType::DeletedBlobFile(file) => DeletedBlobFile { file },
|
CoreEventType::DeletedBlobFile(file) => DeletedBlobFile { file },
|
||||||
EventType::Warning(msg) => Warning { msg },
|
CoreEventType::Warning(msg) => Warning { msg },
|
||||||
EventType::Error(msg) => Error { msg },
|
CoreEventType::Error(msg) => Error { msg },
|
||||||
EventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg },
|
CoreEventType::ErrorSelfNotInGroup(msg) => ErrorSelfNotInGroup { msg },
|
||||||
EventType::MsgsChanged { chat_id, msg_id } => MsgsChanged {
|
CoreEventType::MsgsChanged { chat_id, msg_id } => MsgsChanged {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
EventType::ReactionsChanged {
|
CoreEventType::ReactionsChanged {
|
||||||
chat_id,
|
chat_id,
|
||||||
msg_id,
|
msg_id,
|
||||||
contact_id,
|
contact_id,
|
||||||
@@ -315,92 +332,80 @@ impl From<EventType> for JSONRPCEventType {
|
|||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
contact_id: contact_id.to_u32(),
|
contact_id: contact_id.to_u32(),
|
||||||
},
|
},
|
||||||
EventType::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(),
|
||||||
},
|
},
|
||||||
EventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
|
CoreEventType::IncomingMsgBunch { msg_ids } => IncomingMsgBunch {
|
||||||
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
|
msg_ids: msg_ids.into_iter().map(|id| id.to_u32()).collect(),
|
||||||
},
|
},
|
||||||
EventType::MsgsNoticed(chat_id) => MsgsNoticed {
|
CoreEventType::MsgsNoticed(chat_id) => MsgsNoticed {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
},
|
},
|
||||||
EventType::MsgDelivered { chat_id, msg_id } => MsgDelivered {
|
CoreEventType::MsgDelivered { chat_id, msg_id } => MsgDelivered {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
EventType::MsgFailed { chat_id, msg_id } => MsgFailed {
|
CoreEventType::MsgFailed { chat_id, msg_id } => MsgFailed {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
EventType::MsgRead { chat_id, msg_id } => MsgRead {
|
CoreEventType::MsgRead { chat_id, msg_id } => MsgRead {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
EventType::ChatModified(chat_id) => ChatModified {
|
CoreEventType::MsgDeleted { chat_id, msg_id } => MsgDeleted {
|
||||||
|
chat_id: chat_id.to_u32(),
|
||||||
|
msg_id: msg_id.to_u32(),
|
||||||
|
},
|
||||||
|
CoreEventType::ChatModified(chat_id) => ChatModified {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
},
|
},
|
||||||
EventType::ChatEphemeralTimerModified { chat_id, timer } => {
|
CoreEventType::ChatEphemeralTimerModified { chat_id, timer } => {
|
||||||
ChatEphemeralTimerModified {
|
ChatEphemeralTimerModified {
|
||||||
chat_id: chat_id.to_u32(),
|
chat_id: chat_id.to_u32(),
|
||||||
timer: timer.to_u32(),
|
timer: timer.to_u32(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EventType::ContactsChanged(contact) => ContactsChanged {
|
CoreEventType::ContactsChanged(contact) => ContactsChanged {
|
||||||
contact_id: contact.map(|c| c.to_u32()),
|
contact_id: contact.map(|c| c.to_u32()),
|
||||||
},
|
},
|
||||||
EventType::LocationChanged(contact) => LocationChanged {
|
CoreEventType::LocationChanged(contact) => LocationChanged {
|
||||||
contact_id: contact.map(|c| c.to_u32()),
|
contact_id: contact.map(|c| c.to_u32()),
|
||||||
},
|
},
|
||||||
EventType::ConfigureProgress { progress, comment } => {
|
CoreEventType::ConfigureProgress { progress, comment } => {
|
||||||
ConfigureProgress { progress, comment }
|
ConfigureProgress { progress, comment }
|
||||||
}
|
}
|
||||||
EventType::ImexProgress(progress) => ImexProgress { progress },
|
CoreEventType::ImexProgress(progress) => ImexProgress { progress },
|
||||||
EventType::ImexFileWritten(path) => ImexFileWritten {
|
CoreEventType::ImexFileWritten(path) => ImexFileWritten {
|
||||||
path: path.to_str().unwrap_or_default().to_owned(),
|
path: path.to_str().unwrap_or_default().to_owned(),
|
||||||
},
|
},
|
||||||
EventType::SecurejoinInviterProgress {
|
CoreEventType::SecurejoinInviterProgress {
|
||||||
contact_id,
|
contact_id,
|
||||||
progress,
|
progress,
|
||||||
} => SecurejoinInviterProgress {
|
} => SecurejoinInviterProgress {
|
||||||
contact_id: contact_id.to_u32(),
|
contact_id: contact_id.to_u32(),
|
||||||
progress,
|
progress,
|
||||||
},
|
},
|
||||||
EventType::SecurejoinJoinerProgress {
|
CoreEventType::SecurejoinJoinerProgress {
|
||||||
contact_id,
|
contact_id,
|
||||||
progress,
|
progress,
|
||||||
} => SecurejoinJoinerProgress {
|
} => SecurejoinJoinerProgress {
|
||||||
contact_id: contact_id.to_u32(),
|
contact_id: contact_id.to_u32(),
|
||||||
progress,
|
progress,
|
||||||
},
|
},
|
||||||
EventType::ConnectivityChanged => ConnectivityChanged,
|
CoreEventType::ConnectivityChanged => ConnectivityChanged,
|
||||||
EventType::SelfavatarChanged => SelfavatarChanged,
|
CoreEventType::SelfavatarChanged => SelfavatarChanged,
|
||||||
EventType::WebxdcStatusUpdate {
|
CoreEventType::WebxdcStatusUpdate {
|
||||||
msg_id,
|
msg_id,
|
||||||
status_update_serial,
|
status_update_serial,
|
||||||
} => WebxdcStatusUpdate {
|
} => WebxdcStatusUpdate {
|
||||||
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(),
|
||||||
},
|
},
|
||||||
EventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
|
CoreEventType::WebxdcInstanceDeleted { msg_id } => WebxdcInstanceDeleted {
|
||||||
msg_id: msg_id.to_u32(),
|
msg_id: msg_id.to_u32(),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
#[test]
|
|
||||||
fn generate_events_ts_types_definition() {
|
|
||||||
let events = {
|
|
||||||
let mut buf = Vec::new();
|
|
||||||
let options = typescript_type_def::DefinitionFileOptions {
|
|
||||||
root_namespace: None,
|
|
||||||
..typescript_type_def::DefinitionFileOptions::default()
|
|
||||||
};
|
|
||||||
typescript_type_def::write_definition_file::<_, JSONRPCEventType>(&mut buf, options)
|
|
||||||
.unwrap();
|
|
||||||
String::from_utf8(buf).unwrap()
|
|
||||||
};
|
|
||||||
std::fs::write("typescript/generated/events.ts", events).unwrap();
|
|
||||||
}
|
|
||||||
29
deltachat-jsonrpc/src/api/types/http.rs
Normal file
29
deltachat-jsonrpc/src/api/types/http.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
use deltachat::net::HttpResponse as CoreHttpResponse;
|
||||||
|
use serde::Serialize;
|
||||||
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
pub struct HttpResponse {
|
||||||
|
/// base64-encoded response body.
|
||||||
|
blob: String,
|
||||||
|
|
||||||
|
/// MIME type, e.g. "text/plain" or "text/html".
|
||||||
|
mimetype: Option<String>,
|
||||||
|
|
||||||
|
/// Encoding, e.g. "utf-8".
|
||||||
|
encoding: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<CoreHttpResponse> for HttpResponse {
|
||||||
|
fn from(response: CoreHttpResponse) -> Self {
|
||||||
|
use base64::{engine::general_purpose, Engine as _};
|
||||||
|
let blob = general_purpose::STANDARD_NO_PAD.encode(response.blob);
|
||||||
|
let mimetype = response.mimetype;
|
||||||
|
let encoding = response.encoding;
|
||||||
|
HttpResponse {
|
||||||
|
blob,
|
||||||
|
mimetype,
|
||||||
|
encoding,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ use deltachat::location::Location;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "Location", rename_all = "camelCase")]
|
#[serde(rename = "Location", rename_all = "camelCase")]
|
||||||
pub struct JsonrpcLocation {
|
pub struct JsonrpcLocation {
|
||||||
pub location_id: u32,
|
pub location_id: u32,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use anyhow::{anyhow, Result};
|
use anyhow::{Context as _, Result};
|
||||||
use deltachat::chat::Chat;
|
use deltachat::chat::Chat;
|
||||||
use deltachat::chat::ChatItem;
|
use deltachat::chat::ChatItem;
|
||||||
use deltachat::constants::Chattype;
|
use deltachat::chat::ChatVisibility;
|
||||||
use deltachat::contact::Contact;
|
use deltachat::contact::Contact;
|
||||||
use deltachat::context::Context;
|
use deltachat::context::Context;
|
||||||
use deltachat::download;
|
use deltachat::download;
|
||||||
@@ -10,8 +10,7 @@ use deltachat::message::MsgId;
|
|||||||
use deltachat::message::Viewtype;
|
use deltachat::message::Viewtype;
|
||||||
use deltachat::reaction::get_msg_reactions;
|
use deltachat::reaction::get_msg_reactions;
|
||||||
use num_traits::cast::ToPrimitive;
|
use num_traits::cast::ToPrimitive;
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
use serde::Serialize;
|
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
use super::color_int_to_hex_string;
|
use super::color_int_to_hex_string;
|
||||||
@@ -19,14 +18,14 @@ use super::contact::ContactObject;
|
|||||||
use super::reactions::JSONRPCReactions;
|
use super::reactions::JSONRPCReactions;
|
||||||
use super::webxdc::WebxdcMessageInfo;
|
use super::webxdc::WebxdcMessageInfo;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase", tag = "variant")]
|
#[serde(rename_all = "camelCase", tag = "kind")]
|
||||||
pub enum MessageLoadResult {
|
pub enum MessageLoadResult {
|
||||||
Message(MessageObject),
|
Message(MessageObject),
|
||||||
LoadingError { error: String },
|
LoadingError { error: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "Message", rename_all = "camelCase")]
|
#[serde(rename = "Message", rename_all = "camelCase")]
|
||||||
pub struct MessageObject {
|
pub struct MessageObject {
|
||||||
id: u32,
|
id: u32,
|
||||||
@@ -35,7 +34,7 @@ pub struct MessageObject {
|
|||||||
quote: Option<MessageQuote>,
|
quote: Option<MessageQuote>,
|
||||||
parent_id: Option<u32>,
|
parent_id: Option<u32>,
|
||||||
|
|
||||||
text: Option<String>,
|
text: String,
|
||||||
has_location: bool,
|
has_location: bool,
|
||||||
has_html: bool,
|
has_html: bool,
|
||||||
view_type: MessageViewtype,
|
view_type: MessageViewtype,
|
||||||
@@ -86,7 +85,7 @@ pub struct MessageObject {
|
|||||||
reactions: Option<JSONRPCReactions>,
|
reactions: Option<JSONRPCReactions>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(tag = "kind")]
|
#[serde(tag = "kind")]
|
||||||
enum MessageQuote {
|
enum MessageQuote {
|
||||||
JustText {
|
JustText {
|
||||||
@@ -114,8 +113,12 @@ impl MessageObject {
|
|||||||
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 sender_contact = Contact::load_from_db(context, message.get_from_id()).await?;
|
let sender_contact = Contact::get_by_id(context, message.get_from_id())
|
||||||
let sender = ContactObject::try_from_dc_contact(context, sender_contact).await?;
|
.await
|
||||||
|
.context("failed to load sender contact")?;
|
||||||
|
let sender = ContactObject::try_from_dc_contact(context, sender_contact)
|
||||||
|
.await
|
||||||
|
.context("failed to load sender contact object")?;
|
||||||
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();
|
||||||
|
|
||||||
@@ -132,7 +135,9 @@ impl MessageObject {
|
|||||||
let quote = if let Some(quoted_text) = message.quoted_text() {
|
let quote = if let Some(quoted_text) = message.quoted_text() {
|
||||||
match message.quoted_message(context).await? {
|
match message.quoted_message(context).await? {
|
||||||
Some(quote) => {
|
Some(quote) => {
|
||||||
let quote_author = Contact::load_from_db(context, quote.get_from_id()).await?;
|
let quote_author = Contact::get_by_id(context, quote.get_from_id())
|
||||||
|
.await
|
||||||
|
.context("failed to load quote author contact")?;
|
||||||
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(),
|
||||||
@@ -160,7 +165,9 @@ impl MessageObject {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
|
|
||||||
let reactions = get_msg_reactions(context, msg_id).await?;
|
let reactions = get_msg_reactions(context, msg_id)
|
||||||
|
.await
|
||||||
|
.context("failed to load message reactions")?;
|
||||||
let reactions = if reactions.is_empty() {
|
let reactions = if reactions.is_empty() {
|
||||||
None
|
None
|
||||||
} else {
|
} else {
|
||||||
@@ -180,7 +187,7 @@ impl MessageObject {
|
|||||||
state: message
|
state: message
|
||||||
.get_state()
|
.get_state()
|
||||||
.to_u32()
|
.to_u32()
|
||||||
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
|
.context("state conversion to number failed")?,
|
||||||
error: message.error(),
|
error: message.error(),
|
||||||
|
|
||||||
timestamp: message.get_timestamp(),
|
timestamp: message.get_timestamp(),
|
||||||
@@ -203,7 +210,7 @@ impl MessageObject {
|
|||||||
videochat_type: match message.get_videochat_type() {
|
videochat_type: match message.get_videochat_type() {
|
||||||
Some(vct) => Some(
|
Some(vct) => Some(
|
||||||
vct.to_u32()
|
vct.to_u32()
|
||||||
.ok_or_else(|| anyhow!("state conversion to number failed"))?,
|
.context("videochat type conversion to number failed")?,
|
||||||
),
|
),
|
||||||
None => None,
|
None => None,
|
||||||
},
|
},
|
||||||
@@ -230,7 +237,7 @@ impl MessageObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, TypeDef)]
|
#[derive(Serialize, Deserialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "Viewtype")]
|
#[serde(rename = "Viewtype")]
|
||||||
pub enum MessageViewtype {
|
pub enum MessageViewtype {
|
||||||
Unknown,
|
Unknown,
|
||||||
@@ -306,11 +313,12 @@ impl From<MessageViewtype> for Viewtype {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
pub enum DownloadState {
|
pub enum DownloadState {
|
||||||
Done,
|
Done,
|
||||||
Available,
|
Available,
|
||||||
Failure,
|
Failure,
|
||||||
|
Undecipherable,
|
||||||
InProgress,
|
InProgress,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -320,12 +328,13 @@ impl From<download::DownloadState> for DownloadState {
|
|||||||
download::DownloadState::Done => DownloadState::Done,
|
download::DownloadState::Done => DownloadState::Done,
|
||||||
download::DownloadState::Available => DownloadState::Available,
|
download::DownloadState::Available => DownloadState::Available,
|
||||||
download::DownloadState::Failure => DownloadState::Failure,
|
download::DownloadState::Failure => DownloadState::Failure,
|
||||||
|
download::DownloadState::Undecipherable => DownloadState::Undecipherable,
|
||||||
download::DownloadState::InProgress => DownloadState::InProgress,
|
download::DownloadState::InProgress => DownloadState::InProgress,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
pub enum SystemMessageType {
|
pub enum SystemMessageType {
|
||||||
Unknown,
|
Unknown,
|
||||||
GroupNameChanged,
|
GroupNameChanged,
|
||||||
@@ -380,7 +389,7 @@ impl From<deltachat::mimeparser::SystemMessage> for SystemMessageType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct MessageNotificationInfo {
|
pub struct MessageNotificationInfo {
|
||||||
id: u32,
|
id: u32,
|
||||||
@@ -438,14 +447,22 @@ impl MessageNotificationInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct MessageSearchResult {
|
pub struct MessageSearchResult {
|
||||||
id: u32,
|
id: u32,
|
||||||
author_profile_image: Option<String>,
|
author_profile_image: Option<String>,
|
||||||
|
/// if sender name if overridden it will show it as ~alias
|
||||||
author_name: String,
|
author_name: String,
|
||||||
author_color: String,
|
author_color: String,
|
||||||
chat_name: Option<String>,
|
author_id: u32,
|
||||||
|
chat_profile_image: Option<String>,
|
||||||
|
chat_color: String,
|
||||||
|
chat_name: String,
|
||||||
|
chat_type: u32,
|
||||||
|
is_chat_protected: bool,
|
||||||
|
is_chat_contact_request: bool,
|
||||||
|
is_chat_archived: bool,
|
||||||
message: String,
|
message: String,
|
||||||
timestamp: i64,
|
timestamp: i64,
|
||||||
}
|
}
|
||||||
@@ -454,30 +471,44 @@ impl MessageSearchResult {
|
|||||||
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 chat = Chat::load_from_db(context, message.get_chat_id()).await?;
|
let chat = Chat::load_from_db(context, message.get_chat_id()).await?;
|
||||||
let sender = Contact::load_from_db(context, message.get_from_id()).await?;
|
let sender = Contact::get_by_id(context, message.get_from_id()).await?;
|
||||||
|
|
||||||
let profile_image = match sender.get_profile_image(context).await? {
|
let profile_image = match sender.get_profile_image(context).await? {
|
||||||
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
let chat_profile_image = match chat.get_profile_image(context).await? {
|
||||||
|
Some(path_buf) => path_buf.to_str().map(|s| s.to_owned()),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let author_name = if let Some(name) = message.get_override_sender_name() {
|
||||||
|
format!("~{name}")
|
||||||
|
} else {
|
||||||
|
sender.get_display_name().to_owned()
|
||||||
|
};
|
||||||
|
let chat_color = color_int_to_hex_string(chat.get_color(context).await?);
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
id: msg_id.to_u32(),
|
id: msg_id.to_u32(),
|
||||||
author_profile_image: profile_image,
|
author_profile_image: profile_image,
|
||||||
author_name: sender.get_display_name().to_owned(),
|
author_name,
|
||||||
author_color: color_int_to_hex_string(sender.get_color()),
|
author_color: color_int_to_hex_string(sender.get_color()),
|
||||||
chat_name: if chat.get_type() == Chattype::Single {
|
author_id: sender.id.to_u32(),
|
||||||
Some(chat.get_name().to_owned())
|
chat_name: chat.get_name().to_owned(),
|
||||||
} else {
|
chat_color,
|
||||||
None
|
chat_type: chat.get_type().to_u32().context("unknown chat type id")?,
|
||||||
},
|
chat_profile_image,
|
||||||
message: message.get_text().unwrap_or_default(),
|
is_chat_protected: chat.is_protected(),
|
||||||
|
is_chat_contact_request: chat.is_contact_request(),
|
||||||
|
is_chat_archived: chat.get_visibility() == ChatVisibility::Archived,
|
||||||
|
message: message.get_text(),
|
||||||
timestamp: message.get_timestamp(),
|
timestamp: message.get_timestamp(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[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 {
|
||||||
@@ -503,7 +534,7 @@ impl From<ChatItem> for JSONRPCMessageListItem {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, TypeDef)]
|
#[derive(Deserialize, Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct MessageData {
|
pub struct MessageData {
|
||||||
pub text: Option<String>,
|
pub text: Option<String>,
|
||||||
@@ -514,3 +545,10 @@ pub struct MessageData {
|
|||||||
pub override_sender_name: Option<String>,
|
pub override_sender_name: Option<String>,
|
||||||
pub quoted_message_id: Option<u32>,
|
pub quoted_message_id: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(rename_all = "camelCase")]
|
||||||
|
pub struct MessageReadReceipt {
|
||||||
|
pub contact_id: u32,
|
||||||
|
pub timestamp: i64,
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ pub mod account;
|
|||||||
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 http;
|
||||||
pub mod location;
|
pub mod location;
|
||||||
pub mod message;
|
pub mod message;
|
||||||
pub mod provider_info;
|
pub mod provider_info;
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use num_traits::cast::ToPrimitive;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename_all = "camelCase")]
|
#[serde(rename_all = "camelCase")]
|
||||||
pub struct ProviderInfo {
|
pub struct ProviderInfo {
|
||||||
pub before_login_hint: String,
|
pub before_login_hint: String,
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ use deltachat::qr::Qr;
|
|||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "Qr", rename_all = "camelCase")]
|
#[serde(rename = "Qr", rename_all = "camelCase")]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "kind")]
|
||||||
pub enum QrObject {
|
pub enum QrObject {
|
||||||
AskVerifyContact {
|
AskVerifyContact {
|
||||||
contact_id: u32,
|
contact_id: u32,
|
||||||
|
|||||||
@@ -1,23 +1,37 @@
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use deltachat::contact::ContactId;
|
||||||
use deltachat::reaction::Reactions;
|
use deltachat::reaction::Reactions;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use typescript_type_def::TypeDef;
|
use typescript_type_def::TypeDef;
|
||||||
|
|
||||||
|
/// A single reaction emoji.
|
||||||
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
|
#[serde(rename = "Reaction", rename_all = "camelCase")]
|
||||||
|
pub struct JSONRPCReaction {
|
||||||
|
/// Emoji.
|
||||||
|
emoji: String,
|
||||||
|
|
||||||
|
/// Emoji frequency.
|
||||||
|
count: usize,
|
||||||
|
|
||||||
|
/// True if we reacted with this emoji.
|
||||||
|
is_from_self: bool,
|
||||||
|
}
|
||||||
|
|
||||||
/// Structure representing all reactions to a particular message.
|
/// Structure representing all reactions to a particular message.
|
||||||
#[derive(Serialize, TypeDef)]
|
#[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
|
/// Unique reactions and their count, sorted in descending order.
|
||||||
reactions: BTreeMap<String, u32>,
|
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();
|
||||||
let mut unique_reactions: BTreeMap<String, u32> = BTreeMap::new();
|
|
||||||
|
|
||||||
for contact_id in reactions.contacts() {
|
for contact_id in reactions.contacts() {
|
||||||
let reaction = reactions.get(contact_id);
|
let reaction = reactions.get(contact_id);
|
||||||
@@ -30,18 +44,29 @@ impl From<Reactions> for JSONRPCReactions {
|
|||||||
.map(|emoji| emoji.to_owned())
|
.map(|emoji| emoji.to_owned())
|
||||||
.collect();
|
.collect();
|
||||||
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
|
reactions_by_contact.insert(contact_id.to_u32(), emojis.clone());
|
||||||
for emoji in emojis {
|
}
|
||||||
if let Some(x) = unique_reactions.get_mut(&emoji) {
|
|
||||||
*x += 1;
|
let self_reactions = reactions_by_contact.get(&ContactId::SELF.to_u32());
|
||||||
} else {
|
|
||||||
unique_reactions.insert(emoji, 1);
|
let mut reactions_v = Vec::new();
|
||||||
}
|
for (emoji, count) in reactions.emoji_sorted_by_frequency() {
|
||||||
}
|
let is_from_self = if let Some(self_reactions) = self_reactions {
|
||||||
|
self_reactions.contains(&emoji)
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
};
|
||||||
|
|
||||||
|
let reaction = JSONRPCReaction {
|
||||||
|
emoji,
|
||||||
|
count,
|
||||||
|
is_from_self,
|
||||||
|
};
|
||||||
|
reactions_v.push(reaction)
|
||||||
}
|
}
|
||||||
|
|
||||||
JSONRPCReactions {
|
JSONRPCReactions {
|
||||||
reactions_by_contact,
|
reactions_by_contact,
|
||||||
reactions: unique_reactions,
|
reactions: reactions_v,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use typescript_type_def::TypeDef;
|
|||||||
|
|
||||||
use super::maybe_empty_string_to_option;
|
use super::maybe_empty_string_to_option;
|
||||||
|
|
||||||
#[derive(Serialize, TypeDef)]
|
#[derive(Serialize, TypeDef, schemars::JsonSchema)]
|
||||||
#[serde(rename = "WebxdcMessageInfo", rename_all = "camelCase")]
|
#[serde(rename = "WebxdcMessageInfo", rename_all = "camelCase")]
|
||||||
pub struct WebxdcMessageInfo {
|
pub struct WebxdcMessageInfo {
|
||||||
/// The name of the app.
|
/// The name of the app.
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
pub mod api;
|
pub mod api;
|
||||||
pub use api::events;
|
|
||||||
pub use yerpc;
|
pub use yerpc;
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
@@ -14,7 +13,8 @@ mod tests {
|
|||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
|
async fn basic_json_rpc_functionality() -> anyhow::Result<()> {
|
||||||
let tmp_dir = TempDir::new().unwrap().path().into();
|
let tmp_dir = TempDir::new().unwrap().path().into();
|
||||||
let accounts = Accounts::new(tmp_dir).await?;
|
let writable = true;
|
||||||
|
let accounts = Accounts::new(tmp_dir, writable).await?;
|
||||||
let api = CommandApi::new(accounts);
|
let api = CommandApi::new(accounts);
|
||||||
|
|
||||||
let (sender, mut receiver) = unbounded::<String>();
|
let (sender, mut receiver) = unbounded::<String>();
|
||||||
@@ -55,7 +55,8 @@ mod tests {
|
|||||||
#[tokio::test(flavor = "multi_thread")]
|
#[tokio::test(flavor = "multi_thread")]
|
||||||
async fn test_batch_set_config() -> anyhow::Result<()> {
|
async fn test_batch_set_config() -> anyhow::Result<()> {
|
||||||
let tmp_dir = TempDir::new().unwrap().path().into();
|
let tmp_dir = TempDir::new().unwrap().path().into();
|
||||||
let accounts = Accounts::new(tmp_dir).await?;
|
let writable = true;
|
||||||
|
let accounts = Accounts::new(tmp_dir, writable).await?;
|
||||||
let api = CommandApi::new(accounts);
|
let api = CommandApi::new(accounts);
|
||||||
|
|
||||||
let (sender, mut receiver) = unbounded::<String>();
|
let (sender, mut receiver) = unbounded::<String>();
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ use yerpc::axum::handle_ws_rpc;
|
|||||||
use yerpc::{RpcClient, RpcSession};
|
use yerpc::{RpcClient, RpcSession};
|
||||||
|
|
||||||
mod api;
|
mod api;
|
||||||
use api::events::event_to_json_rpc_notification;
|
|
||||||
use api::{Accounts, CommandApi};
|
use api::{Accounts, CommandApi};
|
||||||
|
|
||||||
const DEFAULT_PORT: u16 = 20808;
|
const DEFAULT_PORT: u16 = 20808;
|
||||||
@@ -20,7 +19,8 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
|
.map(|port| port.parse::<u16>().expect("DC_PORT must be a number"))
|
||||||
.unwrap_or(DEFAULT_PORT);
|
.unwrap_or(DEFAULT_PORT);
|
||||||
log::info!("Starting with accounts directory `{path}`.");
|
log::info!("Starting with accounts directory `{path}`.");
|
||||||
let accounts = Accounts::new(PathBuf::from(&path)).await.unwrap();
|
let writable = true;
|
||||||
|
let accounts = Accounts::new(PathBuf::from(&path), writable).await.unwrap();
|
||||||
let state = CommandApi::new(accounts);
|
let state = CommandApi::new(accounts);
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
@@ -44,12 +44,5 @@ async fn main() -> Result<(), std::io::Error> {
|
|||||||
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response {
|
async fn handler(ws: WebSocketUpgrade, Extension(api): Extension<CommandApi>) -> Response {
|
||||||
let (client, out_receiver) = RpcClient::new();
|
let (client, out_receiver) = RpcClient::new();
|
||||||
let session = RpcSession::new(client.clone(), api.clone());
|
let session = RpcSession::new(client.clone(), api.clone());
|
||||||
tokio::spawn(async move {
|
|
||||||
let events = api.accounts.read().await.get_event_emitter();
|
|
||||||
while let Some(event) = events.recv().await {
|
|
||||||
let event = event_to_json_rpc_notification(event);
|
|
||||||
client.send_notification("event", Some(event)).await.ok();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
handle_ws_rpc(ws, out_receiver, session).await
|
handle_ws_rpc(ws, out_receiver, session).await
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ async function run() {
|
|||||||
const accounts = await client.rpc.getAllAccounts();
|
const accounts = await client.rpc.getAllAccounts();
|
||||||
console.log("accounts loaded", accounts);
|
console.log("accounts loaded", accounts);
|
||||||
for (const account of accounts) {
|
for (const account of accounts) {
|
||||||
if (account.type === "Configured") {
|
if (account.kind === "Configured") {
|
||||||
write(
|
write(
|
||||||
$head,
|
$head,
|
||||||
`<a href="#" onclick="selectDeltaAccount(${account.id})">
|
`<a href="#" onclick="selectDeltaAccount(${account.id})">
|
||||||
@@ -57,7 +57,7 @@ async function run() {
|
|||||||
clear($main);
|
clear($main);
|
||||||
const selectedAccount = SELECTED_ACCOUNT;
|
const selectedAccount = SELECTED_ACCOUNT;
|
||||||
const info = await client.rpc.getAccountInfo(selectedAccount);
|
const info = await client.rpc.getAccountInfo(selectedAccount);
|
||||||
if (info.type !== "Configured") {
|
if (info.kind !== "Configured") {
|
||||||
return write($main, "Account is not configured");
|
return write($main, "Account is not configured");
|
||||||
}
|
}
|
||||||
write($main, `<h2>${info.addr!}</h2>`);
|
write($main, `<h2>${info.addr!}</h2>`);
|
||||||
@@ -67,7 +67,7 @@ async function run() {
|
|||||||
null,
|
null,
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
for (const [chatId, _messageId] of chats) {
|
for (const chatId of chats) {
|
||||||
const chat = await client.rpc.getFullChatById(selectedAccount, chatId);
|
const chat = await client.rpc.getFullChatById(selectedAccount, chatId);
|
||||||
write($main, `<h3>${chat.name}</h3>`);
|
write($main, `<h3>${chat.name}</h3>`);
|
||||||
const messageIds = await client.rpc.getMessageIds(
|
const messageIds = await client.rpc.getMessageIds(
|
||||||
@@ -81,8 +81,7 @@ async function run() {
|
|||||||
messageIds
|
messageIds
|
||||||
);
|
);
|
||||||
for (const [_messageId, message] of Object.entries(messages)) {
|
for (const [_messageId, message] of Object.entries(messages)) {
|
||||||
if (message.variant === "message")
|
if (message.kind === "message") write($main, `<p>${message.text}</p>`);
|
||||||
write($main, `<p>${message.text}</p>`);
|
|
||||||
else write($main, `<p>loading error: ${message.error}</p>`);
|
else write($main, `<p>loading error: ${message.error}</p>`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -93,9 +92,9 @@ async function run() {
|
|||||||
$side,
|
$side,
|
||||||
`
|
`
|
||||||
<p class="message">
|
<p class="message">
|
||||||
[<strong>${event.type}</strong> on account ${accountId}]<br>
|
[<strong>${event.kind}</strong> on account ${accountId}]<br>
|
||||||
<em>f1:</em> ${JSON.stringify(
|
<em>f1:</em> ${JSON.stringify(
|
||||||
Object.assign({}, event, { type: undefined })
|
Object.assign({}, event, { kind: undefined })
|
||||||
)}
|
)}
|
||||||
</p>`
|
</p>`
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -55,5 +55,5 @@
|
|||||||
},
|
},
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"types": "dist/deltachat.d.ts",
|
"types": "dist/deltachat.d.ts",
|
||||||
"version": "1.112.6"
|
"version": "1.126.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,34 +1,28 @@
|
|||||||
import * as T from "../generated/types.js";
|
import * as T 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 { Event } from "../generated/events.js";
|
|
||||||
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
|
import { WebsocketTransport, BaseTransport, Request } from "yerpc";
|
||||||
import { TinyEmitter } from "@deltachat/tiny-emitter";
|
import { TinyEmitter } from "@deltachat/tiny-emitter";
|
||||||
|
|
||||||
type DCWireEvent<T extends Event> = {
|
type Events = { ALL: (accountId: number, event: EventType) => void } & {
|
||||||
event: T;
|
[Property in EventType["kind"]]: (
|
||||||
contextId: number;
|
|
||||||
};
|
|
||||||
// export type Events = Record<
|
|
||||||
// Event["type"] | "ALL",
|
|
||||||
// (event: DeltaChatEvent<Event>) => void
|
|
||||||
// >;
|
|
||||||
|
|
||||||
type Events = { ALL: (accountId: number, event: Event) => void } & {
|
|
||||||
[Property in Event["type"]]: (
|
|
||||||
accountId: number,
|
accountId: number,
|
||||||
event: Extract<Event, { type: Property }>
|
event: Extract<EventType, { kind: Property }>
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ContextEvents = { ALL: (event: Event) => void } & {
|
type ContextEvents = { ALL: (event: EventType) => void } & {
|
||||||
[Property in Event["type"]]: (
|
[Property in EventType["kind"]]: (
|
||||||
event: Extract<Event, { type: Property }>
|
event: Extract<EventType, { kind: Property }>
|
||||||
) => void;
|
) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DcEvent = Event;
|
export type DcEvent = EventType;
|
||||||
export type DcEventType<T extends Event["type"]> = Extract<Event, { type: T }>;
|
export type DcEventType<T extends EventType["kind"]> = Extract<
|
||||||
|
EventType,
|
||||||
|
{ kind: T }
|
||||||
|
>;
|
||||||
|
|
||||||
export class BaseDeltaChat<
|
export class BaseDeltaChat<
|
||||||
Transport extends BaseTransport<any>
|
Transport extends BaseTransport<any>
|
||||||
@@ -36,27 +30,34 @@ export class BaseDeltaChat<
|
|||||||
rpc: RawClient;
|
rpc: RawClient;
|
||||||
account?: T.Account;
|
account?: T.Account;
|
||||||
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
|
private contextEmitters: { [key: number]: TinyEmitter<ContextEvents> } = {};
|
||||||
constructor(public transport: Transport) {
|
|
||||||
|
//@ts-ignore
|
||||||
|
private eventTask: Promise<void>;
|
||||||
|
|
||||||
|
constructor(public transport: Transport, startEventLoop: boolean) {
|
||||||
super();
|
super();
|
||||||
this.rpc = new RawClient(this.transport);
|
this.rpc = new RawClient(this.transport);
|
||||||
this.transport.on("request", (request: Request) => {
|
if (startEventLoop) {
|
||||||
const method = request.method;
|
this.eventTask = this.eventLoop();
|
||||||
if (method === "event") {
|
}
|
||||||
const event = request.params! as DCWireEvent<Event>;
|
}
|
||||||
//@ts-ignore
|
|
||||||
this.emit(event.event.type, event.contextId, event.event as any);
|
|
||||||
this.emit("ALL", event.contextId, event.event as any);
|
|
||||||
|
|
||||||
if (this.contextEmitters[event.contextId]) {
|
async eventLoop(): Promise<void> {
|
||||||
this.contextEmitters[event.contextId].emit(
|
while (true) {
|
||||||
event.event.type,
|
const event = await this.rpc.getNextEvent();
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
event.event as any
|
this.emit(event.event.kind, event.contextId, event.event);
|
||||||
);
|
this.emit("ALL", event.contextId, event.event);
|
||||||
this.contextEmitters[event.contextId].emit("ALL", event.event);
|
|
||||||
}
|
if (this.contextEmitters[event.contextId]) {
|
||||||
|
this.contextEmitters[event.contextId].emit(
|
||||||
|
event.event.kind,
|
||||||
|
//@ts-ignore
|
||||||
|
event.event as any
|
||||||
|
);
|
||||||
|
this.contextEmitters[event.contextId].emit("ALL", event.event as any);
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async listAccounts(): Promise<T.Account[]> {
|
async listAccounts(): Promise<T.Account[]> {
|
||||||
@@ -75,10 +76,12 @@ export class BaseDeltaChat<
|
|||||||
|
|
||||||
export type Opts = {
|
export type Opts = {
|
||||||
url: string;
|
url: string;
|
||||||
|
startEventLoop: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const DEFAULT_OPTS: Opts = {
|
export const DEFAULT_OPTS: Opts = {
|
||||||
url: "ws://localhost:20808/ws",
|
url: "ws://localhost:20808/ws",
|
||||||
|
startEventLoop: true,
|
||||||
};
|
};
|
||||||
export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
|
export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
|
||||||
opts: Opts;
|
opts: Opts;
|
||||||
@@ -86,20 +89,24 @@ export class DeltaChat extends BaseDeltaChat<WebsocketTransport> {
|
|||||||
this.transport.close();
|
this.transport.close();
|
||||||
}
|
}
|
||||||
constructor(opts?: Opts | string) {
|
constructor(opts?: Opts | string) {
|
||||||
if (typeof opts === "string") opts = { url: opts };
|
if (typeof opts === "string") {
|
||||||
if (opts) opts = { ...DEFAULT_OPTS, ...opts };
|
opts = { ...DEFAULT_OPTS, url: opts };
|
||||||
else opts = { ...DEFAULT_OPTS };
|
} else if (opts) {
|
||||||
|
opts = { ...DEFAULT_OPTS, ...opts };
|
||||||
|
} else {
|
||||||
|
opts = { ...DEFAULT_OPTS };
|
||||||
|
}
|
||||||
const transport = new WebsocketTransport(opts.url);
|
const transport = new WebsocketTransport(opts.url);
|
||||||
super(transport);
|
super(transport, opts.startEventLoop);
|
||||||
this.opts = opts;
|
this.opts = opts;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
|
export class StdioDeltaChat extends BaseDeltaChat<StdioTransport> {
|
||||||
close() {}
|
close() {}
|
||||||
constructor(input: any, output: any) {
|
constructor(input: any, output: any, startEventLoop: boolean) {
|
||||||
const transport = new StdioTransport(input, output);
|
const transport = new StdioTransport(input, output);
|
||||||
super(transport);
|
super(transport, startEventLoop);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
export * as RPC from "../generated/jsonrpc.js";
|
export * as RPC from "../generated/jsonrpc.js";
|
||||||
export * as T from "../generated/types.js";
|
export * as T from "../generated/types.js";
|
||||||
export * from "../generated/events.js";
|
|
||||||
export { RawClient } from "../generated/client.js";
|
export { RawClient } from "../generated/client.js";
|
||||||
export * from "./client.js";
|
export * from "./client.js";
|
||||||
export * as yerpc from "yerpc";
|
export * as yerpc from "yerpc";
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ describe("basic tests", () => {
|
|||||||
|
|
||||||
before(async () => {
|
before(async () => {
|
||||||
serverHandle = await startServer();
|
serverHandle = await startServer();
|
||||||
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
|
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
|
||||||
// dc.on("ALL", (event) => {
|
// dc.on("ALL", (event) => {
|
||||||
//console.log("event", event);
|
//console.log("event", event);
|
||||||
// });
|
// });
|
||||||
|
|||||||
@@ -27,10 +27,10 @@ describe("online tests", function () {
|
|||||||
this.skip();
|
this.skip();
|
||||||
}
|
}
|
||||||
serverHandle = await startServer();
|
serverHandle = await startServer();
|
||||||
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout);
|
dc = new DeltaChat(serverHandle.stdin, serverHandle.stdout, true);
|
||||||
|
|
||||||
dc.on("ALL", (contextId, { type }) => {
|
dc.on("ALL", (contextId, { kind }) => {
|
||||||
if (type !== "Info") console.log(contextId, type);
|
if (kind !== "Info") console.log(contextId, kind);
|
||||||
});
|
});
|
||||||
|
|
||||||
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
|
account1 = await createTempUser(process.env.DCC_NEW_TMP_EMAIL);
|
||||||
@@ -148,7 +148,7 @@ describe("online tests", function () {
|
|||||||
waitForEvent(dc, "IncomingMsg", accountId1),
|
waitForEvent(dc, "IncomingMsg", accountId1),
|
||||||
]);
|
]);
|
||||||
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
|
dc.rpc.miscSendTextMessage(accountId2, chatId, "super secret message");
|
||||||
// Check if answer arives at A and if it is encrypted
|
// Check if answer arrives at A and if it is encrypted
|
||||||
await eventPromise2;
|
await eventPromise2;
|
||||||
|
|
||||||
const messageId = (
|
const messageId = (
|
||||||
@@ -177,12 +177,12 @@ describe("online tests", function () {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function waitForEvent<T extends DcEvent["type"]>(
|
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, { type: 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")),
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-repl"
|
name = "deltachat-repl"
|
||||||
version = "1.112.6"
|
version = "1.126.1"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
@@ -9,10 +9,10 @@ ansi_term = "0.12.1"
|
|||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
deltachat = { path = "..", features = ["internals"]}
|
deltachat = { path = "..", features = ["internals"]}
|
||||||
dirs = "5"
|
dirs = "5"
|
||||||
log = "0.4.16"
|
log = "0.4.20"
|
||||||
pretty_env_logger = "0.4"
|
pretty_env_logger = "0.5"
|
||||||
rusqlite = "0.29"
|
rusqlite = "0.29"
|
||||||
rustyline = "11"
|
rustyline = "12"
|
||||||
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
tokio = { version = "1", features = ["fs", "rt-multi-thread", "macros"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ use deltachat::imex::*;
|
|||||||
use deltachat::location;
|
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::peerstate::*;
|
use deltachat::peerstate::*;
|
||||||
use deltachat::qr::*;
|
use deltachat::qr::*;
|
||||||
use deltachat::reaction::send_reaction;
|
use deltachat::reaction::send_reaction;
|
||||||
@@ -138,11 +139,7 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
|||||||
/* import a directory */
|
/* import a directory */
|
||||||
let dir_name = std::path::Path::new(&real_spec);
|
let dir_name = std::path::Path::new(&real_spec);
|
||||||
let dir = fs::read_dir(dir_name).await;
|
let dir = fs::read_dir(dir_name).await;
|
||||||
if dir.is_err() {
|
if let Ok(mut dir) = dir {
|
||||||
error!(context, "Import: Cannot open directory \"{}\".", &real_spec,);
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
let mut dir = dir.unwrap();
|
|
||||||
while let Ok(Some(entry)) = dir.next_entry().await {
|
while let Ok(Some(entry)) = dir.next_entry().await {
|
||||||
let name_f = entry.file_name();
|
let name_f = entry.file_name();
|
||||||
let name = name_f.to_string_lossy();
|
let name = name_f.to_string_lossy();
|
||||||
@@ -154,6 +151,9 @@ async fn poke_spec(context: &Context, spec: Option<&str>) -> bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
error!(context, "Import: Cannot open directory \"{}\".", &real_spec);
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
|
println!("Import: {} items read from \"{}\".", read_cnt, &real_spec);
|
||||||
@@ -187,6 +187,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
|||||||
DownloadState::Available => " [⬇ Download available]",
|
DownloadState::Available => " [⬇ Download available]",
|
||||||
DownloadState::InProgress => " [⬇ Download in progress...]️",
|
DownloadState::InProgress => " [⬇ Download in progress...]️",
|
||||||
DownloadState::Failure => " [⬇ Download failed]",
|
DownloadState::Failure => " [⬇ Download failed]",
|
||||||
|
DownloadState::Undecipherable => " [⬇ Decryption failed]",
|
||||||
};
|
};
|
||||||
|
|
||||||
let temp2 = timestamp_to_str(msg.get_timestamp());
|
let temp2 = timestamp_to_str(msg.get_timestamp());
|
||||||
@@ -199,7 +200,7 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
|||||||
if msg.has_location() { "📍" } else { "" },
|
if msg.has_location() { "📍" } else { "" },
|
||||||
&contact_name,
|
&contact_name,
|
||||||
contact_id,
|
contact_id,
|
||||||
msgtext.unwrap_or_default(),
|
msgtext,
|
||||||
if msg.has_html() { "[HAS-HTML]️" } else { "" },
|
if msg.has_html() { "[HAS-HTML]️" } else { "" },
|
||||||
if msg.get_from_id() == ContactId::SELF {
|
if msg.get_from_id() == ContactId::SELF {
|
||||||
""
|
""
|
||||||
@@ -210,7 +211,17 @@ async fn log_msg(context: &Context, prefix: impl AsRef<str>, msg: &Message) {
|
|||||||
} else {
|
} else {
|
||||||
"[FRESH]"
|
"[FRESH]"
|
||||||
},
|
},
|
||||||
if msg.is_info() { "[INFO]" } else { "" },
|
if msg.is_info() {
|
||||||
|
if msg.get_info_type() == SystemMessage::ChatProtectionEnabled {
|
||||||
|
"[INFO 🛡️]"
|
||||||
|
} else if msg.get_info_type() == SystemMessage::ChatProtectionDisabled {
|
||||||
|
"[INFO 🛡️❌]"
|
||||||
|
} else {
|
||||||
|
"[INFO]"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
},
|
||||||
if msg.get_viewtype() == Viewtype::VideochatInvitation {
|
if msg.get_viewtype() == Viewtype::VideochatInvitation {
|
||||||
format!(
|
format!(
|
||||||
"[VIDEOCHAT-INVITATION: {}, type={}]",
|
"[VIDEOCHAT-INVITATION: {}, type={}]",
|
||||||
@@ -395,8 +406,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
unpin <chat-id>\n\
|
unpin <chat-id>\n\
|
||||||
mute <chat-id> [<seconds>]\n\
|
mute <chat-id> [<seconds>]\n\
|
||||||
unmute <chat-id>\n\
|
unmute <chat-id>\n\
|
||||||
protect <chat-id>\n\
|
|
||||||
unprotect <chat-id>\n\
|
|
||||||
delchat <chat-id>\n\
|
delchat <chat-id>\n\
|
||||||
accept <chat-id>\n\
|
accept <chat-id>\n\
|
||||||
decline <chat-id>\n\
|
decline <chat-id>\n\
|
||||||
@@ -805,15 +814,30 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
}
|
}
|
||||||
"chatinfo" => {
|
"chatinfo" => {
|
||||||
ensure!(sel_chat.is_some(), "No chat selected.");
|
ensure!(sel_chat.is_some(), "No chat selected.");
|
||||||
|
let sel_chat_id = sel_chat.as_ref().unwrap().get_id();
|
||||||
|
|
||||||
let contacts =
|
let contacts = chat::get_chat_contacts(&context, sel_chat_id).await?;
|
||||||
chat::get_chat_contacts(&context, sel_chat.as_ref().unwrap().get_id()).await?;
|
|
||||||
println!("Memberlist:");
|
println!("Memberlist:");
|
||||||
|
|
||||||
log_contactlist(&context, &contacts).await?;
|
log_contactlist(&context, &contacts).await?;
|
||||||
|
println!("{} contacts", contacts.len());
|
||||||
|
|
||||||
|
let similar_chats = sel_chat_id.get_similar_chat_ids(&context).await?;
|
||||||
|
if !similar_chats.is_empty() {
|
||||||
|
println!("Similar chats: ");
|
||||||
|
for (similar_chat_id, metric) in similar_chats {
|
||||||
|
let similar_chat = Chat::load_from_db(&context, similar_chat_id).await?;
|
||||||
|
println!(
|
||||||
|
"{} (#{}) {:.1}",
|
||||||
|
similar_chat.name,
|
||||||
|
similar_chat_id,
|
||||||
|
100.0 * metric
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
"{} contacts\nLocation streaming: {}",
|
"Location streaming: {}",
|
||||||
contacts.len(),
|
|
||||||
location::is_sending_locations_to_chat(
|
location::is_sending_locations_to_chat(
|
||||||
&context,
|
&context,
|
||||||
Some(sel_chat.as_ref().unwrap().get_id())
|
Some(sel_chat.as_ref().unwrap().get_id())
|
||||||
@@ -878,7 +902,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
let latitude = arg1.parse()?;
|
let latitude = arg1.parse()?;
|
||||||
let longitude = arg2.parse()?;
|
let longitude = arg2.parse()?;
|
||||||
|
|
||||||
let continue_streaming = location::set(&context, latitude, longitude, 0.).await;
|
let continue_streaming = location::set(&context, latitude, longitude, 0.).await?;
|
||||||
if continue_streaming {
|
if continue_streaming {
|
||||||
println!("Success, streaming should be continued.");
|
println!("Success, streaming should be continued.");
|
||||||
} else {
|
} else {
|
||||||
@@ -912,9 +936,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
Viewtype::File
|
Viewtype::File
|
||||||
});
|
});
|
||||||
msg.set_file(arg1, None);
|
msg.set_file(arg1, None);
|
||||||
if !arg2.is_empty() {
|
msg.set_text(arg2.to_string());
|
||||||
msg.set_text(Some(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?;
|
||||||
}
|
}
|
||||||
"sendhtml" => {
|
"sendhtml" => {
|
||||||
@@ -926,11 +948,11 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
|
|
||||||
let mut msg = Message::new(Viewtype::Text);
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
msg.set_html(Some(html.to_string()));
|
msg.set_html(Some(html.to_string()));
|
||||||
msg.set_text(Some(if arg2.is_empty() {
|
msg.set_text(if arg2.is_empty() {
|
||||||
path.file_name().unwrap().to_string_lossy().to_string()
|
path.file_name().unwrap().to_string_lossy().to_string()
|
||||||
} else {
|
} else {
|
||||||
arg2.to_string()
|
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?;
|
||||||
}
|
}
|
||||||
"sendsyncmsg" => match context.send_sync_msg().await? {
|
"sendsyncmsg" => match context.send_sync_msg().await? {
|
||||||
@@ -979,7 +1001,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
|
|
||||||
if !arg1.is_empty() {
|
if !arg1.is_empty() {
|
||||||
let mut draft = Message::new(Viewtype::Text);
|
let mut draft = Message::new(Viewtype::Text);
|
||||||
draft.set_text(Some(arg1.to_string()));
|
draft.set_text(arg1.to_string());
|
||||||
sel_chat
|
sel_chat
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.unwrap()
|
.unwrap()
|
||||||
@@ -1003,7 +1025,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
"Please specify text to add as device message."
|
"Please specify text to add as device message."
|
||||||
);
|
);
|
||||||
let mut msg = Message::new(Viewtype::Text);
|
let mut msg = Message::new(Viewtype::Text);
|
||||||
msg.set_text(Some(arg1.to_string()));
|
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" => {
|
||||||
@@ -1058,20 +1080,6 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
};
|
};
|
||||||
chat::set_muted(&context, chat_id, duration).await?;
|
chat::set_muted(&context, chat_id, duration).await?;
|
||||||
}
|
}
|
||||||
"protect" | "unprotect" => {
|
|
||||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
|
||||||
let chat_id = ChatId::new(arg1.parse()?);
|
|
||||||
chat_id
|
|
||||||
.set_protection(
|
|
||||||
&context,
|
|
||||||
match arg0 {
|
|
||||||
"protect" => ProtectionStatus::Protected,
|
|
||||||
"unprotect" => ProtectionStatus::Unprotected,
|
|
||||||
_ => unreachable!("arg0={:?}", arg0),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
"delchat" => {
|
"delchat" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
ensure!(!arg1.is_empty(), "Argument <chat-id> missing.");
|
||||||
let chat_id = ChatId::new(arg1.parse()?);
|
let chat_id = ChatId::new(arg1.parse()?);
|
||||||
@@ -1090,7 +1098,7 @@ pub async fn cmdline(context: Context, line: &str, chat_id: &mut ChatId) -> Resu
|
|||||||
"msginfo" => {
|
"msginfo" => {
|
||||||
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
ensure!(!arg1.is_empty(), "Argument <msg-id> missing.");
|
||||||
let id = MsgId::new(arg1.parse()?);
|
let id = MsgId::new(arg1.parse()?);
|
||||||
let res = message::get_msg_info(&context, id).await?;
|
let res = id.get_info(&context).await?;
|
||||||
println!("{res}");
|
println!("{res}");
|
||||||
}
|
}
|
||||||
"download" => {
|
"download" => {
|
||||||
|
|||||||
373
deltachat-rpc-client/LICENSE
Normal file
373
deltachat-rpc-client/LICENSE
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
||||||
@@ -5,9 +5,23 @@ and provides asynchronous interface to it.
|
|||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`.
|
To use Delta Chat RPC client, first build a `deltachat-rpc-server` with `cargo build -p deltachat-rpc-server`
|
||||||
|
or download a prebuilt release.
|
||||||
Install it anywhere in your `PATH`.
|
Install it anywhere in your `PATH`.
|
||||||
|
|
||||||
|
[Create a virtual environment](https://docs.python.org/3/library/venv.html)
|
||||||
|
if you don't have one already and activate it.
|
||||||
|
```
|
||||||
|
$ python -m venv env
|
||||||
|
$ . env/bin/activate
|
||||||
|
```
|
||||||
|
|
||||||
|
Install `deltachat-rpc-client` from source:
|
||||||
|
```
|
||||||
|
$ cd deltachat-rpc-client
|
||||||
|
$ 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`.
|
||||||
@@ -23,19 +37,14 @@ $ tox --devenv env
|
|||||||
$ . env/bin/activate
|
$ . env/bin/activate
|
||||||
```
|
```
|
||||||
|
|
||||||
It is recommended to use IPython, because it supports using `await` directly
|
|
||||||
from the REPL.
|
|
||||||
|
|
||||||
```
|
```
|
||||||
$ pip install ipython
|
$ python
|
||||||
$ PATH="../target/debug:$PATH" ipython
|
>>> from deltachat_rpc_client import *
|
||||||
...
|
>>> rpc = Rpc()
|
||||||
In [1]: from deltachat_rpc_client import *
|
>>> rpc.start()
|
||||||
In [2]: rpc = Rpc()
|
>>> dc = DeltaChat(rpc)
|
||||||
In [3]: await rpc.start()
|
>>> system_info = dc.get_system_info()
|
||||||
In [4]: dc = DeltaChat(rpc)
|
>>> system_info["level"]
|
||||||
In [5]: system_info = await dc.get_system_info()
|
'awesome'
|
||||||
In [6]: system_info["level"]
|
>>> rpc.close()
|
||||||
Out[6]: 'awesome'
|
|
||||||
In [7]: await rpc.close()
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -4,23 +4,21 @@
|
|||||||
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.
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
|
|
||||||
from deltachat_rpc_client import events, run_bot_cli
|
from deltachat_rpc_client import events, run_bot_cli
|
||||||
|
|
||||||
hooks = events.HookCollection()
|
hooks = events.HookCollection()
|
||||||
|
|
||||||
|
|
||||||
@hooks.on(events.RawEvent)
|
@hooks.on(events.RawEvent)
|
||||||
async def log_event(event):
|
def log_event(event):
|
||||||
print(event)
|
print(event)
|
||||||
|
|
||||||
|
|
||||||
@hooks.on(events.NewMessage)
|
@hooks.on(events.NewMessage)
|
||||||
async def echo(event):
|
def echo(event):
|
||||||
snapshot = event.message_snapshot
|
snapshot = event.message_snapshot
|
||||||
await snapshot.chat.send_text(snapshot.text)
|
snapshot.chat.send_text(snapshot.text)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
asyncio.run(run_bot_cli(hooks))
|
run_bot_cli(hooks)
|
||||||
|
|||||||
@@ -3,9 +3,9 @@
|
|||||||
|
|
||||||
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 asyncio
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
from threading import Thread
|
||||||
|
|
||||||
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
|
from deltachat_rpc_client import Bot, DeltaChat, EventType, Rpc, events
|
||||||
|
|
||||||
@@ -13,62 +13,62 @@ hooks = events.HookCollection()
|
|||||||
|
|
||||||
|
|
||||||
@hooks.on(events.RawEvent)
|
@hooks.on(events.RawEvent)
|
||||||
async def log_event(event):
|
def log_event(event):
|
||||||
if event.type == EventType.INFO:
|
if event.kind == EventType.INFO:
|
||||||
logging.info(event.msg)
|
logging.info(event.msg)
|
||||||
elif event.type == EventType.WARNING:
|
elif event.kind == EventType.WARNING:
|
||||||
logging.warning(event.msg)
|
logging.warning(event.msg)
|
||||||
|
|
||||||
|
|
||||||
@hooks.on(events.RawEvent(EventType.ERROR))
|
@hooks.on(events.RawEvent(EventType.ERROR))
|
||||||
async def log_error(event):
|
def log_error(event):
|
||||||
logging.error(event.msg)
|
logging.error(event.msg)
|
||||||
|
|
||||||
|
|
||||||
@hooks.on(events.MemberListChanged)
|
@hooks.on(events.MemberListChanged)
|
||||||
async def on_memberlist_changed(event):
|
def on_memberlist_changed(event):
|
||||||
logging.info("member %s was %s", event.member, "added" if event.member_added else "removed")
|
logging.info("member %s was %s", event.member, "added" if event.member_added else "removed")
|
||||||
|
|
||||||
|
|
||||||
@hooks.on(events.GroupImageChanged)
|
@hooks.on(events.GroupImageChanged)
|
||||||
async def on_group_image_changed(event):
|
def on_group_image_changed(event):
|
||||||
logging.info("group image %s", "deleted" if event.image_deleted else "changed")
|
logging.info("group image %s", "deleted" if event.image_deleted else "changed")
|
||||||
|
|
||||||
|
|
||||||
@hooks.on(events.GroupNameChanged)
|
@hooks.on(events.GroupNameChanged)
|
||||||
async def on_group_name_changed(event):
|
def on_group_name_changed(event):
|
||||||
logging.info("group name changed, old name: %s", event.old_name)
|
logging.info("group name changed, old name: %s", event.old_name)
|
||||||
|
|
||||||
|
|
||||||
@hooks.on(events.NewMessage(func=lambda e: not e.command))
|
@hooks.on(events.NewMessage(func=lambda e: not e.command))
|
||||||
async def echo(event):
|
def echo(event):
|
||||||
snapshot = event.message_snapshot
|
snapshot = event.message_snapshot
|
||||||
if snapshot.text or snapshot.file:
|
if snapshot.text or snapshot.file:
|
||||||
await snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
|
snapshot.chat.send_message(text=snapshot.text, file=snapshot.file)
|
||||||
|
|
||||||
|
|
||||||
@hooks.on(events.NewMessage(command="/help"))
|
@hooks.on(events.NewMessage(command="/help"))
|
||||||
async def help_command(event):
|
def help_command(event):
|
||||||
snapshot = event.message_snapshot
|
snapshot = event.message_snapshot
|
||||||
await snapshot.chat.send_text("Send me any message and I will echo it back")
|
snapshot.chat.send_text("Send me any message and I will echo it back")
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
def main():
|
||||||
async with Rpc() as rpc:
|
with Rpc() as rpc:
|
||||||
deltachat = DeltaChat(rpc)
|
deltachat = DeltaChat(rpc)
|
||||||
system_info = await deltachat.get_system_info()
|
system_info = deltachat.get_system_info()
|
||||||
logging.info("Running deltachat core %s", system_info.deltachat_core_version)
|
logging.info("Running deltachat core %s", system_info.deltachat_core_version)
|
||||||
|
|
||||||
accounts = await deltachat.get_all_accounts()
|
accounts = deltachat.get_all_accounts()
|
||||||
account = accounts[0] if accounts else await deltachat.add_account()
|
account = accounts[0] if accounts else deltachat.add_account()
|
||||||
|
|
||||||
bot = Bot(account, hooks)
|
bot = Bot(account, hooks)
|
||||||
if not await bot.is_configured():
|
if not bot.is_configured():
|
||||||
# Save a reference to avoid garbage collection of the task.
|
configure_thread = Thread(run=bot.configure, kwargs={"email": sys.argv[1], "password": sys.argv[2]})
|
||||||
_configure_task = asyncio.create_task(bot.configure(email=sys.argv[1], password=sys.argv[2]))
|
configure_thread.start()
|
||||||
await bot.run_forever()
|
bot.run_forever()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
asyncio.run(main())
|
main()
|
||||||
|
|||||||
@@ -2,45 +2,44 @@
|
|||||||
"""
|
"""
|
||||||
Example echo bot without using hooks
|
Example echo bot without using hooks
|
||||||
"""
|
"""
|
||||||
import asyncio
|
|
||||||
import logging
|
import logging
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId
|
from deltachat_rpc_client import DeltaChat, EventType, Rpc, SpecialContactId
|
||||||
|
|
||||||
|
|
||||||
async def main():
|
def main():
|
||||||
async with Rpc() as rpc:
|
with Rpc() as rpc:
|
||||||
deltachat = DeltaChat(rpc)
|
deltachat = DeltaChat(rpc)
|
||||||
system_info = await deltachat.get_system_info()
|
system_info = deltachat.get_system_info()
|
||||||
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
|
logging.info("Running deltachat core %s", system_info["deltachat_core_version"])
|
||||||
|
|
||||||
accounts = await deltachat.get_all_accounts()
|
accounts = deltachat.get_all_accounts()
|
||||||
account = accounts[0] if accounts else await deltachat.add_account()
|
account = accounts[0] if accounts else deltachat.add_account()
|
||||||
|
|
||||||
await account.set_config("bot", "1")
|
account.set_config("bot", "1")
|
||||||
if not await account.is_configured():
|
if not account.is_configured():
|
||||||
logging.info("Account is not configured, configuring")
|
logging.info("Account is not configured, configuring")
|
||||||
await account.set_config("addr", sys.argv[1])
|
account.set_config("addr", sys.argv[1])
|
||||||
await account.set_config("mail_pw", sys.argv[2])
|
account.set_config("mail_pw", sys.argv[2])
|
||||||
await account.configure()
|
account.configure()
|
||||||
logging.info("Configured")
|
logging.info("Configured")
|
||||||
else:
|
else:
|
||||||
logging.info("Account is already configured")
|
logging.info("Account is already configured")
|
||||||
await deltachat.start_io()
|
deltachat.start_io()
|
||||||
|
|
||||||
async def process_messages():
|
def process_messages():
|
||||||
for message in await account.get_next_messages():
|
for message in account.get_next_messages():
|
||||||
snapshot = await message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
|
if snapshot.from_id != SpecialContactId.SELF and not snapshot.is_bot and not snapshot.is_info:
|
||||||
await snapshot.chat.send_text(snapshot.text)
|
snapshot.chat.send_text(snapshot.text)
|
||||||
await snapshot.message.mark_seen()
|
snapshot.message.mark_seen()
|
||||||
|
|
||||||
# Process old messages.
|
# Process old messages.
|
||||||
await process_messages()
|
process_messages()
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
event = await account.wait_for_event()
|
event = account.wait_for_event()
|
||||||
if event["type"] == EventType.INFO:
|
if event["type"] == EventType.INFO:
|
||||||
logging.info("%s", event["msg"])
|
logging.info("%s", event["msg"])
|
||||||
elif event["type"] == EventType.WARNING:
|
elif event["type"] == EventType.WARNING:
|
||||||
@@ -49,9 +48,9 @@ async def main():
|
|||||||
logging.error("%s", event["msg"])
|
logging.error("%s", event["msg"])
|
||||||
elif event["type"] == EventType.INCOMING_MSG:
|
elif event["type"] == EventType.INCOMING_MSG:
|
||||||
logging.info("Got an incoming message")
|
logging.info("Got an incoming message")
|
||||||
await process_messages()
|
process_messages()
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
asyncio.run(main())
|
main()
|
||||||
|
|||||||
@@ -5,9 +5,19 @@ build-backend = "setuptools.build_meta"
|
|||||||
[project]
|
[project]
|
||||||
name = "deltachat-rpc-client"
|
name = "deltachat-rpc-client"
|
||||||
description = "Python client for Delta Chat core JSON-RPC interface"
|
description = "Python client for Delta Chat core JSON-RPC interface"
|
||||||
dependencies = [
|
classifiers = [
|
||||||
"aiohttp",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"aiodns"
|
"Intended Audience :: Developers",
|
||||||
|
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||||
|
"Operating System :: POSIX :: Linux",
|
||||||
|
"Operating System :: MacOS :: MacOS X",
|
||||||
|
"Programming Language :: Python :: 3",
|
||||||
|
"Programming Language :: Python :: 3.8",
|
||||||
|
"Programming Language :: Python :: 3.9",
|
||||||
|
"Programming Language :: Python :: 3.10",
|
||||||
|
"Programming Language :: Python :: 3.11",
|
||||||
|
"Topic :: Communications :: Chat",
|
||||||
|
"Topic :: Communications :: Email"
|
||||||
]
|
]
|
||||||
dynamic = [
|
dynamic = [
|
||||||
"version"
|
"version"
|
||||||
@@ -61,3 +71,6 @@ line-length = 120
|
|||||||
|
|
||||||
[tool.isort]
|
[tool.isort]
|
||||||
profile = "black"
|
profile = "black"
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
log_cli = true
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"""Delta Chat asynchronous 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
|
||||||
from .chat import Chat
|
from .chat import Chat
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
|
from threading import Thread
|
||||||
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union
|
from typing import TYPE_CHECKING, Callable, Iterable, Optional, Tuple, Type, Union
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
@@ -43,7 +43,7 @@ class AttrDict(dict):
|
|||||||
super().__setattr__(attr, val)
|
super().__setattr__(attr, val)
|
||||||
|
|
||||||
|
|
||||||
async def run_client_cli(
|
def run_client_cli(
|
||||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||||
argv: Optional[list] = None,
|
argv: Optional[list] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@@ -54,10 +54,10 @@ async def run_client_cli(
|
|||||||
"""
|
"""
|
||||||
from .client import Client
|
from .client import Client
|
||||||
|
|
||||||
await _run_cli(Client, hooks, argv, **kwargs)
|
_run_cli(Client, hooks, argv, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
async def run_bot_cli(
|
def run_bot_cli(
|
||||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||||
argv: Optional[list] = None,
|
argv: Optional[list] = None,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
@@ -68,10 +68,10 @@ async def run_bot_cli(
|
|||||||
"""
|
"""
|
||||||
from .client import Bot
|
from .client import Bot
|
||||||
|
|
||||||
await _run_cli(Bot, hooks, argv, **kwargs)
|
_run_cli(Bot, hooks, argv, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
async def _run_cli(
|
def _run_cli(
|
||||||
client_type: Type["Client"],
|
client_type: Type["Client"],
|
||||||
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
hooks: Optional[Iterable[Tuple[Callable, Union[type, "EventFilter"]]]] = None,
|
||||||
argv: Optional[list] = None,
|
argv: Optional[list] = None,
|
||||||
@@ -93,20 +93,20 @@ async def _run_cli(
|
|||||||
parser.add_argument("--password", action="store", help="password")
|
parser.add_argument("--password", action="store", help="password")
|
||||||
args = parser.parse_args(argv[1:])
|
args = parser.parse_args(argv[1:])
|
||||||
|
|
||||||
async with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
|
with Rpc(accounts_dir=args.accounts_dir, **kwargs) as rpc:
|
||||||
deltachat = DeltaChat(rpc)
|
deltachat = DeltaChat(rpc)
|
||||||
core_version = (await deltachat.get_system_info()).deltachat_core_version
|
core_version = (deltachat.get_system_info()).deltachat_core_version
|
||||||
accounts = await deltachat.get_all_accounts()
|
accounts = deltachat.get_all_accounts()
|
||||||
account = accounts[0] if accounts else await deltachat.add_account()
|
account = accounts[0] if accounts else deltachat.add_account()
|
||||||
|
|
||||||
client = client_type(account, hooks)
|
client = client_type(account, hooks)
|
||||||
client.logger.debug("Running deltachat core %s", core_version)
|
client.logger.debug("Running deltachat core %s", core_version)
|
||||||
if not await 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"
|
||||||
# Save a reference to avoid garbage collection of the task.
|
configure_thread = Thread(run=client.configure, kwargs={"email": args.email, "password": args.password})
|
||||||
_configure_task = asyncio.create_task(client.configure(email=args.email, password=args.password))
|
configure_thread.start()
|
||||||
await client.run_forever()
|
client.run_forever()
|
||||||
|
|
||||||
|
|
||||||
def extract_addr(text: str) -> str:
|
def extract_addr(text: str) -> str:
|
||||||
|
|||||||
@@ -24,63 +24,63 @@ class Account:
|
|||||||
def _rpc(self) -> "Rpc":
|
def _rpc(self) -> "Rpc":
|
||||||
return self.manager.rpc
|
return self.manager.rpc
|
||||||
|
|
||||||
async def wait_for_event(self) -> AttrDict:
|
def wait_for_event(self) -> AttrDict:
|
||||||
"""Wait until the next event and return it."""
|
"""Wait until the next event and return it."""
|
||||||
return AttrDict(await self._rpc.wait_for_event(self.id))
|
return AttrDict(self._rpc.wait_for_event(self.id))
|
||||||
|
|
||||||
async def remove(self) -> None:
|
def remove(self) -> None:
|
||||||
"""Remove the account."""
|
"""Remove the account."""
|
||||||
await self._rpc.remove_account(self.id)
|
self._rpc.remove_account(self.id)
|
||||||
|
|
||||||
async def start_io(self) -> None:
|
def start_io(self) -> None:
|
||||||
"""Start the account I/O."""
|
"""Start the account I/O."""
|
||||||
await self._rpc.start_io(self.id)
|
self._rpc.start_io(self.id)
|
||||||
|
|
||||||
async def stop_io(self) -> None:
|
def stop_io(self) -> None:
|
||||||
"""Stop the account I/O."""
|
"""Stop the account I/O."""
|
||||||
await self._rpc.stop_io(self.id)
|
self._rpc.stop_io(self.id)
|
||||||
|
|
||||||
async def get_info(self) -> AttrDict:
|
def get_info(self) -> AttrDict:
|
||||||
"""Return dictionary of this account configuration parameters."""
|
"""Return dictionary of this account configuration parameters."""
|
||||||
return AttrDict(await self._rpc.get_info(self.id))
|
return AttrDict(self._rpc.get_info(self.id))
|
||||||
|
|
||||||
async def get_size(self) -> int:
|
def get_size(self) -> int:
|
||||||
"""Get the combined filesize of an account in bytes."""
|
"""Get the combined filesize of an account in bytes."""
|
||||||
return await self._rpc.get_account_file_size(self.id)
|
return self._rpc.get_account_file_size(self.id)
|
||||||
|
|
||||||
async def is_configured(self) -> bool:
|
def is_configured(self) -> bool:
|
||||||
"""Return True if this account is configured."""
|
"""Return True if this account is configured."""
|
||||||
return await self._rpc.is_configured(self.id)
|
return self._rpc.is_configured(self.id)
|
||||||
|
|
||||||
async def set_config(self, key: str, value: Optional[str] = None) -> None:
|
def set_config(self, key: str, value: Optional[str] = None) -> None:
|
||||||
"""Set configuration value."""
|
"""Set configuration value."""
|
||||||
await self._rpc.set_config(self.id, key, value)
|
self._rpc.set_config(self.id, key, value)
|
||||||
|
|
||||||
async def get_config(self, key: str) -> Optional[str]:
|
def get_config(self, key: str) -> Optional[str]:
|
||||||
"""Get configuration value."""
|
"""Get configuration value."""
|
||||||
return await self._rpc.get_config(self.id, key)
|
return self._rpc.get_config(self.id, key)
|
||||||
|
|
||||||
async 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():
|
||||||
await self.set_config(key, value)
|
self.set_config(key, value)
|
||||||
|
|
||||||
async def set_avatar(self, img_path: Optional[str] = None) -> None:
|
def set_avatar(self, img_path: Optional[str] = None) -> None:
|
||||||
"""Set self avatar.
|
"""Set self avatar.
|
||||||
|
|
||||||
Passing None will discard the currently set avatar.
|
Passing None will discard the currently set avatar.
|
||||||
"""
|
"""
|
||||||
await self.set_config("selfavatar", img_path)
|
self.set_config("selfavatar", img_path)
|
||||||
|
|
||||||
async def get_avatar(self) -> Optional[str]:
|
def get_avatar(self) -> Optional[str]:
|
||||||
"""Get self avatar."""
|
"""Get self avatar."""
|
||||||
return await self.get_config("selfavatar")
|
return self.get_config("selfavatar")
|
||||||
|
|
||||||
async def configure(self) -> None:
|
def configure(self) -> None:
|
||||||
"""Configure an account."""
|
"""Configure an account."""
|
||||||
await self._rpc.configure(self.id)
|
self._rpc.configure(self.id)
|
||||||
|
|
||||||
async def create_contact(self, obj: Union[int, str, Contact], name: Optional[str] = None) -> Contact:
|
def create_contact(self, obj: Union[int, str, Contact], 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
|
||||||
@@ -94,24 +94,24 @@ class Account:
|
|||||||
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 = (await obj.get_snapshot()).address
|
obj = obj.get_snapshot().address
|
||||||
return Contact(self, await self._rpc.create_contact(self.id, obj, name))
|
return Contact(self, self._rpc.create_contact(self.id, obj, name))
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
async def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
def get_contact_by_addr(self, address: str) -> Optional[Contact]:
|
||||||
"""Check if an e-mail address belongs to a known and unblocked contact."""
|
"""Check if an e-mail address belongs to a known and unblocked contact."""
|
||||||
contact_id = await 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)
|
||||||
|
|
||||||
async 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 = await 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]
|
||||||
|
|
||||||
async def get_contacts(
|
def get_contacts(
|
||||||
self,
|
self,
|
||||||
query: Optional[str] = None,
|
query: Optional[str] = None,
|
||||||
with_self: bool = False,
|
with_self: bool = False,
|
||||||
@@ -133,9 +133,9 @@ class Account:
|
|||||||
flags |= ContactFlag.ADD_SELF
|
flags |= ContactFlag.ADD_SELF
|
||||||
|
|
||||||
if snapshot:
|
if snapshot:
|
||||||
contacts = await self._rpc.get_contacts(self.id, flags, query)
|
contacts = self._rpc.get_contacts(self.id, flags, query)
|
||||||
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
return [AttrDict(contact=Contact(self, contact["id"]), **contact) for contact in contacts]
|
||||||
contacts = await self._rpc.get_contact_ids(self.id, flags, query)
|
contacts = self._rpc.get_contact_ids(self.id, flags, query)
|
||||||
return [Contact(self, contact_id) for contact_id in contacts]
|
return [Contact(self, contact_id) for contact_id in contacts]
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -143,7 +143,7 @@ class Account:
|
|||||||
"""This account's identity as a Contact."""
|
"""This account's identity as a Contact."""
|
||||||
return Contact(self, SpecialContactId.SELF)
|
return Contact(self, SpecialContactId.SELF)
|
||||||
|
|
||||||
async def get_chatlist(
|
def get_chatlist(
|
||||||
self,
|
self,
|
||||||
query: Optional[str] = None,
|
query: Optional[str] = None,
|
||||||
contact: Optional[Contact] = None,
|
contact: Optional[Contact] = None,
|
||||||
@@ -175,29 +175,29 @@ class Account:
|
|||||||
if alldone_hint:
|
if alldone_hint:
|
||||||
flags |= ChatlistFlag.ADD_ALLDONE_HINT
|
flags |= ChatlistFlag.ADD_ALLDONE_HINT
|
||||||
|
|
||||||
entries = await self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
|
entries = self._rpc.get_chatlist_entries(self.id, flags, query, contact and contact.id)
|
||||||
if not snapshot:
|
if not snapshot:
|
||||||
return [Chat(self, entry[0]) for entry in entries]
|
return [Chat(self, entry) for entry in entries]
|
||||||
|
|
||||||
items = await self._rpc.get_chatlist_items_by_entries(self.id, entries)
|
items = self._rpc.get_chatlist_items_by_entries(self.id, entries)
|
||||||
chats = []
|
chats = []
|
||||||
for item in items.values():
|
for item in items.values():
|
||||||
item["chat"] = Chat(self, item["id"])
|
item["chat"] = Chat(self, item["id"])
|
||||||
chats.append(AttrDict(item))
|
chats.append(AttrDict(item))
|
||||||
return chats
|
return chats
|
||||||
|
|
||||||
async def create_group(self, name: str, protect: bool = False) -> Chat:
|
def create_group(self, name: str, protect: bool = False) -> Chat:
|
||||||
"""Create a new group chat.
|
"""Create a new group chat.
|
||||||
|
|
||||||
After creation, the group has only self-contact as member and is in unpromoted state.
|
After creation, the group has only self-contact as member and is in unpromoted state.
|
||||||
"""
|
"""
|
||||||
return Chat(self, await self._rpc.create_group_chat(self.id, name, protect))
|
return Chat(self, self._rpc.create_group_chat(self.id, name, protect))
|
||||||
|
|
||||||
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)
|
||||||
|
|
||||||
async 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
|
"""Continue a Setup-Contact or Verified-Group-Invite protocol started on
|
||||||
another device.
|
another device.
|
||||||
|
|
||||||
@@ -208,54 +208,62 @@ class Account:
|
|||||||
|
|
||||||
:param qrdata: The text of the scanned QR code.
|
:param qrdata: The text of the scanned QR code.
|
||||||
"""
|
"""
|
||||||
return Chat(self, await self._rpc.secure_join(self.id, qrdata))
|
return Chat(self, self._rpc.secure_join(self.id, qrdata))
|
||||||
|
|
||||||
async def get_qr_code(self) -> Tuple[str, str]:
|
def get_qr_code(self) -> Tuple[str, str]:
|
||||||
"""Get Setup-Contact QR Code text and SVG data.
|
"""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 await 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)
|
||||||
|
|
||||||
async 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."""
|
||||||
await self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
|
self._rpc.markseen_msgs(self.id, [msg.id for msg in messages])
|
||||||
|
|
||||||
async 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)."""
|
||||||
await self._rpc.delete_messages(self.id, [msg.id for msg in messages])
|
self._rpc.delete_messages(self.id, [msg.id for msg in messages])
|
||||||
|
|
||||||
async 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.
|
||||||
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
|
If you are writing a bot, use `get_fresh_messages_in_arrival_order()` instead,
|
||||||
to process oldest messages first.
|
to process oldest messages first.
|
||||||
"""
|
"""
|
||||||
fresh_msg_ids = await 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]
|
||||||
|
|
||||||
async 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 = await 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]
|
||||||
|
|
||||||
async 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 = await self._rpc.wait_next_msgs(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]
|
||||||
|
|
||||||
async def get_fresh_messages_in_arrival_order(self) -> List[Message]:
|
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.",
|
||||||
DeprecationWarning,
|
DeprecationWarning,
|
||||||
stacklevel=2,
|
stacklevel=2,
|
||||||
)
|
)
|
||||||
fresh_msg_ids = sorted(await self._rpc.get_fresh_msgs(self.id))
|
fresh_msg_ids = sorted(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 export_backup(self, path, passphrase: str = "") -> None:
|
||||||
|
"""Export backup."""
|
||||||
|
self._rpc.export_backup(self.id, str(path), passphrase)
|
||||||
|
|
||||||
|
def import_backup(self, path, passphrase: str = "") -> None:
|
||||||
|
"""Import backup."""
|
||||||
|
self._rpc.import_backup(self.id, str(path), passphrase)
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ class Chat:
|
|||||||
def _rpc(self) -> "Rpc":
|
def _rpc(self) -> "Rpc":
|
||||||
return self.account._rpc
|
return self.account._rpc
|
||||||
|
|
||||||
async def delete(self) -> None:
|
def delete(self) -> None:
|
||||||
"""Delete this chat and all its messages.
|
"""Delete this chat and all its messages.
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
@@ -33,83 +33,83 @@ class Chat:
|
|||||||
- does not delete messages on server
|
- does not delete messages on server
|
||||||
- the chat or contact is not blocked, new message will arrive
|
- the chat or contact is not blocked, new message will arrive
|
||||||
"""
|
"""
|
||||||
await self._rpc.delete_chat(self.account.id, self.id)
|
self._rpc.delete_chat(self.account.id, self.id)
|
||||||
|
|
||||||
async def block(self) -> None:
|
def block(self) -> None:
|
||||||
"""Block this chat."""
|
"""Block this chat."""
|
||||||
await self._rpc.block_chat(self.account.id, self.id)
|
self._rpc.block_chat(self.account.id, self.id)
|
||||||
|
|
||||||
async def accept(self) -> None:
|
def accept(self) -> None:
|
||||||
"""Accept this contact request chat."""
|
"""Accept this contact request chat."""
|
||||||
await self._rpc.accept_chat(self.account.id, self.id)
|
self._rpc.accept_chat(self.account.id, self.id)
|
||||||
|
|
||||||
async def leave(self) -> None:
|
def leave(self) -> None:
|
||||||
"""Leave this chat."""
|
"""Leave this chat."""
|
||||||
await self._rpc.leave_group(self.account.id, self.id)
|
self._rpc.leave_group(self.account.id, self.id)
|
||||||
|
|
||||||
async def mute(self, duration: Optional[int] = None) -> None:
|
def mute(self, duration: Optional[int] = None) -> None:
|
||||||
"""Mute this chat, if a duration is not provided the chat is muted forever.
|
"""Mute this chat, if a duration is not provided the chat is muted forever.
|
||||||
|
|
||||||
:param duration: mute duration from now in seconds. Must be greater than zero.
|
:param duration: mute duration from now in seconds. Must be greater than zero.
|
||||||
"""
|
"""
|
||||||
if duration is not None:
|
if duration is not None:
|
||||||
assert duration > 0, "Invalid duration"
|
assert duration > 0, "Invalid duration"
|
||||||
dur: Union[str, dict] = {"Until": duration}
|
dur: dict = {"kind": "Until", "duration": duration}
|
||||||
else:
|
else:
|
||||||
dur = "Forever"
|
dur = {"kind": "Forever"}
|
||||||
await self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
|
self._rpc.set_chat_mute_duration(self.account.id, self.id, dur)
|
||||||
|
|
||||||
async def unmute(self) -> None:
|
def unmute(self) -> None:
|
||||||
"""Unmute this chat."""
|
"""Unmute this chat."""
|
||||||
await self._rpc.set_chat_mute_duration(self.account.id, self.id, "NotMuted")
|
self._rpc.set_chat_mute_duration(self.account.id, self.id, {"kind": "NotMuted"})
|
||||||
|
|
||||||
async def pin(self) -> None:
|
def pin(self) -> None:
|
||||||
"""Pin this chat."""
|
"""Pin this chat."""
|
||||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
|
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.PINNED)
|
||||||
|
|
||||||
async def unpin(self) -> None:
|
def unpin(self) -> None:
|
||||||
"""Unpin this chat."""
|
"""Unpin this chat."""
|
||||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||||
|
|
||||||
async def archive(self) -> None:
|
def archive(self) -> None:
|
||||||
"""Archive this chat."""
|
"""Archive this chat."""
|
||||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
|
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.ARCHIVED)
|
||||||
|
|
||||||
async def unarchive(self) -> None:
|
def unarchive(self) -> None:
|
||||||
"""Unarchive this chat."""
|
"""Unarchive this chat."""
|
||||||
await self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
self._rpc.set_chat_visibility(self.account.id, self.id, ChatVisibility.NORMAL)
|
||||||
|
|
||||||
async def set_name(self, name: str) -> None:
|
def set_name(self, name: str) -> None:
|
||||||
"""Set name of this chat."""
|
"""Set name of this chat."""
|
||||||
await self._rpc.set_chat_name(self.account.id, self.id, name)
|
self._rpc.set_chat_name(self.account.id, self.id, name)
|
||||||
|
|
||||||
async def set_ephemeral_timer(self, timer: int) -> None:
|
def set_ephemeral_timer(self, timer: int) -> None:
|
||||||
"""Set ephemeral timer of this chat."""
|
"""Set ephemeral timer of this chat."""
|
||||||
await self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
self._rpc.set_chat_ephemeral_timer(self.account.id, self.id, timer)
|
||||||
|
|
||||||
async 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 await self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
return self._rpc.get_chat_encryption_info(self.account.id, self.id)
|
||||||
|
|
||||||
async def get_qr_code(self) -> Tuple[str, str]:
|
def get_qr_code(self) -> Tuple[str, str]:
|
||||||
"""Get Join-Group QR code text and SVG data."""
|
"""Get Join-Group QR code text and SVG data."""
|
||||||
return await 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)
|
||||||
|
|
||||||
async def get_basic_snapshot(self) -> AttrDict:
|
def get_basic_snapshot(self) -> AttrDict:
|
||||||
"""Get a chat snapshot with basic info about this chat."""
|
"""Get a chat snapshot with basic info about this chat."""
|
||||||
info = await self._rpc.get_basic_chat_info(self.account.id, self.id)
|
info = self._rpc.get_basic_chat_info(self.account.id, self.id)
|
||||||
return AttrDict(chat=self, **info)
|
return AttrDict(chat=self, **info)
|
||||||
|
|
||||||
async def get_full_snapshot(self) -> AttrDict:
|
def get_full_snapshot(self) -> AttrDict:
|
||||||
"""Get a full snapshot of this chat."""
|
"""Get a full snapshot of this chat."""
|
||||||
info = await self._rpc.get_full_chat_by_id(self.account.id, self.id)
|
info = self._rpc.get_full_chat_by_id(self.account.id, self.id)
|
||||||
return AttrDict(chat=self, **info)
|
return AttrDict(chat=self, **info)
|
||||||
|
|
||||||
async def can_send(self) -> bool:
|
def can_send(self) -> bool:
|
||||||
"""Return true if messages can be sent to the chat."""
|
"""Return true if messages can be sent to the chat."""
|
||||||
return await self._rpc.can_send(self.account.id, self.id)
|
return self._rpc.can_send(self.account.id, self.id)
|
||||||
|
|
||||||
async def send_message(
|
def send_message(
|
||||||
self,
|
self,
|
||||||
text: Optional[str] = None,
|
text: Optional[str] = None,
|
||||||
html: Optional[str] = None,
|
html: Optional[str] = None,
|
||||||
@@ -132,30 +132,30 @@ class Chat:
|
|||||||
"overrideSenderName": override_sender_name,
|
"overrideSenderName": override_sender_name,
|
||||||
"quotedMessageId": quoted_msg,
|
"quotedMessageId": quoted_msg,
|
||||||
}
|
}
|
||||||
msg_id = await self._rpc.send_msg(self.account.id, self.id, draft)
|
msg_id = self._rpc.send_msg(self.account.id, self.id, draft)
|
||||||
return Message(self.account, msg_id)
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
async def send_text(self, text: str) -> Message:
|
def send_text(self, text: str) -> Message:
|
||||||
"""Send a text message and return the resulting Message instance."""
|
"""Send a text message and return the resulting Message instance."""
|
||||||
msg_id = await 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)
|
||||||
|
|
||||||
async 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 = await self._rpc.send_videochat_invitation(self.account.id, self.id)
|
msg_id = self._rpc.send_videochat_invitation(self.account.id, self.id)
|
||||||
return Message(self.account, msg_id)
|
return Message(self.account, msg_id)
|
||||||
|
|
||||||
async def send_sticker(self, path: str) -> Message:
|
def send_sticker(self, path: str) -> Message:
|
||||||
"""Send an sticker and return the resulting Message instance."""
|
"""Send an sticker and return the resulting Message instance."""
|
||||||
msg_id = await 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)
|
||||||
|
|
||||||
async def forward_messages(self, messages: List[Message]) -> None:
|
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]
|
||||||
await self._rpc.forward_messages(self.account.id, msg_ids, self.id)
|
self._rpc.forward_messages(self.account.id, msg_ids, self.id)
|
||||||
|
|
||||||
async def set_draft(
|
def set_draft(
|
||||||
self,
|
self,
|
||||||
text: Optional[str] = None,
|
text: Optional[str] = None,
|
||||||
file: Optional[str] = None,
|
file: Optional[str] = None,
|
||||||
@@ -164,15 +164,15 @@ class Chat:
|
|||||||
"""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
|
||||||
await self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
|
self._rpc.misc_set_draft(self.account.id, self.id, text, file, quoted_msg)
|
||||||
|
|
||||||
async def remove_draft(self) -> None:
|
def remove_draft(self) -> None:
|
||||||
"""Remove draft message."""
|
"""Remove draft message."""
|
||||||
await self._rpc.remove_draft(self.account.id, self.id)
|
self._rpc.remove_draft(self.account.id, self.id)
|
||||||
|
|
||||||
async def get_draft(self) -> Optional[AttrDict]:
|
def get_draft(self) -> Optional[AttrDict]:
|
||||||
"""Get draft message."""
|
"""Get draft message."""
|
||||||
snapshot = await self._rpc.get_draft(self.account.id, self.id)
|
snapshot = self._rpc.get_draft(self.account.id, self.id)
|
||||||
if not snapshot:
|
if not snapshot:
|
||||||
return None
|
return None
|
||||||
snapshot = AttrDict(snapshot)
|
snapshot = AttrDict(snapshot)
|
||||||
@@ -181,61 +181,61 @@ class Chat:
|
|||||||
snapshot["message"] = Message(self.account, snapshot.id)
|
snapshot["message"] = Message(self.account, snapshot.id)
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
async 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 = await 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]
|
||||||
|
|
||||||
async 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 await self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
return self._rpc.get_fresh_msg_cnt(self.account.id, self.id)
|
||||||
|
|
||||||
async def mark_noticed(self) -> None:
|
def mark_noticed(self) -> None:
|
||||||
"""Mark all messages in this chat as noticed."""
|
"""Mark all messages in this chat as noticed."""
|
||||||
await self._rpc.marknoticed_chat(self.account.id, self.id)
|
self._rpc.marknoticed_chat(self.account.id, self.id)
|
||||||
|
|
||||||
async def add_contact(self, *contact: Union[int, str, Contact]) -> None:
|
def add_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||||
"""Add contacts to this group."""
|
"""Add contacts to this group."""
|
||||||
for cnt in contact:
|
for cnt in contact:
|
||||||
if isinstance(cnt, str):
|
if isinstance(cnt, str):
|
||||||
contact_id = (await self.account.create_contact(cnt)).id
|
contact_id = self.account.create_contact(cnt).id
|
||||||
elif not isinstance(cnt, int):
|
elif not isinstance(cnt, int):
|
||||||
contact_id = cnt.id
|
contact_id = cnt.id
|
||||||
else:
|
else:
|
||||||
contact_id = cnt
|
contact_id = cnt
|
||||||
await self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
|
self._rpc.add_contact_to_chat(self.account.id, self.id, contact_id)
|
||||||
|
|
||||||
async def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
|
def remove_contact(self, *contact: Union[int, str, Contact]) -> None:
|
||||||
"""Remove members from this group."""
|
"""Remove members from this group."""
|
||||||
for cnt in contact:
|
for cnt in contact:
|
||||||
if isinstance(cnt, str):
|
if isinstance(cnt, str):
|
||||||
contact_id = (await self.account.create_contact(cnt)).id
|
contact_id = self.account.create_contact(cnt).id
|
||||||
elif not isinstance(cnt, int):
|
elif not isinstance(cnt, int):
|
||||||
contact_id = cnt.id
|
contact_id = cnt.id
|
||||||
else:
|
else:
|
||||||
contact_id = cnt
|
contact_id = cnt
|
||||||
await 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)
|
||||||
|
|
||||||
async 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.
|
||||||
"""
|
"""
|
||||||
contacts = await 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]
|
||||||
|
|
||||||
async 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.
|
||||||
|
|
||||||
:param path: Full path of the image to use as the group image.
|
:param path: Full path of the image to use as the group image.
|
||||||
"""
|
"""
|
||||||
await self._rpc.set_chat_profile_image(self.account.id, self.id, path)
|
self._rpc.set_chat_profile_image(self.account.id, self.id, path)
|
||||||
|
|
||||||
async def remove_image(self) -> None:
|
def remove_image(self) -> None:
|
||||||
"""Remove profile image of this chat."""
|
"""Remove profile image of this chat."""
|
||||||
await self._rpc.set_chat_profile_image(self.account.id, self.id, None)
|
self._rpc.set_chat_profile_image(self.account.id, self.id, None)
|
||||||
|
|
||||||
async def get_locations(
|
def get_locations(
|
||||||
self,
|
self,
|
||||||
contact: Optional[Contact] = None,
|
contact: Optional[Contact] = None,
|
||||||
timestamp_from: Optional["datetime"] = None,
|
timestamp_from: Optional["datetime"] = None,
|
||||||
@@ -246,7 +246,7 @@ class Chat:
|
|||||||
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
|
time_to = calendar.timegm(timestamp_to.utctimetuple()) if timestamp_to else 0
|
||||||
contact_id = contact.id if contact else 0
|
contact_id = contact.id if contact else 0
|
||||||
|
|
||||||
result = await 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:
|
||||||
|
|||||||
@@ -1,10 +1,8 @@
|
|||||||
"""Event loop implementations offering high level event handling/hooking."""
|
"""Event loop implementations offering high level event handling/hooking."""
|
||||||
import inspect
|
|
||||||
import logging
|
import logging
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
Callable,
|
Callable,
|
||||||
Coroutine,
|
|
||||||
Dict,
|
Dict,
|
||||||
Iterable,
|
Iterable,
|
||||||
Optional,
|
Optional,
|
||||||
@@ -78,22 +76,22 @@ class Client:
|
|||||||
)
|
)
|
||||||
self._hooks.get(type(event), set()).remove((hook, event))
|
self._hooks.get(type(event), set()).remove((hook, event))
|
||||||
|
|
||||||
async def is_configured(self) -> bool:
|
def is_configured(self) -> bool:
|
||||||
return await self.account.is_configured()
|
return self.account.is_configured()
|
||||||
|
|
||||||
async def configure(self, email: str, password: str, **kwargs) -> None:
|
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||||
await self.account.set_config("addr", email)
|
self.account.set_config("addr", email)
|
||||||
await self.account.set_config("mail_pw", password)
|
self.account.set_config("mail_pw", password)
|
||||||
for key, value in kwargs.items():
|
for key, value in kwargs.items():
|
||||||
await self.account.set_config(key, value)
|
self.account.set_config(key, value)
|
||||||
await self.account.configure()
|
self.account.configure()
|
||||||
self.logger.debug("Account configured")
|
self.logger.debug("Account configured")
|
||||||
|
|
||||||
async def run_forever(self) -> None:
|
def run_forever(self) -> None:
|
||||||
"""Process events forever."""
|
"""Process events forever."""
|
||||||
await self.run_until(lambda _: False)
|
self.run_until(lambda _: False)
|
||||||
|
|
||||||
async def run_until(self, func: Callable[[AttrDict], Union[bool, Coroutine]]) -> AttrDict:
|
def run_until(self, func: Callable[[AttrDict], bool]) -> AttrDict:
|
||||||
"""Process events until the given callable evaluates to True.
|
"""Process events until the given callable evaluates to True.
|
||||||
|
|
||||||
The callable should accept an AttrDict object representing the
|
The callable should accept an AttrDict object representing the
|
||||||
@@ -101,39 +99,37 @@ class Client:
|
|||||||
evaluates to True.
|
evaluates to True.
|
||||||
"""
|
"""
|
||||||
self.logger.debug("Listening to incoming events...")
|
self.logger.debug("Listening to incoming events...")
|
||||||
if await self.is_configured():
|
if self.is_configured():
|
||||||
await self.account.start_io()
|
self.account.start_io()
|
||||||
await self._process_messages() # Process old messages.
|
self._process_messages() # Process old messages.
|
||||||
while True:
|
while True:
|
||||||
event = await self.account.wait_for_event()
|
event = self.account.wait_for_event()
|
||||||
event["type"] = EventType(event.type)
|
event["kind"] = EventType(event.kind)
|
||||||
event["account"] = self.account
|
event["account"] = self.account
|
||||||
await self._on_event(event)
|
self._on_event(event)
|
||||||
if event.type == EventType.INCOMING_MSG:
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
await self._process_messages()
|
self._process_messages()
|
||||||
|
|
||||||
stop = func(event)
|
stop = func(event)
|
||||||
if inspect.isawaitable(stop):
|
|
||||||
stop = await stop
|
|
||||||
if stop:
|
if stop:
|
||||||
return event
|
return event
|
||||||
|
|
||||||
async def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
|
def _on_event(self, event: AttrDict, filter_type: Type[EventFilter] = RawEvent) -> None:
|
||||||
for hook, evfilter in self._hooks.get(filter_type, []):
|
for hook, evfilter in self._hooks.get(filter_type, []):
|
||||||
if await evfilter.filter(event):
|
if evfilter.filter(event):
|
||||||
try:
|
try:
|
||||||
await hook(event)
|
hook(event)
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
self.logger.exception(ex)
|
self.logger.exception(ex)
|
||||||
|
|
||||||
async def _parse_command(self, event: AttrDict) -> None:
|
def _parse_command(self, event: AttrDict) -> None:
|
||||||
cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
|
cmds = [hook[1].command for hook in self._hooks.get(NewMessage, []) if hook[1].command]
|
||||||
parts = event.message_snapshot.text.split(maxsplit=1)
|
parts = event.message_snapshot.text.split(maxsplit=1)
|
||||||
payload = parts[1] if len(parts) > 1 else ""
|
payload = parts[1] if len(parts) > 1 else ""
|
||||||
cmd = parts.pop(0)
|
cmd = parts.pop(0)
|
||||||
|
|
||||||
if "@" in cmd:
|
if "@" in cmd:
|
||||||
suffix = "@" + (await self.account.self_contact.get_snapshot()).address
|
suffix = "@" + self.account.self_contact.get_snapshot().address
|
||||||
if cmd.endswith(suffix):
|
if cmd.endswith(suffix):
|
||||||
cmd = cmd[: -len(suffix)]
|
cmd = cmd[: -len(suffix)]
|
||||||
else:
|
else:
|
||||||
@@ -153,32 +149,32 @@ class Client:
|
|||||||
|
|
||||||
event["command"], event["payload"] = cmd, payload
|
event["command"], event["payload"] = cmd, payload
|
||||||
|
|
||||||
async def _on_new_msg(self, snapshot: AttrDict) -> None:
|
def _on_new_msg(self, snapshot: AttrDict) -> None:
|
||||||
event = AttrDict(command="", payload="", message_snapshot=snapshot)
|
event = AttrDict(command="", payload="", message_snapshot=snapshot)
|
||||||
if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX):
|
if not snapshot.is_info and snapshot.text.startswith(COMMAND_PREFIX):
|
||||||
await self._parse_command(event)
|
self._parse_command(event)
|
||||||
await self._on_event(event, NewMessage)
|
self._on_event(event, NewMessage)
|
||||||
|
|
||||||
async def _handle_info_msg(self, snapshot: AttrDict) -> None:
|
def _handle_info_msg(self, snapshot: AttrDict) -> None:
|
||||||
event = AttrDict(message_snapshot=snapshot)
|
event = AttrDict(message_snapshot=snapshot)
|
||||||
|
|
||||||
img_changed = parse_system_image_changed(snapshot.text)
|
img_changed = parse_system_image_changed(snapshot.text)
|
||||||
if img_changed:
|
if img_changed:
|
||||||
_, event["image_deleted"] = img_changed
|
_, event["image_deleted"] = img_changed
|
||||||
await self._on_event(event, GroupImageChanged)
|
self._on_event(event, GroupImageChanged)
|
||||||
return
|
return
|
||||||
|
|
||||||
title_changed = parse_system_title_changed(snapshot.text)
|
title_changed = parse_system_title_changed(snapshot.text)
|
||||||
if title_changed:
|
if title_changed:
|
||||||
_, event["old_name"] = title_changed
|
_, event["old_name"] = title_changed
|
||||||
await self._on_event(event, GroupNameChanged)
|
self._on_event(event, GroupNameChanged)
|
||||||
return
|
return
|
||||||
|
|
||||||
members_changed = parse_system_add_remove(snapshot.text)
|
members_changed = parse_system_add_remove(snapshot.text)
|
||||||
if members_changed:
|
if members_changed:
|
||||||
action, event["member"], _ = members_changed
|
action, event["member"], _ = members_changed
|
||||||
event["member_added"] = action == "added"
|
event["member_added"] = action == "added"
|
||||||
await self._on_event(event, MemberListChanged)
|
self._on_event(event, MemberListChanged)
|
||||||
return
|
return
|
||||||
|
|
||||||
self.logger.warning(
|
self.logger.warning(
|
||||||
@@ -187,20 +183,20 @@ class Client:
|
|||||||
snapshot.text,
|
snapshot.text,
|
||||||
)
|
)
|
||||||
|
|
||||||
async def _process_messages(self) -> None:
|
def _process_messages(self) -> None:
|
||||||
if self._should_process_messages:
|
if self._should_process_messages:
|
||||||
for message in await self.account.get_next_messages():
|
for message in self.account.get_next_messages():
|
||||||
snapshot = await message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]:
|
if snapshot.from_id not in [SpecialContactId.SELF, SpecialContactId.DEVICE]:
|
||||||
await self._on_new_msg(snapshot)
|
self._on_new_msg(snapshot)
|
||||||
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
|
if snapshot.is_info and snapshot.system_message_type != SystemMessageType.WEBXDC_INFO_MESSAGE:
|
||||||
await self._handle_info_msg(snapshot)
|
self._handle_info_msg(snapshot)
|
||||||
await snapshot.message.mark_seen()
|
snapshot.message.mark_seen()
|
||||||
|
|
||||||
|
|
||||||
class Bot(Client):
|
class Bot(Client):
|
||||||
"""Simple bot implementation that listent to events of a single account."""
|
"""Simple bot implementation that listent to events of a single account."""
|
||||||
|
|
||||||
async def configure(self, email: str, password: str, **kwargs) -> None:
|
def configure(self, email: str, password: str, **kwargs) -> None:
|
||||||
kwargs.setdefault("bot", "1")
|
kwargs.setdefault("bot", "1")
|
||||||
await super().configure(email, password, **kwargs)
|
super().configure(email, password, **kwargs)
|
||||||
|
|||||||
@@ -45,6 +45,7 @@ class EventType(str, Enum):
|
|||||||
MSG_DELIVERED = "MsgDelivered"
|
MSG_DELIVERED = "MsgDelivered"
|
||||||
MSG_FAILED = "MsgFailed"
|
MSG_FAILED = "MsgFailed"
|
||||||
MSG_READ = "MsgRead"
|
MSG_READ = "MsgRead"
|
||||||
|
MSG_DELETED = "MsgDeleted"
|
||||||
CHAT_MODIFIED = "ChatModified"
|
CHAT_MODIFIED = "ChatModified"
|
||||||
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
|
CHAT_EPHEMERAL_TIMER_MODIFIED = "ChatEphemeralTimerModified"
|
||||||
CONTACTS_CHANGED = "ContactsChanged"
|
CONTACTS_CHANGED = "ContactsChanged"
|
||||||
|
|||||||
@@ -24,39 +24,39 @@ class Contact:
|
|||||||
def _rpc(self) -> "Rpc":
|
def _rpc(self) -> "Rpc":
|
||||||
return self.account._rpc
|
return self.account._rpc
|
||||||
|
|
||||||
async def block(self) -> None:
|
def block(self) -> None:
|
||||||
"""Block contact."""
|
"""Block contact."""
|
||||||
await self._rpc.block_contact(self.account.id, self.id)
|
self._rpc.block_contact(self.account.id, self.id)
|
||||||
|
|
||||||
async def unblock(self) -> None:
|
def unblock(self) -> None:
|
||||||
"""Unblock contact."""
|
"""Unblock contact."""
|
||||||
await self._rpc.unblock_contact(self.account.id, self.id)
|
self._rpc.unblock_contact(self.account.id, self.id)
|
||||||
|
|
||||||
async def delete(self) -> None:
|
def delete(self) -> None:
|
||||||
"""Delete contact."""
|
"""Delete contact."""
|
||||||
await self._rpc.delete_contact(self.account.id, self.id)
|
self._rpc.delete_contact(self.account.id, self.id)
|
||||||
|
|
||||||
async def set_name(self, name: str) -> None:
|
def set_name(self, name: str) -> None:
|
||||||
"""Change the name of this contact."""
|
"""Change the name of this contact."""
|
||||||
await self._rpc.change_contact_name(self.account.id, self.id, name)
|
self._rpc.change_contact_name(self.account.id, self.id, name)
|
||||||
|
|
||||||
async def get_encryption_info(self) -> str:
|
def get_encryption_info(self) -> str:
|
||||||
"""Get a multi-line encryption info, containing your fingerprint and
|
"""Get a multi-line encryption info, containing your fingerprint and
|
||||||
the fingerprint of the contact.
|
the fingerprint of the contact.
|
||||||
"""
|
"""
|
||||||
return await self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
return self._rpc.get_contact_encryption_info(self.account.id, self.id)
|
||||||
|
|
||||||
async def get_snapshot(self) -> AttrDict:
|
def get_snapshot(self) -> AttrDict:
|
||||||
"""Return a dictionary with a snapshot of all contact properties."""
|
"""Return a dictionary with a snapshot of all contact properties."""
|
||||||
snapshot = AttrDict(await self._rpc.get_contact(self.account.id, self.id))
|
snapshot = AttrDict(self._rpc.get_contact(self.account.id, self.id))
|
||||||
snapshot["contact"] = self
|
snapshot["contact"] = self
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
async def create_chat(self) -> "Chat":
|
def create_chat(self) -> "Chat":
|
||||||
"""Create or get an existing 1:1 chat for this contact."""
|
"""Create or get an existing 1:1 chat for this contact."""
|
||||||
from .chat import Chat
|
from .chat import Chat
|
||||||
|
|
||||||
return Chat(
|
return Chat(
|
||||||
self.account,
|
self.account,
|
||||||
await self._rpc.create_chat_by_contact_id(self.account.id, self.id),
|
self._rpc.create_chat_by_contact_id(self.account.id, self.id),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -16,34 +16,34 @@ class DeltaChat:
|
|||||||
def __init__(self, rpc: "Rpc") -> None:
|
def __init__(self, rpc: "Rpc") -> None:
|
||||||
self.rpc = rpc
|
self.rpc = rpc
|
||||||
|
|
||||||
async def add_account(self) -> Account:
|
def add_account(self) -> Account:
|
||||||
"""Create a new account database."""
|
"""Create a new account database."""
|
||||||
account_id = await self.rpc.add_account()
|
account_id = self.rpc.add_account()
|
||||||
return Account(self, account_id)
|
return Account(self, account_id)
|
||||||
|
|
||||||
async 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 = await 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]
|
||||||
|
|
||||||
async def start_io(self) -> None:
|
def start_io(self) -> None:
|
||||||
"""Start the I/O of all accounts."""
|
"""Start the I/O of all accounts."""
|
||||||
await self.rpc.start_io_for_all_accounts()
|
self.rpc.start_io_for_all_accounts()
|
||||||
|
|
||||||
async def stop_io(self) -> None:
|
def stop_io(self) -> None:
|
||||||
"""Stop the I/O of all accounts."""
|
"""Stop the I/O of all accounts."""
|
||||||
await self.rpc.stop_io_for_all_accounts()
|
self.rpc.stop_io_for_all_accounts()
|
||||||
|
|
||||||
async def maybe_network(self) -> None:
|
def maybe_network(self) -> None:
|
||||||
"""Indicate that the network likely has come back or just that the network
|
"""Indicate that the network likely has come back or just that the network
|
||||||
conditions might have changed.
|
conditions might have changed.
|
||||||
"""
|
"""
|
||||||
await self.rpc.maybe_network()
|
self.rpc.maybe_network()
|
||||||
|
|
||||||
async 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(await self.rpc.get_system_info())
|
return AttrDict(self.rpc.get_system_info())
|
||||||
|
|
||||||
async 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."""
|
||||||
await self.rpc.set_stock_strings(translations)
|
self.rpc.set_stock_strings(translations)
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
"""High-level classes for event processing and filtering."""
|
"""High-level classes for event processing and filtering."""
|
||||||
import inspect
|
|
||||||
import re
|
import re
|
||||||
from abc import ABC, abstractmethod
|
from abc import ABC, abstractmethod
|
||||||
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
|
from typing import TYPE_CHECKING, Callable, Iterable, Iterator, Optional, Set, Tuple, Union
|
||||||
@@ -24,7 +23,7 @@ def _tuple_of(obj, type_: type) -> tuple:
|
|||||||
class EventFilter(ABC):
|
class EventFilter(ABC):
|
||||||
"""The base event filter.
|
"""The base event filter.
|
||||||
|
|
||||||
:param func: A Callable (async or not) function that should accept the event as input
|
:param func: A Callable function that should accept the event as input
|
||||||
parameter, and return a bool value indicating whether the event
|
parameter, and return a bool value indicating whether the event
|
||||||
should be dispatched or not.
|
should be dispatched or not.
|
||||||
"""
|
"""
|
||||||
@@ -43,16 +42,13 @@ class EventFilter(ABC):
|
|||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not self == other
|
return not self == other
|
||||||
|
|
||||||
async def _call_func(self, event) -> bool:
|
def _call_func(self, event) -> bool:
|
||||||
if not self.func:
|
if not self.func:
|
||||||
return True
|
return True
|
||||||
res = self.func(event)
|
return self.func(event)
|
||||||
if inspect.isawaitable(res):
|
|
||||||
return await res
|
|
||||||
return res
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def filter(self, event):
|
def filter(self, event):
|
||||||
"""Return True-like value if the event passed the filter and should be
|
"""Return True-like value if the event passed the filter and should be
|
||||||
used, or False-like value otherwise.
|
used, or False-like value otherwise.
|
||||||
"""
|
"""
|
||||||
@@ -62,7 +58,7 @@ class RawEvent(EventFilter):
|
|||||||
"""Matches raw core events.
|
"""Matches raw core events.
|
||||||
|
|
||||||
:param types: The types of event to match.
|
:param types: The types of event to match.
|
||||||
:param func: A Callable (async or not) function that should accept the event as input
|
:param func: A Callable function that should accept the event as input
|
||||||
parameter, and return a bool value indicating whether the event
|
parameter, and return a bool value indicating whether the event
|
||||||
should be dispatched or not.
|
should be dispatched or not.
|
||||||
"""
|
"""
|
||||||
@@ -82,10 +78,10 @@ class RawEvent(EventFilter):
|
|||||||
return (self.types, self.func) == (other.types, other.func)
|
return (self.types, self.func) == (other.types, other.func)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
if self.types and event.type not in self.types:
|
if self.types and event.kind not in self.types:
|
||||||
return False
|
return False
|
||||||
return await self._call_func(event)
|
return self._call_func(event)
|
||||||
|
|
||||||
|
|
||||||
class NewMessage(EventFilter):
|
class NewMessage(EventFilter):
|
||||||
@@ -104,7 +100,7 @@ class NewMessage(EventFilter):
|
|||||||
:param is_info: If set to True only match info/system messages, if set to False
|
: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
|
only match messages that are not info/system messages. If omitted
|
||||||
info/system messages as well as normal messages will be matched.
|
info/system messages as well as normal messages will be matched.
|
||||||
:param func: A Callable (async or not) function that should accept the event as input
|
:param func: A Callable function that should accept the event as input
|
||||||
parameter, and return a bool value indicating whether the event
|
parameter, and return a bool value indicating whether the event
|
||||||
should be dispatched or not.
|
should be dispatched or not.
|
||||||
"""
|
"""
|
||||||
@@ -159,7 +155,7 @@ class NewMessage(EventFilter):
|
|||||||
)
|
)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
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:
|
||||||
@@ -168,11 +164,9 @@ class NewMessage(EventFilter):
|
|||||||
return False
|
return False
|
||||||
if self.pattern:
|
if self.pattern:
|
||||||
match = self.pattern(event.message_snapshot.text)
|
match = self.pattern(event.message_snapshot.text)
|
||||||
if inspect.isawaitable(match):
|
|
||||||
match = await match
|
|
||||||
if not match:
|
if not match:
|
||||||
return False
|
return False
|
||||||
return await super()._call_func(event)
|
return super()._call_func(event)
|
||||||
|
|
||||||
|
|
||||||
class MemberListChanged(EventFilter):
|
class MemberListChanged(EventFilter):
|
||||||
@@ -184,7 +178,7 @@ class MemberListChanged(EventFilter):
|
|||||||
:param added: If set to True only match if a member was added, if set to False
|
:param added: If set to True only match if a member was added, if set to False
|
||||||
only match if a member was removed. If omitted both, member additions
|
only match if a member was removed. If omitted both, member additions
|
||||||
and removals, will be matched.
|
and removals, will be matched.
|
||||||
:param func: A Callable (async or not) function that should accept the event as input
|
:param func: A Callable function that should accept the event as input
|
||||||
parameter, and return a bool value indicating whether the event
|
parameter, and return a bool value indicating whether the event
|
||||||
should be dispatched or not.
|
should be dispatched or not.
|
||||||
"""
|
"""
|
||||||
@@ -201,10 +195,10 @@ class MemberListChanged(EventFilter):
|
|||||||
return (self.added, self.func) == (other.added, other.func)
|
return (self.added, self.func) == (other.added, other.func)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
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 await self._call_func(event)
|
return self._call_func(event)
|
||||||
|
|
||||||
|
|
||||||
class GroupImageChanged(EventFilter):
|
class GroupImageChanged(EventFilter):
|
||||||
@@ -216,7 +210,7 @@ class GroupImageChanged(EventFilter):
|
|||||||
:param deleted: If set to True only match if the image was deleted, if set to False
|
:param deleted: If set to True only match if the image was deleted, if set to False
|
||||||
only match if a new image was set. If omitted both, image changes and
|
only match if a new image was set. If omitted both, image changes and
|
||||||
removals, will be matched.
|
removals, will be matched.
|
||||||
:param func: A Callable (async or not) function that should accept the event as input
|
:param func: A Callable function that should accept the event as input
|
||||||
parameter, and return a bool value indicating whether the event
|
parameter, and return a bool value indicating whether the event
|
||||||
should be dispatched or not.
|
should be dispatched or not.
|
||||||
"""
|
"""
|
||||||
@@ -233,10 +227,10 @@ class GroupImageChanged(EventFilter):
|
|||||||
return (self.deleted, self.func) == (other.deleted, other.func)
|
return (self.deleted, self.func) == (other.deleted, other.func)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
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 await self._call_func(event)
|
return self._call_func(event)
|
||||||
|
|
||||||
|
|
||||||
class GroupNameChanged(EventFilter):
|
class GroupNameChanged(EventFilter):
|
||||||
@@ -245,7 +239,7 @@ class GroupNameChanged(EventFilter):
|
|||||||
Warning: registering a handler for this event will cause the messages
|
Warning: registering a handler for this event will cause the messages
|
||||||
to be marked as read. Its usage is mainly intended for bots.
|
to be marked as read. Its usage is mainly intended for bots.
|
||||||
|
|
||||||
:param func: A Callable (async or not) function that should accept the event as input
|
:param func: A Callable function that should accept the event as input
|
||||||
parameter, and return a bool value indicating whether the event
|
parameter, and return a bool value indicating whether the event
|
||||||
should be dispatched or not.
|
should be dispatched or not.
|
||||||
"""
|
"""
|
||||||
@@ -258,8 +252,8 @@ class GroupNameChanged(EventFilter):
|
|||||||
return self.func == other.func
|
return self.func == other.func
|
||||||
return False
|
return False
|
||||||
|
|
||||||
async def filter(self, event: "AttrDict") -> bool:
|
def filter(self, event: "AttrDict") -> bool:
|
||||||
return await self._call_func(event)
|
return self._call_func(event)
|
||||||
|
|
||||||
|
|
||||||
class HookCollection:
|
class HookCollection:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import TYPE_CHECKING, Union
|
from typing import TYPE_CHECKING, Optional, Union
|
||||||
|
|
||||||
from ._utils import AttrDict
|
from ._utils import AttrDict
|
||||||
from .contact import Contact
|
from .contact import Contact
|
||||||
@@ -21,32 +21,39 @@ class Message:
|
|||||||
def _rpc(self) -> "Rpc":
|
def _rpc(self) -> "Rpc":
|
||||||
return self.account._rpc
|
return self.account._rpc
|
||||||
|
|
||||||
async def send_reaction(self, *reaction: str):
|
def send_reaction(self, *reaction: str):
|
||||||
"""Send a reaction to this message."""
|
"""Send a reaction to this message."""
|
||||||
await self._rpc.send_reaction(self.account.id, self.id, reaction)
|
self._rpc.send_reaction(self.account.id, self.id, reaction)
|
||||||
|
|
||||||
async def get_snapshot(self) -> AttrDict:
|
def get_snapshot(self) -> AttrDict:
|
||||||
"""Get a snapshot with the properties of this message."""
|
"""Get a snapshot with the properties of this message."""
|
||||||
from .chat import Chat
|
from .chat import Chat
|
||||||
|
|
||||||
snapshot = AttrDict(await self._rpc.get_message(self.account.id, self.id))
|
snapshot = AttrDict(self._rpc.get_message(self.account.id, self.id))
|
||||||
snapshot["chat"] = Chat(self.account, snapshot.chat_id)
|
snapshot["chat"] = Chat(self.account, snapshot.chat_id)
|
||||||
snapshot["sender"] = Contact(self.account, snapshot.from_id)
|
snapshot["sender"] = Contact(self.account, snapshot.from_id)
|
||||||
snapshot["message"] = self
|
snapshot["message"] = self
|
||||||
return snapshot
|
return snapshot
|
||||||
|
|
||||||
async def mark_seen(self) -> None:
|
def get_reactions(self) -> Optional[AttrDict]:
|
||||||
"""Mark the message as seen."""
|
"""Get message reactions."""
|
||||||
await self._rpc.markseen_msgs(self.account.id, [self.id])
|
reactions = self._rpc.get_message_reactions(self.account.id, self.id)
|
||||||
|
if reactions:
|
||||||
|
return AttrDict(reactions)
|
||||||
|
return None
|
||||||
|
|
||||||
async def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
def mark_seen(self) -> None:
|
||||||
|
"""Mark the message as seen."""
|
||||||
|
self._rpc.markseen_msgs(self.account.id, [self.id])
|
||||||
|
|
||||||
|
def send_webxdc_status_update(self, update: Union[dict, str], description: str) -> None:
|
||||||
"""Send a webxdc status update. This message must be a webxdc."""
|
"""Send a webxdc status update. This message must be a webxdc."""
|
||||||
if not isinstance(update, str):
|
if not isinstance(update, str):
|
||||||
update = json.dumps(update)
|
update = json.dumps(update)
|
||||||
await self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
|
self._rpc.send_webxdc_status_update(self.account.id, self.id, update, description)
|
||||||
|
|
||||||
async def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
|
def get_webxdc_status_updates(self, last_known_serial: int = 0) -> list:
|
||||||
return json.loads(await self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
|
return json.loads(self._rpc.get_webxdc_status_updates(self.account.id, self.id, last_known_serial))
|
||||||
|
|
||||||
async def get_webxdc_info(self) -> dict:
|
def get_webxdc_info(self) -> dict:
|
||||||
return await self._rpc.get_webxdc_info(self.account.id, self.id)
|
return self._rpc.get_webxdc_info(self.account.id, self.id)
|
||||||
|
|||||||
@@ -1,71 +1,67 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
|
import urllib.request
|
||||||
from typing import AsyncGenerator, List, Optional
|
from typing import AsyncGenerator, List, Optional
|
||||||
|
|
||||||
import aiohttp
|
import pytest
|
||||||
import pytest_asyncio
|
|
||||||
|
|
||||||
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
|
from . import Account, AttrDict, Bot, Client, DeltaChat, EventType, Message
|
||||||
from .rpc import Rpc
|
from .rpc import Rpc
|
||||||
|
|
||||||
|
|
||||||
async def get_temp_credentials() -> dict:
|
def get_temp_credentials() -> dict:
|
||||||
url = os.getenv("DCC_NEW_TMP_EMAIL")
|
url = os.getenv("DCC_NEW_TMP_EMAIL")
|
||||||
assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set"
|
assert url, "Failed to get online account, DCC_NEW_TMP_EMAIL is not set"
|
||||||
|
|
||||||
# Replace default 5 minute timeout with a 1 minute timeout.
|
request = urllib.request.Request(url, method="POST")
|
||||||
timeout = aiohttp.ClientTimeout(total=60)
|
with urllib.request.urlopen(request, timeout=60) as f:
|
||||||
async with aiohttp.ClientSession() as session:
|
return json.load(f)
|
||||||
async with session.post(url, timeout=timeout) as response:
|
|
||||||
return json.loads(await response.text())
|
|
||||||
|
|
||||||
|
|
||||||
class ACFactory:
|
class ACFactory:
|
||||||
def __init__(self, deltachat: DeltaChat) -> None:
|
def __init__(self, deltachat: DeltaChat) -> None:
|
||||||
self.deltachat = deltachat
|
self.deltachat = deltachat
|
||||||
|
|
||||||
async def get_unconfigured_account(self) -> Account:
|
def get_unconfigured_account(self) -> Account:
|
||||||
return await self.deltachat.add_account()
|
return self.deltachat.add_account()
|
||||||
|
|
||||||
async def get_unconfigured_bot(self) -> Bot:
|
def get_unconfigured_bot(self) -> Bot:
|
||||||
return Bot(await self.get_unconfigured_account())
|
return Bot(self.get_unconfigured_account())
|
||||||
|
|
||||||
async def new_preconfigured_account(self) -> Account:
|
def new_preconfigured_account(self) -> Account:
|
||||||
"""Make a new account with configuration options set, but configuration not started."""
|
"""Make a new account with configuration options set, but configuration not started."""
|
||||||
credentials = await get_temp_credentials()
|
credentials = get_temp_credentials()
|
||||||
account = await self.get_unconfigured_account()
|
account = self.get_unconfigured_account()
|
||||||
await account.set_config("addr", credentials["email"])
|
account.set_config("addr", credentials["email"])
|
||||||
await account.set_config("mail_pw", credentials["password"])
|
account.set_config("mail_pw", credentials["password"])
|
||||||
assert not await account.is_configured()
|
assert not account.is_configured()
|
||||||
return account
|
return account
|
||||||
|
|
||||||
async def new_configured_account(self) -> Account:
|
def new_configured_account(self) -> Account:
|
||||||
account = await self.new_preconfigured_account()
|
account = self.new_preconfigured_account()
|
||||||
await account.configure()
|
account.configure()
|
||||||
assert await account.is_configured()
|
assert account.is_configured()
|
||||||
return account
|
return account
|
||||||
|
|
||||||
async def new_configured_bot(self) -> Bot:
|
def new_configured_bot(self) -> Bot:
|
||||||
credentials = await get_temp_credentials()
|
credentials = get_temp_credentials()
|
||||||
bot = await self.get_unconfigured_bot()
|
bot = self.get_unconfigured_bot()
|
||||||
await bot.configure(credentials["email"], credentials["password"])
|
bot.configure(credentials["email"], credentials["password"])
|
||||||
return bot
|
return bot
|
||||||
|
|
||||||
async def get_online_account(self) -> Account:
|
def get_online_account(self) -> Account:
|
||||||
account = await self.new_configured_account()
|
account = self.new_configured_account()
|
||||||
await account.start_io()
|
account.start_io()
|
||||||
while True:
|
while True:
|
||||||
event = await account.wait_for_event()
|
event = account.wait_for_event()
|
||||||
print(event)
|
if event.kind == EventType.IMAP_INBOX_IDLE:
|
||||||
if event.type == EventType.IMAP_INBOX_IDLE:
|
|
||||||
break
|
break
|
||||||
return account
|
return account
|
||||||
|
|
||||||
async def get_online_accounts(self, num: int) -> List[Account]:
|
def get_online_accounts(self, num: int) -> List[Account]:
|
||||||
return await asyncio.gather(*[self.get_online_account() for _ in range(num)])
|
return [self.get_online_account() for _ in range(num)]
|
||||||
|
|
||||||
async def send_message(
|
def send_message(
|
||||||
self,
|
self,
|
||||||
to_account: Account,
|
to_account: Account,
|
||||||
from_account: Optional[Account] = None,
|
from_account: Optional[Account] = None,
|
||||||
@@ -74,16 +70,16 @@ class ACFactory:
|
|||||||
group: Optional[str] = None,
|
group: Optional[str] = None,
|
||||||
) -> Message:
|
) -> Message:
|
||||||
if not from_account:
|
if not from_account:
|
||||||
from_account = (await self.get_online_accounts(1))[0]
|
from_account = (self.get_online_accounts(1))[0]
|
||||||
to_contact = await from_account.create_contact(await to_account.get_config("addr"))
|
to_contact = from_account.create_contact(to_account.get_config("addr"))
|
||||||
if group:
|
if group:
|
||||||
to_chat = await from_account.create_group(group)
|
to_chat = from_account.create_group(group)
|
||||||
await to_chat.add_contact(to_contact)
|
to_chat.add_contact(to_contact)
|
||||||
else:
|
else:
|
||||||
to_chat = await to_contact.create_chat()
|
to_chat = to_contact.create_chat()
|
||||||
return await to_chat.send_message(text=text, file=file)
|
return to_chat.send_message(text=text, file=file)
|
||||||
|
|
||||||
async def process_message(
|
def process_message(
|
||||||
self,
|
self,
|
||||||
to_client: Client,
|
to_client: Client,
|
||||||
from_account: Optional[Account] = None,
|
from_account: Optional[Account] = None,
|
||||||
@@ -91,7 +87,7 @@ class ACFactory:
|
|||||||
file: Optional[str] = None,
|
file: Optional[str] = None,
|
||||||
group: Optional[str] = None,
|
group: Optional[str] = None,
|
||||||
) -> AttrDict:
|
) -> AttrDict:
|
||||||
await self.send_message(
|
self.send_message(
|
||||||
to_account=to_client.account,
|
to_account=to_client.account,
|
||||||
from_account=from_account,
|
from_account=from_account,
|
||||||
text=text,
|
text=text,
|
||||||
@@ -99,16 +95,16 @@ class ACFactory:
|
|||||||
group=group,
|
group=group,
|
||||||
)
|
)
|
||||||
|
|
||||||
return await to_client.run_until(lambda e: e.type == EventType.INCOMING_MSG)
|
return to_client.run_until(lambda e: e.kind == EventType.INCOMING_MSG)
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture
|
@pytest.fixture()
|
||||||
async def rpc(tmp_path) -> AsyncGenerator:
|
def rpc(tmp_path) -> AsyncGenerator:
|
||||||
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
|
rpc_server = Rpc(accounts_dir=str(tmp_path / "accounts"))
|
||||||
async with rpc_server:
|
with rpc_server:
|
||||||
yield rpc_server
|
yield rpc_server
|
||||||
|
|
||||||
|
|
||||||
@pytest_asyncio.fixture
|
@pytest.fixture()
|
||||||
async def acfactory(rpc) -> AsyncGenerator:
|
def acfactory(rpc) -> AsyncGenerator:
|
||||||
yield ACFactory(DeltaChat(rpc))
|
return ACFactory(DeltaChat(rpc))
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import asyncio
|
|
||||||
import json
|
import json
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from queue import Queue
|
||||||
|
from threading import Event, Thread
|
||||||
from typing import Any, Dict, Optional
|
from typing import Any, Dict, Optional
|
||||||
|
|
||||||
|
|
||||||
@@ -10,7 +14,7 @@ class JsonRpcError(Exception):
|
|||||||
|
|
||||||
class Rpc:
|
class Rpc:
|
||||||
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
def __init__(self, accounts_dir: Optional[str] = None, **kwargs):
|
||||||
"""The given arguments will be passed to asyncio.create_subprocess_exec()"""
|
"""The given arguments will be passed to subprocess.Popen()"""
|
||||||
if accounts_dir:
|
if accounts_dir:
|
||||||
kwargs["env"] = {
|
kwargs["env"] = {
|
||||||
**kwargs.get("env", os.environ),
|
**kwargs.get("env", os.environ),
|
||||||
@@ -18,81 +22,142 @@ class Rpc:
|
|||||||
}
|
}
|
||||||
|
|
||||||
self._kwargs = kwargs
|
self._kwargs = kwargs
|
||||||
self.process: asyncio.subprocess.Process
|
self.process: subprocess.Popen
|
||||||
self.id: int
|
self.id: int
|
||||||
self.event_queues: Dict[int, asyncio.Queue]
|
self.event_queues: Dict[int, Queue]
|
||||||
# Map from request ID to `asyncio.Future` returning the response.
|
# Map from request ID to `threading.Event`.
|
||||||
self.request_events: Dict[int, asyncio.Future]
|
self.request_events: Dict[int, Event]
|
||||||
self.reader_task: asyncio.Task
|
# Map from request ID to the result.
|
||||||
|
self.request_results: Dict[int, Any]
|
||||||
|
self.request_queue: Queue[Any]
|
||||||
|
self.closing: bool
|
||||||
|
self.reader_thread: Thread
|
||||||
|
self.writer_thread: Thread
|
||||||
|
self.events_thread: Thread
|
||||||
|
|
||||||
async def start(self) -> None:
|
def start(self) -> None:
|
||||||
self.process = await asyncio.create_subprocess_exec(
|
if sys.version_info >= (3, 11):
|
||||||
"deltachat-rpc-server",
|
self.process = subprocess.Popen(
|
||||||
stdin=asyncio.subprocess.PIPE,
|
"deltachat-rpc-server",
|
||||||
stdout=asyncio.subprocess.PIPE,
|
stdin=subprocess.PIPE,
|
||||||
**self._kwargs,
|
stdout=subprocess.PIPE,
|
||||||
)
|
# Prevent subprocess from capturing SIGINT.
|
||||||
|
process_group=0,
|
||||||
|
**self._kwargs,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.process = subprocess.Popen(
|
||||||
|
"deltachat-rpc-server",
|
||||||
|
stdin=subprocess.PIPE,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
# `process_group` is not supported before Python 3.11.
|
||||||
|
preexec_fn=os.setpgrp, # noqa: PLW1509
|
||||||
|
**self._kwargs,
|
||||||
|
)
|
||||||
self.id = 0
|
self.id = 0
|
||||||
self.event_queues = {}
|
self.event_queues = {}
|
||||||
self.request_events = {}
|
self.request_events = {}
|
||||||
self.reader_task = asyncio.create_task(self.reader_loop())
|
self.request_results = {}
|
||||||
|
self.request_queue = Queue()
|
||||||
|
self.closing = False
|
||||||
|
self.reader_thread = Thread(target=self.reader_loop)
|
||||||
|
self.reader_thread.start()
|
||||||
|
self.writer_thread = Thread(target=self.writer_loop)
|
||||||
|
self.writer_thread.start()
|
||||||
|
self.events_thread = Thread(target=self.events_loop)
|
||||||
|
self.events_thread.start()
|
||||||
|
|
||||||
async def close(self) -> None:
|
def close(self) -> None:
|
||||||
"""Terminate RPC server process and wait until the reader loop finishes."""
|
"""Terminate RPC server process and wait until the reader loop finishes."""
|
||||||
self.process.terminate()
|
self.closing = True
|
||||||
await self.reader_task
|
self.stop_io_for_all_accounts()
|
||||||
|
self.events_thread.join()
|
||||||
|
self.process.stdin.close()
|
||||||
|
self.reader_thread.join()
|
||||||
|
self.request_queue.put(None)
|
||||||
|
self.writer_thread.join()
|
||||||
|
|
||||||
async def __aenter__(self):
|
def __enter__(self):
|
||||||
await self.start()
|
self.start()
|
||||||
return self
|
return self
|
||||||
|
|
||||||
async def __aexit__(self, _exc_type, _exc, _tb):
|
def __exit__(self, _exc_type, _exc, _tb):
|
||||||
await self.close()
|
self.close()
|
||||||
|
|
||||||
async def reader_loop(self) -> None:
|
def reader_loop(self) -> None:
|
||||||
while True:
|
try:
|
||||||
line = await self.process.stdout.readline() # noqa
|
while True:
|
||||||
if not line: # EOF
|
line = self.process.stdout.readline()
|
||||||
break
|
if not line: # EOF
|
||||||
response = json.loads(line)
|
break
|
||||||
if "id" in response:
|
response = json.loads(line)
|
||||||
fut = self.request_events.pop(response["id"])
|
if "id" in response:
|
||||||
fut.set_result(response)
|
response_id = response["id"]
|
||||||
elif response["method"] == "event":
|
event = self.request_events.pop(response_id)
|
||||||
# An event notification.
|
self.request_results[response_id] = response
|
||||||
params = response["params"]
|
event.set()
|
||||||
account_id = params["contextId"]
|
else:
|
||||||
if account_id not in self.event_queues:
|
logging.warning("Got a response without ID: %s", response)
|
||||||
self.event_queues[account_id] = asyncio.Queue()
|
except Exception:
|
||||||
await self.event_queues[account_id].put(params["event"])
|
# Log an exception if the reader loop dies.
|
||||||
else:
|
logging.exception("Exception in the reader loop")
|
||||||
print(response)
|
|
||||||
|
|
||||||
async def wait_for_event(self, account_id: int) -> Optional[dict]:
|
def writer_loop(self) -> None:
|
||||||
|
"""Writer loop ensuring only a single thread writes requests."""
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
request = self.request_queue.get()
|
||||||
|
if not request:
|
||||||
|
break
|
||||||
|
data = (json.dumps(request) + "\n").encode()
|
||||||
|
self.process.stdin.write(data)
|
||||||
|
self.process.stdin.flush()
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# Log an exception if the writer loop dies.
|
||||||
|
logging.exception("Exception in the writer loop")
|
||||||
|
|
||||||
|
def get_queue(self, account_id: int) -> Queue:
|
||||||
|
if account_id not in self.event_queues:
|
||||||
|
self.event_queues[account_id] = Queue()
|
||||||
|
return self.event_queues[account_id]
|
||||||
|
|
||||||
|
def events_loop(self) -> None:
|
||||||
|
"""Requests new events and distributes them between queues."""
|
||||||
|
try:
|
||||||
|
while True:
|
||||||
|
if self.closing:
|
||||||
|
return
|
||||||
|
event = self.get_next_event()
|
||||||
|
account_id = event["contextId"]
|
||||||
|
queue = self.get_queue(account_id)
|
||||||
|
queue.put(event["event"])
|
||||||
|
except Exception:
|
||||||
|
# Log an exception if the event loop dies.
|
||||||
|
logging.exception("Exception in the event loop")
|
||||||
|
|
||||||
|
def wait_for_event(self, account_id: int) -> Optional[dict]:
|
||||||
"""Waits for the next event from the given account and returns it."""
|
"""Waits for the next event from the given account and returns it."""
|
||||||
if account_id in self.event_queues:
|
queue = self.get_queue(account_id)
|
||||||
return await self.event_queues[account_id].get()
|
return queue.get()
|
||||||
return None
|
|
||||||
|
|
||||||
def __getattr__(self, attr: str):
|
def __getattr__(self, attr: str):
|
||||||
async def method(*args, **kwargs) -> Any:
|
def method(*args) -> Any:
|
||||||
self.id += 1
|
self.id += 1
|
||||||
request_id = self.id
|
request_id = self.id
|
||||||
|
|
||||||
assert not (args and kwargs), "Mixing positional and keyword arguments"
|
|
||||||
|
|
||||||
request = {
|
request = {
|
||||||
"jsonrpc": "2.0",
|
"jsonrpc": "2.0",
|
||||||
"method": attr,
|
"method": attr,
|
||||||
"params": kwargs or args,
|
"params": args,
|
||||||
"id": self.id,
|
"id": self.id,
|
||||||
}
|
}
|
||||||
data = (json.dumps(request) + "\n").encode()
|
event = Event()
|
||||||
self.process.stdin.write(data) # noqa
|
self.request_events[request_id] = event
|
||||||
loop = asyncio.get_running_loop()
|
self.request_queue.put(request)
|
||||||
fut = loop.create_future()
|
event.wait()
|
||||||
self.request_events[request_id] = fut
|
|
||||||
response = await fut
|
response = self.request_results.pop(request_id)
|
||||||
if "error" in response:
|
if "error" in response:
|
||||||
raise JsonRpcError(response["error"])
|
raise JsonRpcError(response["error"])
|
||||||
if "result" in response:
|
if "result" in response:
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import asyncio
|
import concurrent.futures
|
||||||
|
import json
|
||||||
|
import subprocess
|
||||||
from unittest.mock import MagicMock
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
@@ -6,26 +8,26 @@ from deltachat_rpc_client import EventType, events
|
|||||||
from deltachat_rpc_client.rpc import JsonRpcError
|
from deltachat_rpc_client.rpc import JsonRpcError
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_system_info(rpc) -> None:
|
||||||
async def test_system_info(rpc) -> None:
|
system_info = rpc.get_system_info()
|
||||||
system_info = await rpc.get_system_info()
|
|
||||||
assert "arch" in system_info
|
assert "arch" in system_info
|
||||||
assert "deltachat_core_version" in system_info
|
assert "deltachat_core_version" in system_info
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_sleep(rpc) -> None:
|
||||||
async def test_sleep(rpc) -> None:
|
|
||||||
"""Test that long-running task does not block short-running task from completion."""
|
"""Test that long-running task does not block short-running task from completion."""
|
||||||
sleep_5_task = asyncio.create_task(rpc.sleep(5.0))
|
with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
|
||||||
sleep_3_task = asyncio.create_task(rpc.sleep(3.0))
|
sleep_5_future = executor.submit(rpc.sleep, 5.0)
|
||||||
done, pending = await asyncio.wait([sleep_5_task, sleep_3_task], return_when=asyncio.FIRST_COMPLETED)
|
sleep_3_future = executor.submit(rpc.sleep, 3.0)
|
||||||
assert sleep_3_task in done
|
done, pending = concurrent.futures.wait(
|
||||||
assert sleep_5_task in pending
|
[sleep_5_future, sleep_3_future],
|
||||||
sleep_5_task.cancel()
|
return_when=concurrent.futures.FIRST_COMPLETED,
|
||||||
|
)
|
||||||
|
assert sleep_3_future in done
|
||||||
|
assert sleep_5_future in pending
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_email_address_validity(rpc) -> None:
|
||||||
async def test_email_address_validity(rpc) -> None:
|
|
||||||
valid_addresses = [
|
valid_addresses = [
|
||||||
"email@example.com",
|
"email@example.com",
|
||||||
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
|
"36aa165ae3406424e0c61af17700f397cad3fe8ab83d682d0bddf3338a5dd52e@yggmail@yggmail",
|
||||||
@@ -33,17 +35,16 @@ async def test_email_address_validity(rpc) -> None:
|
|||||||
invalid_addresses = ["email@", "example.com", "emai221"]
|
invalid_addresses = ["email@", "example.com", "emai221"]
|
||||||
|
|
||||||
for addr in valid_addresses:
|
for addr in valid_addresses:
|
||||||
assert await rpc.check_email_validity(addr)
|
assert rpc.check_email_validity(addr)
|
||||||
for addr in invalid_addresses:
|
for addr in invalid_addresses:
|
||||||
assert not await rpc.check_email_validity(addr)
|
assert not rpc.check_email_validity(addr)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_acfactory(acfactory) -> None:
|
||||||
async def test_acfactory(acfactory) -> None:
|
account = acfactory.new_configured_account()
|
||||||
account = await acfactory.new_configured_account()
|
|
||||||
while True:
|
while True:
|
||||||
event = await account.wait_for_event()
|
event = account.wait_for_event()
|
||||||
if event.type == EventType.CONFIGURE_PROGRESS:
|
if event.kind == EventType.CONFIGURE_PROGRESS:
|
||||||
assert event.progress != 0 # Progress 0 indicates error.
|
assert event.progress != 0 # Progress 0 indicates error.
|
||||||
if event.progress == 1000: # Success
|
if event.progress == 1000: # Success
|
||||||
break
|
break
|
||||||
@@ -52,234 +53,241 @@ async def test_acfactory(acfactory) -> None:
|
|||||||
print("Successful configuration")
|
print("Successful configuration")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_configure_starttls(acfactory) -> None:
|
||||||
async def test_configure_starttls(acfactory) -> None:
|
account = acfactory.new_preconfigured_account()
|
||||||
account = await acfactory.new_preconfigured_account()
|
|
||||||
|
|
||||||
# Use STARTTLS
|
# Use STARTTLS
|
||||||
await account.set_config("mail_security", "2")
|
account.set_config("mail_security", "2")
|
||||||
await account.set_config("send_security", "2")
|
account.set_config("send_security", "2")
|
||||||
await account.configure()
|
account.configure()
|
||||||
assert await account.is_configured()
|
assert account.is_configured()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_account(acfactory) -> None:
|
||||||
async def test_account(acfactory) -> None:
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
alice, bob = await acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
bob_addr = await bob.get_config("addr")
|
bob_addr = bob.get_config("addr")
|
||||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
await alice_chat_bob.send_text("Hello!")
|
alice_chat_bob.send_text("Hello!")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
event = await bob.wait_for_event()
|
event = bob.wait_for_event()
|
||||||
if event.type == EventType.INCOMING_MSG:
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
chat_id = event.chat_id
|
chat_id = event.chat_id
|
||||||
msg_id = event.msg_id
|
msg_id = event.msg_id
|
||||||
break
|
break
|
||||||
|
|
||||||
message = bob.get_message_by_id(msg_id)
|
message = bob.get_message_by_id(msg_id)
|
||||||
snapshot = await message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
assert snapshot.chat_id == chat_id
|
assert snapshot.chat_id == chat_id
|
||||||
assert snapshot.text == "Hello!"
|
assert snapshot.text == "Hello!"
|
||||||
await bob.mark_seen_messages([message])
|
bob.mark_seen_messages([message])
|
||||||
|
|
||||||
assert alice != bob
|
assert alice != bob
|
||||||
assert repr(alice)
|
assert repr(alice)
|
||||||
assert (await alice.get_info()).level
|
assert alice.get_info().level
|
||||||
assert await alice.get_size()
|
assert alice.get_size()
|
||||||
assert await alice.is_configured()
|
assert alice.is_configured()
|
||||||
assert not await alice.get_avatar()
|
assert not alice.get_avatar()
|
||||||
assert await alice.get_contact_by_addr(bob_addr) == alice_contact_bob
|
assert alice.get_contact_by_addr(bob_addr) == alice_contact_bob
|
||||||
assert await alice.get_contacts()
|
assert alice.get_contacts()
|
||||||
assert await alice.get_contacts(snapshot=True)
|
assert alice.get_contacts(snapshot=True)
|
||||||
assert alice.self_contact
|
assert alice.self_contact
|
||||||
assert await alice.get_chatlist()
|
assert alice.get_chatlist()
|
||||||
assert await alice.get_chatlist(snapshot=True)
|
assert alice.get_chatlist(snapshot=True)
|
||||||
assert await alice.get_qr_code()
|
assert alice.get_qr_code()
|
||||||
assert await alice.get_fresh_messages()
|
assert alice.get_fresh_messages()
|
||||||
assert await alice.get_next_messages()
|
assert alice.get_next_messages()
|
||||||
|
|
||||||
group = await alice.create_group("test group")
|
# Test sending empty message.
|
||||||
await group.add_contact(alice_contact_bob)
|
assert len(bob.wait_next_messages()) == 0
|
||||||
group_msg = await group.send_message(text="hello")
|
alice_chat_bob.send_text("")
|
||||||
|
messages = bob.wait_next_messages()
|
||||||
|
assert len(messages) == 1
|
||||||
|
message = messages[0]
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert snapshot.text == ""
|
||||||
|
bob.mark_seen_messages([message])
|
||||||
|
|
||||||
|
group = alice.create_group("test group")
|
||||||
|
group.add_contact(alice_contact_bob)
|
||||||
|
group_msg = group.send_message(text="hello")
|
||||||
assert group_msg == alice.get_message_by_id(group_msg.id)
|
assert group_msg == alice.get_message_by_id(group_msg.id)
|
||||||
assert group == alice.get_chat_by_id(group.id)
|
assert group == alice.get_chat_by_id(group.id)
|
||||||
await alice.delete_messages([group_msg])
|
alice.delete_messages([group_msg])
|
||||||
|
|
||||||
await alice.set_config("selfstatus", "test")
|
alice.set_config("selfstatus", "test")
|
||||||
assert await alice.get_config("selfstatus") == "test"
|
assert alice.get_config("selfstatus") == "test"
|
||||||
await alice.update_config(selfstatus="test2")
|
alice.update_config(selfstatus="test2")
|
||||||
assert await alice.get_config("selfstatus") == "test2"
|
assert alice.get_config("selfstatus") == "test2"
|
||||||
|
|
||||||
assert not await alice.get_blocked_contacts()
|
assert not alice.get_blocked_contacts()
|
||||||
await alice_contact_bob.block()
|
alice_contact_bob.block()
|
||||||
blocked_contacts = await alice.get_blocked_contacts()
|
blocked_contacts = alice.get_blocked_contacts()
|
||||||
assert blocked_contacts
|
assert blocked_contacts
|
||||||
assert blocked_contacts[0].contact == alice_contact_bob
|
assert blocked_contacts[0].contact == alice_contact_bob
|
||||||
|
|
||||||
await bob.remove()
|
bob.remove()
|
||||||
await alice.stop_io()
|
alice.stop_io()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_chat(acfactory) -> None:
|
||||||
async def test_chat(acfactory) -> None:
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
alice, bob = await acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
bob_addr = await bob.get_config("addr")
|
bob_addr = bob.get_config("addr")
|
||||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
await alice_chat_bob.send_text("Hello!")
|
alice_chat_bob.send_text("Hello!")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
event = await bob.wait_for_event()
|
event = bob.wait_for_event()
|
||||||
if event.type == EventType.INCOMING_MSG:
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
chat_id = event.chat_id
|
chat_id = event.chat_id
|
||||||
msg_id = event.msg_id
|
msg_id = event.msg_id
|
||||||
break
|
break
|
||||||
message = bob.get_message_by_id(msg_id)
|
message = bob.get_message_by_id(msg_id)
|
||||||
snapshot = await message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
assert snapshot.chat_id == chat_id
|
assert snapshot.chat_id == chat_id
|
||||||
assert snapshot.text == "Hello!"
|
assert snapshot.text == "Hello!"
|
||||||
bob_chat_alice = bob.get_chat_by_id(chat_id)
|
bob_chat_alice = bob.get_chat_by_id(chat_id)
|
||||||
|
|
||||||
assert alice_chat_bob != bob_chat_alice
|
assert alice_chat_bob != bob_chat_alice
|
||||||
assert repr(alice_chat_bob)
|
assert repr(alice_chat_bob)
|
||||||
await alice_chat_bob.delete()
|
alice_chat_bob.delete()
|
||||||
assert not await bob_chat_alice.can_send()
|
assert not bob_chat_alice.can_send()
|
||||||
await bob_chat_alice.accept()
|
bob_chat_alice.accept()
|
||||||
assert await bob_chat_alice.can_send()
|
assert bob_chat_alice.can_send()
|
||||||
await bob_chat_alice.block()
|
bob_chat_alice.block()
|
||||||
bob_chat_alice = await snapshot.sender.create_chat()
|
bob_chat_alice = snapshot.sender.create_chat()
|
||||||
await bob_chat_alice.mute()
|
bob_chat_alice.mute()
|
||||||
await bob_chat_alice.unmute()
|
bob_chat_alice.unmute()
|
||||||
await bob_chat_alice.pin()
|
bob_chat_alice.pin()
|
||||||
await bob_chat_alice.unpin()
|
bob_chat_alice.unpin()
|
||||||
await bob_chat_alice.archive()
|
bob_chat_alice.archive()
|
||||||
await bob_chat_alice.unarchive()
|
bob_chat_alice.unarchive()
|
||||||
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
|
with pytest.raises(JsonRpcError): # can't set name for 1:1 chats
|
||||||
await bob_chat_alice.set_name("test")
|
bob_chat_alice.set_name("test")
|
||||||
await bob_chat_alice.set_ephemeral_timer(300)
|
bob_chat_alice.set_ephemeral_timer(300)
|
||||||
await bob_chat_alice.get_encryption_info()
|
bob_chat_alice.get_encryption_info()
|
||||||
|
|
||||||
group = await alice.create_group("test group")
|
group = alice.create_group("test group")
|
||||||
await group.add_contact(alice_contact_bob)
|
group.add_contact(alice_contact_bob)
|
||||||
await group.get_qr_code()
|
group.get_qr_code()
|
||||||
|
|
||||||
snapshot = await group.get_basic_snapshot()
|
snapshot = group.get_basic_snapshot()
|
||||||
assert snapshot.name == "test group"
|
assert snapshot.name == "test group"
|
||||||
await group.set_name("new name")
|
group.set_name("new name")
|
||||||
snapshot = await group.get_full_snapshot()
|
snapshot = group.get_full_snapshot()
|
||||||
assert snapshot.name == "new name"
|
assert snapshot.name == "new name"
|
||||||
|
|
||||||
msg = await group.send_message(text="hi")
|
msg = group.send_message(text="hi")
|
||||||
assert (await msg.get_snapshot()).text == "hi"
|
assert (msg.get_snapshot()).text == "hi"
|
||||||
await group.forward_messages([msg])
|
group.forward_messages([msg])
|
||||||
|
|
||||||
await group.set_draft(text="test draft")
|
group.set_draft(text="test draft")
|
||||||
draft = await group.get_draft()
|
draft = group.get_draft()
|
||||||
assert draft.text == "test draft"
|
assert draft.text == "test draft"
|
||||||
await group.remove_draft()
|
group.remove_draft()
|
||||||
assert not await group.get_draft()
|
assert not group.get_draft()
|
||||||
|
|
||||||
assert await group.get_messages()
|
assert group.get_messages()
|
||||||
await group.get_fresh_message_count()
|
group.get_fresh_message_count()
|
||||||
await group.mark_noticed()
|
group.mark_noticed()
|
||||||
assert await group.get_contacts()
|
assert group.get_contacts()
|
||||||
await group.remove_contact(alice_chat_bob)
|
group.remove_contact(alice_chat_bob)
|
||||||
await group.get_locations()
|
group.get_locations()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_contact(acfactory) -> None:
|
||||||
async def test_contact(acfactory) -> None:
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
alice, bob = await acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
bob_addr = await bob.get_config("addr")
|
bob_addr = bob.get_config("addr")
|
||||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
|
|
||||||
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
|
assert alice_contact_bob == alice.get_contact_by_id(alice_contact_bob.id)
|
||||||
assert repr(alice_contact_bob)
|
assert repr(alice_contact_bob)
|
||||||
await alice_contact_bob.block()
|
alice_contact_bob.block()
|
||||||
await alice_contact_bob.unblock()
|
alice_contact_bob.unblock()
|
||||||
await alice_contact_bob.set_name("new name")
|
alice_contact_bob.set_name("new name")
|
||||||
await alice_contact_bob.get_encryption_info()
|
alice_contact_bob.get_encryption_info()
|
||||||
snapshot = await alice_contact_bob.get_snapshot()
|
snapshot = alice_contact_bob.get_snapshot()
|
||||||
assert snapshot.address == bob_addr
|
assert snapshot.address == bob_addr
|
||||||
await alice_contact_bob.create_chat()
|
alice_contact_bob.create_chat()
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_message(acfactory) -> None:
|
||||||
async def test_message(acfactory) -> None:
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
alice, bob = await acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
bob_addr = await bob.get_config("addr")
|
bob_addr = bob.get_config("addr")
|
||||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
await alice_chat_bob.send_text("Hello!")
|
alice_chat_bob.send_text("Hello!")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
event = await bob.wait_for_event()
|
event = bob.wait_for_event()
|
||||||
if event.type == EventType.INCOMING_MSG:
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
chat_id = event.chat_id
|
chat_id = event.chat_id
|
||||||
msg_id = event.msg_id
|
msg_id = event.msg_id
|
||||||
break
|
break
|
||||||
|
|
||||||
message = bob.get_message_by_id(msg_id)
|
message = bob.get_message_by_id(msg_id)
|
||||||
snapshot = await message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
assert snapshot.chat_id == chat_id
|
assert snapshot.chat_id == chat_id
|
||||||
assert snapshot.text == "Hello!"
|
assert snapshot.text == "Hello!"
|
||||||
assert not snapshot.is_bot
|
assert not snapshot.is_bot
|
||||||
assert repr(message)
|
assert repr(message)
|
||||||
|
|
||||||
with pytest.raises(JsonRpcError): # chat is not accepted
|
with pytest.raises(JsonRpcError): # chat is not accepted
|
||||||
await snapshot.chat.send_text("hi")
|
snapshot.chat.send_text("hi")
|
||||||
await snapshot.chat.accept()
|
snapshot.chat.accept()
|
||||||
await snapshot.chat.send_text("hi")
|
snapshot.chat.send_text("hi")
|
||||||
|
|
||||||
await message.mark_seen()
|
message.mark_seen()
|
||||||
await message.send_reaction("😎")
|
message.send_reaction("😎")
|
||||||
|
reactions = message.get_reactions()
|
||||||
|
assert reactions
|
||||||
|
snapshot = message.get_snapshot()
|
||||||
|
assert reactions == snapshot.reactions
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_is_bot(acfactory) -> None:
|
||||||
async def test_is_bot(acfactory) -> None:
|
|
||||||
"""Test that we can recognize messages submitted by bots."""
|
"""Test that we can recognize messages submitted by bots."""
|
||||||
alice, bob = await acfactory.get_online_accounts(2)
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
|
|
||||||
bob_addr = await bob.get_config("addr")
|
bob_addr = bob.get_config("addr")
|
||||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
|
|
||||||
# Alice becomes a bot.
|
# Alice becomes a bot.
|
||||||
await alice.set_config("bot", "1")
|
alice.set_config("bot", "1")
|
||||||
await alice_chat_bob.send_text("Hello!")
|
alice_chat_bob.send_text("Hello!")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
event = await bob.wait_for_event()
|
event = bob.wait_for_event()
|
||||||
if event.type == EventType.INCOMING_MSG:
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
msg_id = event.msg_id
|
msg_id = event.msg_id
|
||||||
message = bob.get_message_by_id(msg_id)
|
message = bob.get_message_by_id(msg_id)
|
||||||
snapshot = await message.get_snapshot()
|
snapshot = message.get_snapshot()
|
||||||
assert snapshot.chat_id == event.chat_id
|
assert snapshot.chat_id == event.chat_id
|
||||||
assert snapshot.text == "Hello!"
|
assert snapshot.text == "Hello!"
|
||||||
assert snapshot.is_bot
|
assert snapshot.is_bot
|
||||||
break
|
break
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_bot(acfactory) -> None:
|
||||||
async def test_bot(acfactory) -> None:
|
|
||||||
mock = MagicMock()
|
mock = MagicMock()
|
||||||
user = (await acfactory.get_online_accounts(1))[0]
|
user = (acfactory.get_online_accounts(1))[0]
|
||||||
bot = await acfactory.new_configured_bot()
|
bot = acfactory.new_configured_bot()
|
||||||
bot2 = await acfactory.new_configured_bot()
|
bot2 = acfactory.new_configured_bot()
|
||||||
|
|
||||||
assert await bot.is_configured()
|
assert bot.is_configured()
|
||||||
assert await bot.account.get_config("bot") == "1"
|
assert bot.account.get_config("bot") == "1"
|
||||||
|
|
||||||
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
|
hook = lambda e: mock.hook(e.msg_id) and None, events.RawEvent(EventType.INCOMING_MSG)
|
||||||
bot.add_hook(*hook)
|
bot.add_hook(*hook)
|
||||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
|
event = acfactory.process_message(from_account=user, to_client=bot, text="Hello!")
|
||||||
snapshot = await bot.account.get_message_by_id(event.msg_id).get_snapshot()
|
snapshot = bot.account.get_message_by_id(event.msg_id).get_snapshot()
|
||||||
assert not snapshot.is_bot
|
assert not snapshot.is_bot
|
||||||
mock.hook.assert_called_once_with(event.msg_id)
|
mock.hook.assert_called_once_with(event.msg_id)
|
||||||
bot.remove_hook(*hook)
|
bot.remove_hook(*hook)
|
||||||
@@ -291,43 +299,62 @@ async def test_bot(acfactory) -> None:
|
|||||||
hook = track, events.NewMessage(r"hello")
|
hook = track, events.NewMessage(r"hello")
|
||||||
bot.add_hook(*hook)
|
bot.add_hook(*hook)
|
||||||
bot.add_hook(track, events.NewMessage(command="/help"))
|
bot.add_hook(track, events.NewMessage(command="/help"))
|
||||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
event = acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||||
mock.hook.assert_called_with(event.msg_id)
|
mock.hook.assert_called_with(event.msg_id)
|
||||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="hello!")
|
event = acfactory.process_message(from_account=user, to_client=bot, text="hello!")
|
||||||
mock.hook.assert_called_with(event.msg_id)
|
mock.hook.assert_called_with(event.msg_id)
|
||||||
await acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
|
acfactory.process_message(from_account=bot2.account, to_client=bot, text="hello")
|
||||||
assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots
|
assert len(mock.hook.mock_calls) == 2 # bot messages are ignored between bots
|
||||||
await acfactory.process_message(from_account=user, to_client=bot, text="hey!")
|
acfactory.process_message(from_account=user, to_client=bot, text="hey!")
|
||||||
assert len(mock.hook.mock_calls) == 2
|
assert len(mock.hook.mock_calls) == 2
|
||||||
bot.remove_hook(*hook)
|
bot.remove_hook(*hook)
|
||||||
|
|
||||||
mock.hook.reset_mock()
|
mock.hook.reset_mock()
|
||||||
await acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
acfactory.process_message(from_account=user, to_client=bot, text="hello")
|
||||||
event = await acfactory.process_message(from_account=user, to_client=bot, text="/help")
|
event = acfactory.process_message(from_account=user, to_client=bot, text="/help")
|
||||||
mock.hook.assert_called_once_with(event.msg_id)
|
mock.hook.assert_called_once_with(event.msg_id)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_wait_next_messages(acfactory) -> None:
|
||||||
async def test_wait_next_messages(acfactory) -> None:
|
alice = acfactory.new_configured_account()
|
||||||
alice = await acfactory.new_configured_account()
|
|
||||||
|
|
||||||
# Create a bot account so it does not receive device messages in the beginning.
|
# Create a bot account so it does not receive device messages in the beginning.
|
||||||
bot = await acfactory.new_preconfigured_account()
|
bot = acfactory.new_preconfigured_account()
|
||||||
await bot.set_config("bot", "1")
|
bot.set_config("bot", "1")
|
||||||
await bot.configure()
|
bot.configure()
|
||||||
|
|
||||||
# There are no old messages and the call returns immediately.
|
# There are no old messages and the call returns immediately.
|
||||||
assert not await bot.wait_next_messages()
|
assert not bot.wait_next_messages()
|
||||||
|
|
||||||
# Bot starts waiting for messages.
|
with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor:
|
||||||
next_messages_task = asyncio.create_task(bot.wait_next_messages())
|
# Bot starts waiting for messages.
|
||||||
|
next_messages_task = executor.submit(bot.wait_next_messages)
|
||||||
|
|
||||||
bot_addr = await bot.get_config("addr")
|
bot_addr = bot.get_config("addr")
|
||||||
alice_contact_bot = await alice.create_contact(bot_addr, "Bob")
|
alice_contact_bot = alice.create_contact(bot_addr, "Bob")
|
||||||
alice_chat_bot = await alice_contact_bot.create_chat()
|
alice_chat_bot = alice_contact_bot.create_chat()
|
||||||
await alice_chat_bot.send_text("Hello!")
|
alice_chat_bot.send_text("Hello!")
|
||||||
|
|
||||||
next_messages = await next_messages_task
|
next_messages = next_messages_task.result()
|
||||||
assert len(next_messages) == 1
|
assert len(next_messages) == 1
|
||||||
snapshot = await next_messages[0].get_snapshot()
|
snapshot = next_messages[0].get_snapshot()
|
||||||
assert snapshot.text == "Hello!"
|
assert snapshot.text == "Hello!"
|
||||||
|
|
||||||
|
|
||||||
|
def test_import_export(acfactory, tmp_path) -> None:
|
||||||
|
alice = acfactory.new_configured_account()
|
||||||
|
alice.export_backup(tmp_path)
|
||||||
|
|
||||||
|
files = list(tmp_path.glob("*.tar"))
|
||||||
|
alice2 = acfactory.get_unconfigured_account()
|
||||||
|
alice2.import_backup(files[0])
|
||||||
|
|
||||||
|
assert alice2.manager.get_system_info()
|
||||||
|
|
||||||
|
|
||||||
|
def test_openrpc_command_line() -> None:
|
||||||
|
"""Test that "deltachat-rpc-server --openrpc" command returns an OpenRPC specification."""
|
||||||
|
out = subprocess.run(["deltachat-rpc-server", "--openrpc"], capture_output=True, check=True).stdout
|
||||||
|
openrpc = json.loads(out)
|
||||||
|
assert "openrpc" in openrpc
|
||||||
|
assert "methods" in openrpc
|
||||||
|
|||||||
@@ -1,24 +1,22 @@
|
|||||||
import pytest
|
|
||||||
from deltachat_rpc_client import EventType
|
from deltachat_rpc_client import EventType
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio()
|
def test_webxdc(acfactory) -> None:
|
||||||
async def test_webxdc(acfactory) -> None:
|
alice, bob = acfactory.get_online_accounts(2)
|
||||||
alice, bob = await acfactory.get_online_accounts(2)
|
|
||||||
|
|
||||||
bob_addr = await bob.get_config("addr")
|
bob_addr = bob.get_config("addr")
|
||||||
alice_contact_bob = await alice.create_contact(bob_addr, "Bob")
|
alice_contact_bob = alice.create_contact(bob_addr, "Bob")
|
||||||
alice_chat_bob = await alice_contact_bob.create_chat()
|
alice_chat_bob = alice_contact_bob.create_chat()
|
||||||
await alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
alice_chat_bob.send_message(text="Let's play chess!", file="../test-data/webxdc/chess.xdc")
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
event = await bob.wait_for_event()
|
event = bob.wait_for_event()
|
||||||
if event.type == EventType.INCOMING_MSG:
|
if event.kind == EventType.INCOMING_MSG:
|
||||||
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
bob_chat_alice = bob.get_chat_by_id(event.chat_id)
|
||||||
message = bob.get_message_by_id(event.msg_id)
|
message = bob.get_message_by_id(event.msg_id)
|
||||||
break
|
break
|
||||||
|
|
||||||
webxdc_info = await message.get_webxdc_info()
|
webxdc_info = message.get_webxdc_info()
|
||||||
assert webxdc_info == {
|
assert webxdc_info == {
|
||||||
"document": None,
|
"document": None,
|
||||||
"icon": "icon.png",
|
"icon": "icon.png",
|
||||||
@@ -28,20 +26,20 @@ async def test_webxdc(acfactory) -> None:
|
|||||||
"summary": None,
|
"summary": None,
|
||||||
}
|
}
|
||||||
|
|
||||||
status_updates = await message.get_webxdc_status_updates()
|
status_updates = message.get_webxdc_status_updates()
|
||||||
assert status_updates == []
|
assert status_updates == []
|
||||||
|
|
||||||
await bob_chat_alice.accept()
|
bob_chat_alice.accept()
|
||||||
await message.send_webxdc_status_update({"payload": 42}, "")
|
message.send_webxdc_status_update({"payload": 42}, "")
|
||||||
await message.send_webxdc_status_update({"payload": "Second update"}, "description")
|
message.send_webxdc_status_update({"payload": "Second update"}, "description")
|
||||||
|
|
||||||
status_updates = await message.get_webxdc_status_updates()
|
status_updates = message.get_webxdc_status_updates()
|
||||||
assert status_updates == [
|
assert status_updates == [
|
||||||
{"payload": 42, "serial": 1, "max_serial": 2},
|
{"payload": 42, "serial": 1, "max_serial": 2},
|
||||||
{"payload": "Second update", "serial": 2, "max_serial": 2},
|
{"payload": "Second update", "serial": 2, "max_serial": 2},
|
||||||
]
|
]
|
||||||
|
|
||||||
status_updates = await message.get_webxdc_status_updates(1)
|
status_updates = message.get_webxdc_status_updates(1)
|
||||||
assert status_updates == [
|
assert status_updates == [
|
||||||
{"payload": "Second update", "serial": 2, "max_serial": 2},
|
{"payload": "Second update", "serial": 2, "max_serial": 2},
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ envlist =
|
|||||||
|
|
||||||
[testenv]
|
[testenv]
|
||||||
commands =
|
commands =
|
||||||
pytest {posargs}
|
pytest -n6 {posargs}
|
||||||
setenv =
|
setenv =
|
||||||
# Avoid stack overflow when Rust core is built without optimizations.
|
# Avoid stack overflow when Rust core is built without optimizations.
|
||||||
RUST_MIN_STACK=8388608
|
RUST_MIN_STACK=8388608
|
||||||
@@ -14,10 +14,8 @@ passenv =
|
|||||||
DCC_NEW_TMP_EMAIL
|
DCC_NEW_TMP_EMAIL
|
||||||
deps =
|
deps =
|
||||||
pytest
|
pytest
|
||||||
pytest-asyncio
|
|
||||||
pytest-timeout
|
pytest-timeout
|
||||||
aiohttp
|
pytest-xdist
|
||||||
aiodns
|
|
||||||
|
|
||||||
[testenv:lint]
|
[testenv:lint]
|
||||||
skipsdist = True
|
skipsdist = True
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat-rpc-server"
|
name = "deltachat-rpc-server"
|
||||||
version = "1.112.6"
|
version = "1.126.1"
|
||||||
description = "DeltaChat JSON-RPC server"
|
description = "DeltaChat JSON-RPC server"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
@@ -15,13 +15,13 @@ deltachat = { path = "..", default-features = false }
|
|||||||
|
|
||||||
anyhow = "1"
|
anyhow = "1"
|
||||||
env_logger = { version = "0.10.0" }
|
env_logger = { version = "0.10.0" }
|
||||||
futures-lite = "1.12.0"
|
futures-lite = "1.13.0"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
serde_json = "1.0.95"
|
serde_json = "1.0.105"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
tokio = { version = "1.27.0", features = ["io-std"] }
|
tokio = { version = "1.32.0", features = ["io-std"] }
|
||||||
tokio-util = "0.7.7"
|
tokio-util = "0.7.9"
|
||||||
yerpc = { version = "0.4.0", features = ["anyhow_expose"] }
|
yerpc = { version = "0.5.2", features = ["anyhow_expose", "openrpc"] }
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored"]
|
||||||
|
|||||||
@@ -32,3 +32,6 @@ languages other than Rust, for example:
|
|||||||
|
|
||||||
1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/
|
1. Python: https://github.com/deltachat/deltachat-core-rust/tree/master/deltachat-rpc-client/
|
||||||
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/
|
2. Go: https://github.com/deltachat/deltachat-rpc-client-go/
|
||||||
|
|
||||||
|
Run `deltachat-rpc-server --version` to check the version of the server.
|
||||||
|
Run `deltachat-rpc-server --openrpc` to get [OpenRPC](https://open-rpc.org/) specification of the provided JSON-RPC API.
|
||||||
|
|||||||
@@ -1,23 +1,36 @@
|
|||||||
|
//! Delta Chat core RPC server.
|
||||||
|
//!
|
||||||
|
//! It speaks JSON Lines over stdio.
|
||||||
use std::env;
|
use std::env;
|
||||||
///! Delta Chat core RPC server.
|
|
||||||
///!
|
|
||||||
///! It speaks JSON Lines over stdio.
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::{anyhow, Context as _, Result};
|
use anyhow::{anyhow, Context as _, Result};
|
||||||
use deltachat::constants::DC_VERSION_STR;
|
use deltachat::constants::DC_VERSION_STR;
|
||||||
use deltachat_jsonrpc::api::events::event_to_json_rpc_notification;
|
|
||||||
use deltachat_jsonrpc::api::{Accounts, CommandApi};
|
use deltachat_jsonrpc::api::{Accounts, CommandApi};
|
||||||
use futures_lite::stream::StreamExt;
|
use futures_lite::stream::StreamExt;
|
||||||
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
use tokio::io::{self, AsyncBufReadExt, BufReader};
|
||||||
|
use yerpc::RpcServer as _;
|
||||||
|
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
use tokio::signal::unix as signal_unix;
|
||||||
|
|
||||||
use tokio::sync::RwLock;
|
use tokio::sync::RwLock;
|
||||||
use tokio::task::JoinHandle;
|
use tokio::task::JoinHandle;
|
||||||
use tokio_util::sync::CancellationToken;
|
use tokio_util::sync::CancellationToken;
|
||||||
use yerpc::{RpcClient, RpcSession};
|
use yerpc::{RpcClient, RpcSession};
|
||||||
|
|
||||||
#[tokio::main(flavor = "multi_thread")]
|
#[tokio::main(flavor = "multi_thread")]
|
||||||
async fn main() -> Result<()> {
|
async fn main() {
|
||||||
|
let r = main_impl().await;
|
||||||
|
// From tokio documentation:
|
||||||
|
// "For technical reasons, stdin is implemented by using an ordinary blocking read on a separate
|
||||||
|
// thread, and it is impossible to cancel that read. This can make shutdown of the runtime hang
|
||||||
|
// until the user presses enter."
|
||||||
|
std::process::exit(if r.is_ok() { 0 } else { 1 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn main_impl() -> Result<()> {
|
||||||
let mut args = env::args_os();
|
let mut args = env::args_os();
|
||||||
let _program_name = args.next().context("no command line arguments found")?;
|
let _program_name = args.next().context("no command line arguments found")?;
|
||||||
if let Some(first_arg) = args.next() {
|
if let Some(first_arg) = args.next() {
|
||||||
@@ -27,6 +40,12 @@ async fn main() -> Result<()> {
|
|||||||
}
|
}
|
||||||
eprintln!("{}", &*DC_VERSION_STR);
|
eprintln!("{}", &*DC_VERSION_STR);
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
} else if first_arg.to_str() == Some("--openrpc") {
|
||||||
|
if let Some(arg) = args.next() {
|
||||||
|
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
||||||
|
}
|
||||||
|
println!("{}", CommandApi::openrpc_specification()?);
|
||||||
|
return Ok(());
|
||||||
} else {
|
} else {
|
||||||
return Err(anyhow!("Unrecognized option {:?}", first_arg));
|
return Err(anyhow!("Unrecognized option {:?}", first_arg));
|
||||||
}
|
}
|
||||||
@@ -35,12 +54,17 @@ async fn main() -> Result<()> {
|
|||||||
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
return Err(anyhow!("Unrecognized argument {:?}", arg));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Install signal handlers early so that the shutdown is graceful starting from here.
|
||||||
|
let _ctrl_c = tokio::signal::ctrl_c();
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
let mut sigterm = signal_unix::signal(signal_unix::SignalKind::terminate())?;
|
||||||
|
|
||||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
|
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 path = std::env::var("DC_ACCOUNTS_PATH").unwrap_or_else(|_| "accounts".to_string());
|
||||||
log::info!("Starting with accounts directory `{}`.", path);
|
log::info!("Starting with accounts directory `{}`.", path);
|
||||||
let accounts = Accounts::new(PathBuf::from(&path)).await?;
|
let writable = true;
|
||||||
let events = accounts.get_event_emitter();
|
let accounts = Accounts::new(PathBuf::from(&path), writable).await?;
|
||||||
|
|
||||||
log::info!("Creating JSON-RPC API.");
|
log::info!("Creating JSON-RPC API.");
|
||||||
let accounts = Arc::new(RwLock::new(accounts));
|
let accounts = Arc::new(RwLock::new(accounts));
|
||||||
@@ -48,28 +72,15 @@ async fn main() -> Result<()> {
|
|||||||
|
|
||||||
let (client, mut out_receiver) = RpcClient::new();
|
let (client, mut out_receiver) = RpcClient::new();
|
||||||
let session = RpcSession::new(client.clone(), state.clone());
|
let session = RpcSession::new(client.clone(), state.clone());
|
||||||
let canceler = CancellationToken::new();
|
let main_cancel = CancellationToken::new();
|
||||||
|
|
||||||
// Events task converts core events to JSON-RPC notifications.
|
|
||||||
let events_task: JoinHandle<Result<()>> = tokio::spawn(async move {
|
|
||||||
let mut r = Ok(());
|
|
||||||
while let Some(event) = events.recv().await {
|
|
||||||
if r.is_err() {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let event = event_to_json_rpc_notification(event);
|
|
||||||
r = client.send_notification("event", Some(event)).await;
|
|
||||||
}
|
|
||||||
r?;
|
|
||||||
Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send task prints JSON responses to stdout.
|
// Send task prints JSON responses to stdout.
|
||||||
let cancelable = canceler.clone();
|
let cancel = main_cancel.clone();
|
||||||
let send_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
|
let send_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
|
||||||
|
let _cancel_guard = cancel.clone().drop_guard();
|
||||||
loop {
|
loop {
|
||||||
let message = tokio::select! {
|
let message = tokio::select! {
|
||||||
_ = cancelable.cancelled() => break,
|
_ = cancel.cancelled() => break,
|
||||||
message = out_receiver.next() => match message {
|
message = out_receiver.next() => match message {
|
||||||
None => break,
|
None => break,
|
||||||
Some(message) => serde_json::to_string(&message)?,
|
Some(message) => serde_json::to_string(&message)?,
|
||||||
@@ -81,12 +92,32 @@ async fn main() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let cancel = main_cancel.clone();
|
||||||
|
let sigterm_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
{
|
||||||
|
let _cancel_guard = cancel.clone().drop_guard();
|
||||||
|
tokio::select! {
|
||||||
|
_ = cancel.cancelled() => (),
|
||||||
|
_ = sigterm.recv() => {
|
||||||
|
log::info!("got SIGTERM");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let _ = cancel;
|
||||||
|
Ok(())
|
||||||
|
});
|
||||||
|
|
||||||
// Receiver task reads JSON requests from stdin.
|
// Receiver task reads JSON requests from stdin.
|
||||||
|
let cancel = main_cancel.clone();
|
||||||
let recv_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
|
let recv_task: JoinHandle<anyhow::Result<()>> = tokio::spawn(async move {
|
||||||
|
let _cancel_guard = cancel.clone().drop_guard();
|
||||||
let stdin = io::stdin();
|
let stdin = io::stdin();
|
||||||
let mut lines = BufReader::new(stdin).lines();
|
let mut lines = BufReader::new(stdin).lines();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let message = tokio::select! {
|
let message = tokio::select! {
|
||||||
|
_ = cancel.cancelled() => break,
|
||||||
_ = tokio::signal::ctrl_c() => {
|
_ = tokio::signal::ctrl_c() => {
|
||||||
log::info!("got ctrl-c event");
|
log::info!("got ctrl-c event");
|
||||||
break;
|
break;
|
||||||
@@ -108,18 +139,13 @@ async fn main() -> Result<()> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for the end of stdin / ctrl-c.
|
main_cancel.cancelled().await;
|
||||||
recv_task.await?.ok();
|
|
||||||
|
|
||||||
// See "Thread safety" section in deltachat-ffi/deltachat.h for explanation.
|
|
||||||
// NB: Events are drained by events_task.
|
|
||||||
canceler.cancel();
|
|
||||||
accounts.read().await.stop_io().await;
|
accounts.read().await.stop_io().await;
|
||||||
|
drop(accounts);
|
||||||
drop(state);
|
drop(state);
|
||||||
let (r0, r1) = tokio::join!(events_task, send_task);
|
send_task.await??;
|
||||||
for r in [r0, r1] {
|
sigterm_task.await??;
|
||||||
r??;
|
recv_task.await??;
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
33
deny.toml
33
deny.toml
@@ -2,9 +2,7 @@
|
|||||||
unmaintained = "allow"
|
unmaintained = "allow"
|
||||||
ignore = [
|
ignore = [
|
||||||
"RUSTSEC-2020-0071",
|
"RUSTSEC-2020-0071",
|
||||||
|
"RUSTSEC-2022-0093",
|
||||||
# Only affects windows if using non-default allocator (and unmaintained).
|
|
||||||
"RUSTSEC-2021-0145",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[bans]
|
[bans]
|
||||||
@@ -17,8 +15,6 @@ skip = [
|
|||||||
{ name = "base64", version = "<0.21" },
|
{ name = "base64", version = "<0.21" },
|
||||||
{ name = "bitflags", version = "1.3.2" },
|
{ name = "bitflags", version = "1.3.2" },
|
||||||
{ name = "block-buffer", version = "<0.10" },
|
{ name = "block-buffer", version = "<0.10" },
|
||||||
{ name = "clap_lex", version = "0.2.4" },
|
|
||||||
{ name = "clap", version = "3.2.23" },
|
|
||||||
{ name = "convert_case", version = "0.4.0" },
|
{ name = "convert_case", version = "0.4.0" },
|
||||||
{ name = "curve25519-dalek", version = "3.2.0" },
|
{ name = "curve25519-dalek", version = "3.2.0" },
|
||||||
{ name = "darling_core", version = "<0.14" },
|
{ name = "darling_core", version = "<0.14" },
|
||||||
@@ -28,12 +24,10 @@ skip = [
|
|||||||
{ name = "digest", version = "<0.10" },
|
{ name = "digest", version = "<0.10" },
|
||||||
{ name = "ed25519-dalek", version = "1.0.1" },
|
{ name = "ed25519-dalek", version = "1.0.1" },
|
||||||
{ name = "ed25519", version = "1.5.3" },
|
{ name = "ed25519", version = "1.5.3" },
|
||||||
{ name = "env_logger", version = "<0.10" },
|
{ name = "fastrand", version = "1.9.0" },
|
||||||
{ name = "getrandom", version = "<0.2" },
|
{ name = "getrandom", version = "<0.2" },
|
||||||
{ name = "hermit-abi", version = "<0.3" },
|
{ name = "hashbrown", version = "<0.14.0" },
|
||||||
{ name = "humantime", version = "<2.1" },
|
{ name = "indexmap", version = "<2.0.0" },
|
||||||
{ name = "idna", version = "<0.3" },
|
|
||||||
{ name = "libm", version = "0.1.4" },
|
|
||||||
{ name = "pem-rfc7468", version = "0.6.0" },
|
{ name = "pem-rfc7468", version = "0.6.0" },
|
||||||
{ name = "pkcs8", version = "0.9.0" },
|
{ name = "pkcs8", version = "0.9.0" },
|
||||||
{ name = "quick-error", version = "<2.0" },
|
{ name = "quick-error", version = "<2.0" },
|
||||||
@@ -41,20 +35,27 @@ skip = [
|
|||||||
{ name = "rand_core", version = "<0.6" },
|
{ name = "rand_core", version = "<0.6" },
|
||||||
{ name = "rand", version = "<0.8" },
|
{ name = "rand", version = "<0.8" },
|
||||||
{ name = "redox_syscall", version = "0.2.16" },
|
{ name = "redox_syscall", version = "0.2.16" },
|
||||||
|
{ name = "regex-automata", version = "0.1.10" },
|
||||||
|
{ name = "regex-syntax", version = "0.6.29" },
|
||||||
{ name = "sec1", version = "0.3.0" },
|
{ name = "sec1", version = "0.3.0" },
|
||||||
{ name = "sha2", version = "<0.10" },
|
{ name = "sha2", version = "<0.10" },
|
||||||
{ name = "signature", version = "1.6.4" },
|
{ name = "signature", version = "1.6.4" },
|
||||||
|
{ name = "socket2", version = "0.4.9" },
|
||||||
{ name = "spin", version = "<0.9.6" },
|
{ name = "spin", version = "<0.9.6" },
|
||||||
{ name = "spki", version = "0.6.0" },
|
{ name = "spki", version = "0.6.0" },
|
||||||
{ name = "syn", version = "1.0.109" },
|
{ name = "syn", version = "1.0.109" },
|
||||||
{ name = "time", version = "<0.3" },
|
{ name = "time", version = "<0.3" },
|
||||||
{ name = "wasi", version = "<0.11" },
|
{ name = "wasi", version = "<0.11" },
|
||||||
{ name = "windows_aarch64_msvc", version = "<0.42" },
|
{ name = "windows_aarch64_gnullvm", version = "<0.48" },
|
||||||
{ name = "windows_i686_gnu", version = "<0.42" },
|
{ name = "windows_aarch64_msvc", version = "<0.48" },
|
||||||
{ name = "windows_i686_msvc", version = "<0.42" },
|
{ name = "windows_i686_gnu", version = "<0.48" },
|
||||||
{ name = "windows-sys", version = "<0.45" },
|
{ name = "windows_i686_msvc", version = "<0.48" },
|
||||||
{ name = "windows_x86_64_gnu", version = "<0.42" },
|
{ name = "windows-sys", version = "<0.48" },
|
||||||
{ name = "windows_x86_64_msvc", version = "<0.42" },
|
{ name = "windows-targets", version = "<0.48" },
|
||||||
|
{ name = "windows", version = "0.32.0" },
|
||||||
|
{ name = "windows_x86_64_gnullvm", version = "<0.48" },
|
||||||
|
{ name = "windows_x86_64_gnu", version = "<0.48" },
|
||||||
|
{ name = "windows_x86_64_msvc", version = "<0.48" },
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ The most obvious alternative would be to create a new contact with the new addre
|
|||||||
#### Upsides:
|
#### Upsides:
|
||||||
- With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
|
- With this approach, it's easier to switch to a model where the info about the transition is encoded in the PGP key. Since the key is gossiped, the information about the transition will spread virally.
|
||||||
- (Also, less important: Slightly faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
|
- (Also, less important: Slightly faster transition: If you send a message to e.g. "Delta Chat Dev", all members of the "sub-group" "delta android" will know of your transition.)
|
||||||
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't wast that much development time.)
|
- It's easier to implement (if too many problems turn up, we can still switch to another approach and didn't waste that much development time.)
|
||||||
|
|
||||||
[full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161)
|
[full messages](https://github.com/deltachat/deltachat-core-rust/pull/2896#discussion_r852002161)
|
||||||
|
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
use deltachat::chat::{self, ChatId};
|
|
||||||
use deltachat::chatlist::*;
|
|
||||||
use deltachat::config;
|
|
||||||
use deltachat::contact::*;
|
|
||||||
use deltachat::context::*;
|
|
||||||
use deltachat::message::Message;
|
|
||||||
use deltachat::stock_str::StockStrings;
|
|
||||||
use deltachat::{EventType, Events};
|
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
fn cb(event: EventType) {
|
|
||||||
match event {
|
|
||||||
EventType::ConfigureProgress { progress, .. } => {
|
|
||||||
log::info!("progress: {}", progress);
|
|
||||||
}
|
|
||||||
EventType::Info(msg) => {
|
|
||||||
log::info!("{}", msg);
|
|
||||||
}
|
|
||||||
EventType::Warning(msg) => {
|
|
||||||
log::warn!("{}", msg);
|
|
||||||
}
|
|
||||||
EventType::Error(msg) => {
|
|
||||||
log::error!("{}", msg);
|
|
||||||
}
|
|
||||||
event => {
|
|
||||||
log::info!("{:?}", event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Run with `RUST_LOG=simple=info cargo run --release --example simple -- email pw`.
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
pretty_env_logger::try_init_timed().ok();
|
|
||||||
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
let dbfile = dir.path().join("db.sqlite");
|
|
||||||
log::info!("creating database {:?}", dbfile);
|
|
||||||
let ctx = Context::new(&dbfile, 0, Events::new(), StockStrings::new())
|
|
||||||
.await
|
|
||||||
.expect("Failed to create context");
|
|
||||||
let info = ctx.get_info().await;
|
|
||||||
log::info!("info: {:#?}", info);
|
|
||||||
|
|
||||||
let events = ctx.get_event_emitter();
|
|
||||||
let events_spawn = tokio::task::spawn(async move {
|
|
||||||
while let Some(event) = events.recv().await {
|
|
||||||
cb(event.typ);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
log::info!("configuring");
|
|
||||||
let args = std::env::args().collect::<Vec<String>>();
|
|
||||||
assert_eq!(args.len(), 3, "requires email password");
|
|
||||||
let email = args[1].clone();
|
|
||||||
let pw = args[2].clone();
|
|
||||||
ctx.set_config(config::Config::Addr, Some(&email))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
ctx.set_config(config::Config::MailPw, Some(&pw))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
ctx.configure().await.unwrap();
|
|
||||||
|
|
||||||
log::info!("------ RUN ------");
|
|
||||||
ctx.start_io().await;
|
|
||||||
log::info!("--- SENDING A MESSAGE ---");
|
|
||||||
|
|
||||||
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let chat_id = ChatId::create_for_contact(&ctx, contact_id).await.unwrap();
|
|
||||||
|
|
||||||
for i in 0..1 {
|
|
||||||
log::info!("sending message {}", i);
|
|
||||||
chat::send_text_msg(&ctx, chat_id, format!("Hi, here is my {i}nth message!"))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for the message to be sent out
|
|
||||||
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
|
||||||
|
|
||||||
log::info!("fetching chats..");
|
|
||||||
let chats = Chatlist::try_load(&ctx, 0, None, None).await.unwrap();
|
|
||||||
|
|
||||||
for i in 0..chats.len() {
|
|
||||||
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap().unwrap())
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
log::info!("[{}] msg: {:?}", i, msg);
|
|
||||||
}
|
|
||||||
|
|
||||||
log::info!("stopping");
|
|
||||||
ctx.stop_io().await;
|
|
||||||
log::info!("closing");
|
|
||||||
drop(ctx);
|
|
||||||
events_spawn.await.unwrap();
|
|
||||||
}
|
|
||||||
472
fuzz/Cargo.lock
generated
472
fuzz/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -48,6 +48,7 @@ module.exports = {
|
|||||||
DC_EVENT_LOCATION_CHANGED: 2035,
|
DC_EVENT_LOCATION_CHANGED: 2035,
|
||||||
DC_EVENT_MSGS_CHANGED: 2000,
|
DC_EVENT_MSGS_CHANGED: 2000,
|
||||||
DC_EVENT_MSGS_NOTICED: 2008,
|
DC_EVENT_MSGS_NOTICED: 2008,
|
||||||
|
DC_EVENT_MSG_DELETED: 2016,
|
||||||
DC_EVENT_MSG_DELIVERED: 2010,
|
DC_EVENT_MSG_DELIVERED: 2010,
|
||||||
DC_EVENT_MSG_FAILED: 2012,
|
DC_EVENT_MSG_FAILED: 2012,
|
||||||
DC_EVENT_MSG_READ: 2015,
|
DC_EVENT_MSG_READ: 2015,
|
||||||
@@ -89,6 +90,7 @@ module.exports = {
|
|||||||
DC_KEY_GEN_DEFAULT: 0,
|
DC_KEY_GEN_DEFAULT: 0,
|
||||||
DC_KEY_GEN_ED25519: 2,
|
DC_KEY_GEN_ED25519: 2,
|
||||||
DC_KEY_GEN_RSA2048: 1,
|
DC_KEY_GEN_RSA2048: 1,
|
||||||
|
DC_KEY_GEN_RSA4096: 3,
|
||||||
DC_LP_AUTH_NORMAL: 4,
|
DC_LP_AUTH_NORMAL: 4,
|
||||||
DC_LP_AUTH_OAUTH2: 2,
|
DC_LP_AUTH_OAUTH2: 2,
|
||||||
DC_MEDIA_QUALITY_BALANCED: 0,
|
DC_MEDIA_QUALITY_BALANCED: 0,
|
||||||
@@ -151,11 +153,14 @@ module.exports = {
|
|||||||
DC_STR_AEAP_EXPLANATION_AND_LINK: 123,
|
DC_STR_AEAP_EXPLANATION_AND_LINK: 123,
|
||||||
DC_STR_ARCHIVEDCHATS: 40,
|
DC_STR_ARCHIVEDCHATS: 40,
|
||||||
DC_STR_AUDIO: 11,
|
DC_STR_AUDIO: 11,
|
||||||
|
DC_STR_BACKUP_TRANSFER_MSG_BODY: 163,
|
||||||
DC_STR_BACKUP_TRANSFER_QR: 162,
|
DC_STR_BACKUP_TRANSFER_QR: 162,
|
||||||
DC_STR_BAD_TIME_MSG_BODY: 85,
|
DC_STR_BAD_TIME_MSG_BODY: 85,
|
||||||
DC_STR_BROADCAST_LIST: 115,
|
DC_STR_BROADCAST_LIST: 115,
|
||||||
DC_STR_CANNOT_LOGIN: 60,
|
DC_STR_CANNOT_LOGIN: 60,
|
||||||
DC_STR_CANTDECRYPT_MSG_BODY: 29,
|
DC_STR_CANTDECRYPT_MSG_BODY: 29,
|
||||||
|
DC_STR_CHAT_PROTECTION_DISABLED: 171,
|
||||||
|
DC_STR_CHAT_PROTECTION_ENABLED: 170,
|
||||||
DC_STR_CONFIGURATION_FAILED: 84,
|
DC_STR_CONFIGURATION_FAILED: 84,
|
||||||
DC_STR_CONNECTED: 107,
|
DC_STR_CONNECTED: 107,
|
||||||
DC_STR_CONNTECTING: 108,
|
DC_STR_CONNTECTING: 108,
|
||||||
@@ -241,12 +246,6 @@ module.exports = {
|
|||||||
DC_STR_OUTGOING_MESSAGES: 104,
|
DC_STR_OUTGOING_MESSAGES: 104,
|
||||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
|
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY: 99,
|
||||||
DC_STR_PART_OF_TOTAL_USED: 116,
|
DC_STR_PART_OF_TOTAL_USED: 116,
|
||||||
DC_STR_PROTECTION_DISABLED: 89,
|
|
||||||
DC_STR_PROTECTION_DISABLED_BY_OTHER: 161,
|
|
||||||
DC_STR_PROTECTION_DISABLED_BY_YOU: 160,
|
|
||||||
DC_STR_PROTECTION_ENABLED: 88,
|
|
||||||
DC_STR_PROTECTION_ENABLED_BY_OTHER: 159,
|
|
||||||
DC_STR_PROTECTION_ENABLED_BY_YOU: 158,
|
|
||||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
|
DC_STR_QUOTA_EXCEEDING_MSG_BODY: 98,
|
||||||
DC_STR_READRCPT: 31,
|
DC_STR_READRCPT: 31,
|
||||||
DC_STR_READRCPT_MAILBODY: 32,
|
DC_STR_READRCPT_MAILBODY: 32,
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ module.exports = {
|
|||||||
2010: 'DC_EVENT_MSG_DELIVERED',
|
2010: 'DC_EVENT_MSG_DELIVERED',
|
||||||
2012: 'DC_EVENT_MSG_FAILED',
|
2012: 'DC_EVENT_MSG_FAILED',
|
||||||
2015: 'DC_EVENT_MSG_READ',
|
2015: 'DC_EVENT_MSG_READ',
|
||||||
|
2016: 'DC_EVENT_MSG_DELETED',
|
||||||
2020: 'DC_EVENT_CHAT_MODIFIED',
|
2020: 'DC_EVENT_CHAT_MODIFIED',
|
||||||
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
|
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
|
||||||
2030: 'DC_EVENT_CONTACTS_CHANGED',
|
2030: 'DC_EVENT_CONTACTS_CHANGED',
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ export enum C {
|
|||||||
DC_EVENT_LOCATION_CHANGED = 2035,
|
DC_EVENT_LOCATION_CHANGED = 2035,
|
||||||
DC_EVENT_MSGS_CHANGED = 2000,
|
DC_EVENT_MSGS_CHANGED = 2000,
|
||||||
DC_EVENT_MSGS_NOTICED = 2008,
|
DC_EVENT_MSGS_NOTICED = 2008,
|
||||||
|
DC_EVENT_MSG_DELETED = 2016,
|
||||||
DC_EVENT_MSG_DELIVERED = 2010,
|
DC_EVENT_MSG_DELIVERED = 2010,
|
||||||
DC_EVENT_MSG_FAILED = 2012,
|
DC_EVENT_MSG_FAILED = 2012,
|
||||||
DC_EVENT_MSG_READ = 2015,
|
DC_EVENT_MSG_READ = 2015,
|
||||||
@@ -89,6 +90,7 @@ export enum C {
|
|||||||
DC_KEY_GEN_DEFAULT = 0,
|
DC_KEY_GEN_DEFAULT = 0,
|
||||||
DC_KEY_GEN_ED25519 = 2,
|
DC_KEY_GEN_ED25519 = 2,
|
||||||
DC_KEY_GEN_RSA2048 = 1,
|
DC_KEY_GEN_RSA2048 = 1,
|
||||||
|
DC_KEY_GEN_RSA4096 = 3,
|
||||||
DC_LP_AUTH_NORMAL = 4,
|
DC_LP_AUTH_NORMAL = 4,
|
||||||
DC_LP_AUTH_OAUTH2 = 2,
|
DC_LP_AUTH_OAUTH2 = 2,
|
||||||
DC_MEDIA_QUALITY_BALANCED = 0,
|
DC_MEDIA_QUALITY_BALANCED = 0,
|
||||||
@@ -151,11 +153,14 @@ export enum C {
|
|||||||
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
|
DC_STR_AEAP_EXPLANATION_AND_LINK = 123,
|
||||||
DC_STR_ARCHIVEDCHATS = 40,
|
DC_STR_ARCHIVEDCHATS = 40,
|
||||||
DC_STR_AUDIO = 11,
|
DC_STR_AUDIO = 11,
|
||||||
|
DC_STR_BACKUP_TRANSFER_MSG_BODY = 163,
|
||||||
DC_STR_BACKUP_TRANSFER_QR = 162,
|
DC_STR_BACKUP_TRANSFER_QR = 162,
|
||||||
DC_STR_BAD_TIME_MSG_BODY = 85,
|
DC_STR_BAD_TIME_MSG_BODY = 85,
|
||||||
DC_STR_BROADCAST_LIST = 115,
|
DC_STR_BROADCAST_LIST = 115,
|
||||||
DC_STR_CANNOT_LOGIN = 60,
|
DC_STR_CANNOT_LOGIN = 60,
|
||||||
DC_STR_CANTDECRYPT_MSG_BODY = 29,
|
DC_STR_CANTDECRYPT_MSG_BODY = 29,
|
||||||
|
DC_STR_CHAT_PROTECTION_DISABLED = 171,
|
||||||
|
DC_STR_CHAT_PROTECTION_ENABLED = 170,
|
||||||
DC_STR_CONFIGURATION_FAILED = 84,
|
DC_STR_CONFIGURATION_FAILED = 84,
|
||||||
DC_STR_CONNECTED = 107,
|
DC_STR_CONNECTED = 107,
|
||||||
DC_STR_CONNTECTING = 108,
|
DC_STR_CONNTECTING = 108,
|
||||||
@@ -241,12 +246,6 @@ export enum C {
|
|||||||
DC_STR_OUTGOING_MESSAGES = 104,
|
DC_STR_OUTGOING_MESSAGES = 104,
|
||||||
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
|
DC_STR_PARTIAL_DOWNLOAD_MSG_BODY = 99,
|
||||||
DC_STR_PART_OF_TOTAL_USED = 116,
|
DC_STR_PART_OF_TOTAL_USED = 116,
|
||||||
DC_STR_PROTECTION_DISABLED = 89,
|
|
||||||
DC_STR_PROTECTION_DISABLED_BY_OTHER = 161,
|
|
||||||
DC_STR_PROTECTION_DISABLED_BY_YOU = 160,
|
|
||||||
DC_STR_PROTECTION_ENABLED = 88,
|
|
||||||
DC_STR_PROTECTION_ENABLED_BY_OTHER = 159,
|
|
||||||
DC_STR_PROTECTION_ENABLED_BY_YOU = 158,
|
|
||||||
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
|
DC_STR_QUOTA_EXCEEDING_MSG_BODY = 98,
|
||||||
DC_STR_READRCPT = 31,
|
DC_STR_READRCPT = 31,
|
||||||
DC_STR_READRCPT_MAILBODY = 32,
|
DC_STR_READRCPT_MAILBODY = 32,
|
||||||
@@ -306,6 +305,7 @@ export const EventId2EventName: { [key: number]: string } = {
|
|||||||
2010: 'DC_EVENT_MSG_DELIVERED',
|
2010: 'DC_EVENT_MSG_DELIVERED',
|
||||||
2012: 'DC_EVENT_MSG_FAILED',
|
2012: 'DC_EVENT_MSG_FAILED',
|
||||||
2015: 'DC_EVENT_MSG_READ',
|
2015: 'DC_EVENT_MSG_READ',
|
||||||
|
2016: 'DC_EVENT_MSG_DELETED',
|
||||||
2020: 'DC_EVENT_CHAT_MODIFIED',
|
2020: 'DC_EVENT_CHAT_MODIFIED',
|
||||||
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
|
2021: 'DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED',
|
||||||
2030: 'DC_EVENT_CONTACTS_CHANGED',
|
2030: 'DC_EVENT_CONTACTS_CHANGED',
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export class Context extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Opens a stanalone context (without an account manager)
|
/** Opens a standalone context (without an account manager)
|
||||||
* automatically starts the event handler */
|
* automatically starts the event handler */
|
||||||
static open(cwd: string): Context {
|
static open(cwd: string): Context {
|
||||||
const dbFile = join(cwd, 'db.sqlite')
|
const dbFile = join(cwd, 'db.sqlite')
|
||||||
@@ -699,23 +699,6 @@ export class Context extends EventEmitter {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
*
|
|
||||||
* @param chatId
|
|
||||||
* @param protect
|
|
||||||
* @returns success boolean
|
|
||||||
*/
|
|
||||||
setChatProtection(chatId: number, protect: boolean) {
|
|
||||||
debug(`setChatProtection ${chatId} ${protect}`)
|
|
||||||
return Boolean(
|
|
||||||
binding.dcn_set_chat_protection(
|
|
||||||
this.dcn_context,
|
|
||||||
Number(chatId),
|
|
||||||
protect ? 1 : 0
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
getChatEphemeralTimer(chatId: number): number {
|
getChatEphemeralTimer(chatId: number): number {
|
||||||
debug(`getChatEphemeralTimer ${chatId}`)
|
debug(`getChatEphemeralTimer ${chatId}`)
|
||||||
return binding.dcn_get_chat_ephemeral_timer(
|
return binding.dcn_get_chat_ephemeral_timer(
|
||||||
|
|||||||
@@ -21,12 +21,15 @@ export class AccountManager extends EventEmitter {
|
|||||||
accountDir: string
|
accountDir: string
|
||||||
jsonRpcStarted = false
|
jsonRpcStarted = false
|
||||||
|
|
||||||
constructor(cwd: string, os = 'deltachat-node') {
|
constructor(cwd: string, writable = true) {
|
||||||
super()
|
super()
|
||||||
debug('DeltaChat constructor')
|
debug('DeltaChat constructor')
|
||||||
|
|
||||||
this.accountDir = cwd
|
this.accountDir = cwd
|
||||||
this.dcn_accounts = binding.dcn_accounts_new(os, this.accountDir)
|
this.dcn_accounts = binding.dcn_accounts_new(
|
||||||
|
this.accountDir,
|
||||||
|
writable ? 1 : 0
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
getAllAccountIds() {
|
getAllAccountIds() {
|
||||||
|
|||||||
@@ -1399,18 +1399,6 @@ NAPI_METHOD(dcn_set_chat_name) {
|
|||||||
NAPI_RETURN_INT32(result);
|
NAPI_RETURN_INT32(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
NAPI_METHOD(dcn_set_chat_protection) {
|
|
||||||
NAPI_ARGV(3);
|
|
||||||
NAPI_DCN_CONTEXT();
|
|
||||||
NAPI_ARGV_UINT32(chat_id, 1);
|
|
||||||
NAPI_ARGV_INT32(protect, 1);
|
|
||||||
|
|
||||||
int result = dc_set_chat_protection(dcn_context->dc_context,
|
|
||||||
chat_id,
|
|
||||||
protect);
|
|
||||||
NAPI_RETURN_INT32(result);
|
|
||||||
}
|
|
||||||
|
|
||||||
NAPI_METHOD(dcn_get_chat_ephemeral_timer) {
|
NAPI_METHOD(dcn_get_chat_ephemeral_timer) {
|
||||||
NAPI_ARGV(2);
|
NAPI_ARGV(2);
|
||||||
NAPI_DCN_CONTEXT();
|
NAPI_DCN_CONTEXT();
|
||||||
@@ -2915,8 +2903,8 @@ NAPI_METHOD(dcn_msg_get_webxdc_blob){
|
|||||||
|
|
||||||
NAPI_METHOD(dcn_accounts_new) {
|
NAPI_METHOD(dcn_accounts_new) {
|
||||||
NAPI_ARGV(2);
|
NAPI_ARGV(2);
|
||||||
NAPI_ARGV_UTF8_MALLOC(os_name, 0);
|
NAPI_ARGV_UTF8_MALLOC(dir, 0);
|
||||||
NAPI_ARGV_UTF8_MALLOC(dir, 1);
|
NAPI_ARGV_INT32(writable, 1);
|
||||||
TRACE("calling..");
|
TRACE("calling..");
|
||||||
|
|
||||||
dcn_accounts_t* dcn_accounts = calloc(1, sizeof(dcn_accounts_t));
|
dcn_accounts_t* dcn_accounts = calloc(1, sizeof(dcn_accounts_t));
|
||||||
@@ -2925,7 +2913,7 @@ NAPI_METHOD(dcn_accounts_new) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
dcn_accounts->dc_accounts = dc_accounts_new(os_name, dir);
|
dcn_accounts->dc_accounts = dc_accounts_new(dir, writable);
|
||||||
|
|
||||||
napi_value result;
|
napi_value result;
|
||||||
NAPI_STATUS_THROWS(napi_create_external(env, dcn_accounts,
|
NAPI_STATUS_THROWS(napi_create_external(env, dcn_accounts,
|
||||||
@@ -3491,7 +3479,6 @@ NAPI_INIT() {
|
|||||||
NAPI_EXPORT_FUNCTION(dcn_send_msg);
|
NAPI_EXPORT_FUNCTION(dcn_send_msg);
|
||||||
NAPI_EXPORT_FUNCTION(dcn_send_videochat_invitation);
|
NAPI_EXPORT_FUNCTION(dcn_send_videochat_invitation);
|
||||||
NAPI_EXPORT_FUNCTION(dcn_set_chat_name);
|
NAPI_EXPORT_FUNCTION(dcn_set_chat_name);
|
||||||
NAPI_EXPORT_FUNCTION(dcn_set_chat_protection);
|
|
||||||
NAPI_EXPORT_FUNCTION(dcn_get_chat_ephemeral_timer);
|
NAPI_EXPORT_FUNCTION(dcn_get_chat_ephemeral_timer);
|
||||||
NAPI_EXPORT_FUNCTION(dcn_set_chat_ephemeral_timer);
|
NAPI_EXPORT_FUNCTION(dcn_set_chat_ephemeral_timer);
|
||||||
NAPI_EXPORT_FUNCTION(dcn_set_chat_profile_image);
|
NAPI_EXPORT_FUNCTION(dcn_set_chat_profile_image);
|
||||||
|
|||||||
@@ -446,7 +446,8 @@ describe('Offline Tests with unconfigured account', function () {
|
|||||||
context.setChatProfileImage(chatId, imagePath)
|
context.setChatProfileImage(chatId, imagePath)
|
||||||
const blobPath = context.getChat(chatId).getProfileImage()
|
const blobPath = context.getChat(chatId).getProfileImage()
|
||||||
expect(blobPath.startsWith(blobs)).to.be.true
|
expect(blobPath.startsWith(blobs)).to.be.true
|
||||||
expect(blobPath.endsWith(image)).to.be.true
|
expect(blobPath.includes('image')).to.be.true
|
||||||
|
expect(blobPath.endsWith('.jpeg')).to.be.true
|
||||||
|
|
||||||
context.setChatProfileImage(chatId, null)
|
context.setChatProfileImage(chatId, null)
|
||||||
expect(context.getChat(chatId).getProfileImage()).to.be.equal(
|
expect(context.getChat(chatId).getProfileImage()).to.be.equal(
|
||||||
|
|||||||
@@ -60,5 +60,5 @@
|
|||||||
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
"test:mocha": "mocha -r esm node/test/test.js --growl --reporter=spec --bail --exit"
|
||||||
},
|
},
|
||||||
"types": "node/dist/index.d.ts",
|
"types": "node/dist/index.d.ts",
|
||||||
"version": "1.112.6"
|
"version": "1.126.1"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -357,7 +357,7 @@ Exhibit A - Source Code Form License Notice
|
|||||||
|
|
||||||
This Source Code Form is subject to the terms of the Mozilla Public
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
License, v. 2.0. If a copy of the MPL was not distributed with this
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
file, You can obtain one at https://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
If it is not possible or desirable to put the notice in a particular
|
If it is not possible or desirable to put the notice in a particular
|
||||||
file, then You may include the notice in a location (such as a LICENSE
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
|||||||
@@ -74,7 +74,9 @@ Developing the bindings
|
|||||||
If you want to develop or debug the bindings,
|
If you want to develop or debug the bindings,
|
||||||
you can create a testing development environment using `tox`::
|
you can create a testing development environment using `tox`::
|
||||||
|
|
||||||
tox -c python --devenv env
|
export DCC_RS_DEV="$PWD"
|
||||||
|
export DCC_RS_TARGET=debug
|
||||||
|
tox -c python --devenv env -e py
|
||||||
. env/bin/activate
|
. env/bin/activate
|
||||||
|
|
||||||
Inside this environment the bindings are installed
|
Inside this environment the bindings are installed
|
||||||
|
|||||||
@@ -2,34 +2,34 @@
|
|||||||
high level API reference
|
high level API reference
|
||||||
========================
|
========================
|
||||||
|
|
||||||
- :class:`deltachat.account.Account` (your main entry point, creates the
|
- :class:`deltachat.Account` (your main entry point, creates the
|
||||||
other classes)
|
other classes)
|
||||||
- :class:`deltachat.contact.Contact`
|
- :class:`deltachat.Contact`
|
||||||
- :class:`deltachat.chat.Chat`
|
- :class:`deltachat.Chat`
|
||||||
- :class:`deltachat.message.Message`
|
- :class:`deltachat.Message`
|
||||||
|
|
||||||
Account
|
Account
|
||||||
-------
|
-------
|
||||||
|
|
||||||
.. autoclass:: deltachat.account.Account
|
.. autoclass:: deltachat.Account
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|
||||||
Contact
|
Contact
|
||||||
-------
|
-------
|
||||||
|
|
||||||
.. autoclass:: deltachat.contact.Contact
|
.. autoclass:: deltachat.Contact
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Chat
|
Chat
|
||||||
----
|
----
|
||||||
|
|
||||||
.. autoclass:: deltachat.chat.Chat
|
.. autoclass:: deltachat.Chat
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
Message
|
Message
|
||||||
-------
|
-------
|
||||||
|
|
||||||
.. autoclass:: deltachat.message.Message
|
.. autoclass:: deltachat.Message
|
||||||
:members:
|
:members:
|
||||||
|
|
||||||
|
|||||||
@@ -32,25 +32,13 @@ class GroupTrackingPlugin:
|
|||||||
|
|
||||||
@account_hookimpl
|
@account_hookimpl
|
||||||
def ac_member_added(self, chat, contact, actor, message):
|
def ac_member_added(self, chat, contact, actor, message):
|
||||||
print(
|
print(f"ac_member_added {contact.addr} to chat {chat.id} from {actor or message.get_sender_contact().addr}")
|
||||||
"ac_member_added {} to chat {} from {}".format(
|
|
||||||
contact.addr,
|
|
||||||
chat.id,
|
|
||||||
actor or message.get_sender_contact().addr,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
for member in chat.get_contacts():
|
for member in chat.get_contacts():
|
||||||
print(f"chat member: {member.addr}")
|
print(f"chat member: {member.addr}")
|
||||||
|
|
||||||
@account_hookimpl
|
@account_hookimpl
|
||||||
def ac_member_removed(self, chat, contact, actor, message):
|
def ac_member_removed(self, chat, contact, actor, message):
|
||||||
print(
|
print(f"ac_member_removed {contact.addr} from chat {chat.id} by {actor or message.get_sender_contact().addr}")
|
||||||
"ac_member_removed {} from chat {} by {}".format(
|
|
||||||
contact.addr,
|
|
||||||
chat.id,
|
|
||||||
actor or message.get_sender_contact().addr,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
def main(argv=None):
|
||||||
|
|||||||
@@ -24,8 +24,6 @@ def test_echo_quit_plugin(acfactory, lp):
|
|||||||
lp.sec("creating a temp account to contact the bot")
|
lp.sec("creating a temp account to contact the bot")
|
||||||
(ac1,) = acfactory.get_online_accounts(1)
|
(ac1,) = acfactory.get_online_accounts(1)
|
||||||
|
|
||||||
botproc.await_resync()
|
|
||||||
|
|
||||||
lp.sec("sending a message to the bot")
|
lp.sec("sending a message to the bot")
|
||||||
bot_contact = ac1.create_contact(botproc.addr)
|
bot_contact = ac1.create_contact(botproc.addr)
|
||||||
bot_chat = bot_contact.create_chat()
|
bot_chat = bot_contact.create_chat()
|
||||||
@@ -54,8 +52,6 @@ def test_group_tracking_plugin(acfactory, lp):
|
|||||||
ac1.add_account_plugin(FFIEventLogger(ac1))
|
ac1.add_account_plugin(FFIEventLogger(ac1))
|
||||||
ac2.add_account_plugin(FFIEventLogger(ac2))
|
ac2.add_account_plugin(FFIEventLogger(ac2))
|
||||||
|
|
||||||
botproc.await_resync()
|
|
||||||
|
|
||||||
lp.sec("creating bot test group with bot")
|
lp.sec("creating bot test group with bot")
|
||||||
bot_contact = ac1.create_contact(botproc.addr)
|
bot_contact = ac1.create_contact(botproc.addr)
|
||||||
ch = ac1.create_group_chat("bot test group")
|
ch = ac1.create_group_chat("bot test group")
|
||||||
|
|||||||
@@ -24,3 +24,5 @@ ignore_missing_imports = True
|
|||||||
[mypy-imap_tools.*]
|
[mypy-imap_tools.*]
|
||||||
ignore_missing_imports = True
|
ignore_missing_imports = True
|
||||||
|
|
||||||
|
[mypy-distutils.*]
|
||||||
|
ignore_missing_imports = True
|
||||||
|
|||||||
@@ -11,10 +11,11 @@ authors = [
|
|||||||
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
|
{ name = "holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors" },
|
||||||
]
|
]
|
||||||
classifiers = [
|
classifiers = [
|
||||||
"Development Status :: 4 - Beta",
|
"Development Status :: 5 - Production/Stable",
|
||||||
"Intended Audience :: Developers",
|
"Intended Audience :: Developers",
|
||||||
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
"License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)",
|
||||||
"Programming Language :: Python :: 3",
|
"Programming Language :: Python :: 3",
|
||||||
|
"Topic :: Communications :: Chat",
|
||||||
"Topic :: Communications :: Email",
|
"Topic :: Communications :: Email",
|
||||||
"Topic :: Software Development :: Libraries",
|
"Topic :: Software Development :: Libraries",
|
||||||
]
|
]
|
||||||
@@ -33,6 +34,7 @@ dynamic = [
|
|||||||
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
|
"Home" = "https://github.com/deltachat/deltachat-core-rust/"
|
||||||
"Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues"
|
"Bug Tracker" = "https://github.com/deltachat/deltachat-core-rust/issues"
|
||||||
"Documentation" = "https://py.delta.chat/"
|
"Documentation" = "https://py.delta.chat/"
|
||||||
|
"Mastodon" = "https://chaos.social/@delta"
|
||||||
|
|
||||||
[project.entry-points.pytest11]
|
[project.entry-points.pytest11]
|
||||||
"deltachat.testplugin" = "deltachat.testplugin"
|
"deltachat.testplugin" = "deltachat.testplugin"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ from array import array
|
|||||||
from contextlib import contextmanager
|
from contextlib import contextmanager
|
||||||
from email.utils import parseaddr
|
from email.utils import parseaddr
|
||||||
from threading import Event
|
from threading import Event
|
||||||
from typing import Any, Dict, Generator, List, Optional, Union, TYPE_CHECKING
|
from typing import TYPE_CHECKING, Any, Dict, Generator, List, Optional, Union
|
||||||
|
|
||||||
from . import const, hookspec
|
from . import const, hookspec
|
||||||
from .capi import ffi, lib
|
from .capi import ffi, lib
|
||||||
@@ -195,7 +195,7 @@ class Account:
|
|||||||
assert res != ffi.NULL, f"config value not found for: {name!r}"
|
assert res != ffi.NULL, f"config value not found for: {name!r}"
|
||||||
return from_dc_charpointer(res)
|
return from_dc_charpointer(res)
|
||||||
|
|
||||||
def _preconfigure_keypair(self, addr: str, public: str, secret: str) -> None:
|
def _preconfigure_keypair(self, addr: str, secret: str) -> None:
|
||||||
"""See dc_preconfigure_keypair() in deltachat.h.
|
"""See dc_preconfigure_keypair() in deltachat.h.
|
||||||
|
|
||||||
In other words, you don't need this.
|
In other words, you don't need this.
|
||||||
@@ -203,7 +203,7 @@ class Account:
|
|||||||
res = lib.dc_preconfigure_keypair(
|
res = lib.dc_preconfigure_keypair(
|
||||||
self._dc_context,
|
self._dc_context,
|
||||||
as_dc_charpointer(addr),
|
as_dc_charpointer(addr),
|
||||||
as_dc_charpointer(public),
|
ffi.NULL,
|
||||||
as_dc_charpointer(secret),
|
as_dc_charpointer(secret),
|
||||||
)
|
)
|
||||||
if res == 0:
|
if res == 0:
|
||||||
@@ -427,7 +427,7 @@ class Account:
|
|||||||
|
|
||||||
assert dc_chatlist != ffi.NULL
|
assert dc_chatlist != ffi.NULL
|
||||||
chatlist = []
|
chatlist = []
|
||||||
for i in range(0, lib.dc_chatlist_get_cnt(dc_chatlist)):
|
for i in range(lib.dc_chatlist_get_cnt(dc_chatlist)):
|
||||||
chat_id = lib.dc_chatlist_get_chat_id(dc_chatlist, i)
|
chat_id = lib.dc_chatlist_get_chat_id(dc_chatlist, i)
|
||||||
chatlist.append(Chat(self, chat_id))
|
chatlist.append(Chat(self, chat_id))
|
||||||
return chatlist
|
return chatlist
|
||||||
@@ -617,18 +617,18 @@ class Account:
|
|||||||
# meta API for start/stop and event based processing
|
# meta API for start/stop and event based processing
|
||||||
#
|
#
|
||||||
|
|
||||||
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False):
|
def run_account(self, addr=None, password=None, account_plugins=None, show_ffi=False, displayname=None):
|
||||||
from .events import FFIEventLogger
|
|
||||||
|
|
||||||
"""get the account running, configure it if necessary. add plugins if provided.
|
"""get the account running, configure it if necessary. add plugins if provided.
|
||||||
|
|
||||||
:param addr: the email address of the account
|
:param addr: the email address of the account
|
||||||
:param password: the password of the account
|
:param password: the password of the account
|
||||||
:param account_plugins: a list of plugins to add
|
:param account_plugins: a list of plugins to add
|
||||||
:param show_ffi: show low level ffi events
|
:param show_ffi: show low level ffi events
|
||||||
|
:param displayname: the display name of the account
|
||||||
"""
|
"""
|
||||||
|
from .events import FFIEventLogger
|
||||||
|
|
||||||
if show_ffi:
|
if show_ffi:
|
||||||
self.set_config("displayname", "bot")
|
|
||||||
log = FFIEventLogger(self)
|
log = FFIEventLogger(self)
|
||||||
self.add_account_plugin(log)
|
self.add_account_plugin(log)
|
||||||
|
|
||||||
@@ -644,6 +644,8 @@ class Account:
|
|||||||
configtracker = self.configure()
|
configtracker = self.configure()
|
||||||
configtracker.wait_finish()
|
configtracker.wait_finish()
|
||||||
|
|
||||||
|
if displayname:
|
||||||
|
self.set_config("displayname", displayname)
|
||||||
# start IO threads and configure if necessary
|
# start IO threads and configure if necessary
|
||||||
self.start_io()
|
self.start_io()
|
||||||
|
|
||||||
|
|||||||
@@ -71,13 +71,16 @@ class Contact:
|
|||||||
"""Unblock this contact. Messages from this contact will be retrieved (again)."""
|
"""Unblock this contact. Messages from this contact will be retrieved (again)."""
|
||||||
return lib.dc_block_contact(self.account._dc_context, self.id, False)
|
return lib.dc_block_contact(self.account._dc_context, self.id, False)
|
||||||
|
|
||||||
def is_verified(self):
|
def is_verified(self) -> bool:
|
||||||
"""Return True if the contact is verified."""
|
"""Return True if the contact is verified."""
|
||||||
return lib.dc_contact_is_verified(self._dc_contact)
|
return lib.dc_contact_is_verified(self._dc_contact) == 2
|
||||||
|
|
||||||
def get_verifier(self, contact):
|
def get_verifier(self, contact) -> Optional["Contact"]:
|
||||||
"""Return the address of the contact that verified the contact."""
|
"""Return the address of the contact that verified the contact."""
|
||||||
return from_dc_charpointer(lib.dc_contact_get_verifier_addr(contact._dc_contact))
|
verifier_id = lib.dc_contact_get_verifier_id(contact._dc_contact)
|
||||||
|
if verifier_id == 0:
|
||||||
|
return None
|
||||||
|
return Contact(self.account, verifier_id)
|
||||||
|
|
||||||
def get_profile_image(self) -> Optional[str]:
|
def get_profile_image(self) -> Optional[str]:
|
||||||
"""Get contact profile image.
|
"""Get contact profile image.
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ def as_dc_charpointer(obj):
|
|||||||
|
|
||||||
|
|
||||||
def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]:
|
def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]:
|
||||||
for i in range(0, lib.dc_array_get_cnt(dc_array_t)):
|
for i in range(lib.dc_array_get_cnt(dc_array_t)):
|
||||||
yield constructor(lib.dc_array_get_id(dc_array_t, i))
|
yield constructor(lib.dc_array_get_id(dc_array_t, i))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user