Compare commits
5 Commits
1.78.0
...
test-ecc-b
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed092fe1ce | ||
|
|
56f589db4a | ||
|
|
49b39b42bf | ||
|
|
7e4951e691 | ||
|
|
f50874acf1 |
232
.circleci/config.yml
Normal file
@@ -0,0 +1,232 @@
|
|||||||
|
version: 2.1
|
||||||
|
executors:
|
||||||
|
default:
|
||||||
|
docker:
|
||||||
|
- image: filecoin/rust:latest
|
||||||
|
working_directory: /mnt/crate
|
||||||
|
doxygen:
|
||||||
|
docker:
|
||||||
|
- image: hrektts/doxygen
|
||||||
|
|
||||||
|
restore-workspace: &restore-workspace
|
||||||
|
attach_workspace:
|
||||||
|
at: /mnt
|
||||||
|
|
||||||
|
restore-cache: &restore-cache
|
||||||
|
restore_cache:
|
||||||
|
keys:
|
||||||
|
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
|
||||||
|
- repo-source-{{ .Branch }}-{{ .Revision }}
|
||||||
|
|
||||||
|
commands:
|
||||||
|
test_target:
|
||||||
|
parameters:
|
||||||
|
target:
|
||||||
|
type: string
|
||||||
|
steps:
|
||||||
|
- *restore-workspace
|
||||||
|
- *restore-cache
|
||||||
|
- run:
|
||||||
|
name: Test (<< parameters.target >>)
|
||||||
|
command: TARGET=<< parameters.target >> ci_scripts/run-rust-test.sh
|
||||||
|
no_output_timeout: 15m
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
cargo_fetch:
|
||||||
|
executor: default
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- restore_cache:
|
||||||
|
keys:
|
||||||
|
- cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
|
||||||
|
- run: rustup install $(cat rust-toolchain)
|
||||||
|
- run: rustup default $(cat rust-toolchain)
|
||||||
|
- run: rustup component add --toolchain $(cat rust-toolchain) rustfmt
|
||||||
|
- run: rustup component add --toolchain $(cat rust-toolchain) clippy-preview
|
||||||
|
- run: cargo fetch
|
||||||
|
- run: rustc +stable --version
|
||||||
|
- run: rustc +$(cat rust-toolchain) --version
|
||||||
|
# make sure this git repo doesn't grow too big
|
||||||
|
- run: git gc
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: /mnt
|
||||||
|
paths:
|
||||||
|
- crate
|
||||||
|
- save_cache:
|
||||||
|
key: cargo-v3-{{ checksum "rust-toolchain" }}-{{ checksum "Cargo.toml" }}-{{ checksum "Cargo.lock" }}-{{ arch }}
|
||||||
|
paths:
|
||||||
|
- "~/.cargo"
|
||||||
|
- "~/.rustup"
|
||||||
|
|
||||||
|
rustfmt:
|
||||||
|
executor: default
|
||||||
|
steps:
|
||||||
|
- *restore-workspace
|
||||||
|
- *restore-cache
|
||||||
|
- run:
|
||||||
|
name: Run cargo fmt
|
||||||
|
command: cargo fmt --all -- --check
|
||||||
|
|
||||||
|
test_macos:
|
||||||
|
macos:
|
||||||
|
xcode: "10.0.0"
|
||||||
|
working_directory: ~/crate
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
name: Configure environment variables
|
||||||
|
command: |
|
||||||
|
echo 'export PATH="${HOME}/.cargo/bin:${HOME}/.bin:${PATH}"' >> $BASH_ENV
|
||||||
|
echo 'export CIRCLE_ARTIFACTS="/tmp"' >> $BASH_ENV
|
||||||
|
- checkout
|
||||||
|
- run:
|
||||||
|
name: Install Rust
|
||||||
|
command: |
|
||||||
|
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||||
|
- run: rustup install $(cat rust-toolchain)
|
||||||
|
- run: rustup default $(cat rust-toolchain)
|
||||||
|
- run: cargo fetch
|
||||||
|
- run:
|
||||||
|
name: Test
|
||||||
|
command: TARGET=x86_64-apple-darwin ci_scripts/run-rust-test.sh
|
||||||
|
|
||||||
|
test_x86_64-unknown-linux-gnu:
|
||||||
|
executor: default
|
||||||
|
steps:
|
||||||
|
- test_target:
|
||||||
|
target: "x86_64-unknown-linux-gnu"
|
||||||
|
|
||||||
|
test_i686-unknown-linux-gnu:
|
||||||
|
executor: default
|
||||||
|
steps:
|
||||||
|
- test_target:
|
||||||
|
target: "i686-unknown-linux-gnu"
|
||||||
|
|
||||||
|
test_aarch64-linux-android:
|
||||||
|
executor: default
|
||||||
|
steps:
|
||||||
|
- test_target:
|
||||||
|
target: "aarch64-linux-android"
|
||||||
|
|
||||||
|
|
||||||
|
build_doxygen:
|
||||||
|
executor: doxygen
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run: bash ci_scripts/run-doxygen.sh
|
||||||
|
- run: mkdir -p workspace/c-docs
|
||||||
|
- run: cp -av deltachat-ffi/{html,xml} workspace/c-docs/
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: workspace
|
||||||
|
paths:
|
||||||
|
- c-docs
|
||||||
|
|
||||||
|
remote_python_packaging:
|
||||||
|
machine: true
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
# the following commands on success produces
|
||||||
|
# workspace/{wheelhouse,py-docs} as artefact directories
|
||||||
|
- run: bash ci_scripts/remote_python_packaging.sh
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: workspace
|
||||||
|
paths:
|
||||||
|
# - c-docs
|
||||||
|
- py-docs
|
||||||
|
- wheelhouse
|
||||||
|
|
||||||
|
remote_tests_rust:
|
||||||
|
machine: true
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run: ci_scripts/remote_tests_rust.sh
|
||||||
|
|
||||||
|
remote_tests_python:
|
||||||
|
machine: true
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run: ci_scripts/remote_tests_python.sh
|
||||||
|
|
||||||
|
upload_docs_wheels:
|
||||||
|
machine: true
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- attach_workspace:
|
||||||
|
at: workspace
|
||||||
|
- run: pyenv versions
|
||||||
|
- run: pyenv global 3.5.2
|
||||||
|
- run: ls -laR workspace
|
||||||
|
- run: ci_scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse workspace/c-docs
|
||||||
|
|
||||||
|
clippy:
|
||||||
|
executor: default
|
||||||
|
steps:
|
||||||
|
- *restore-workspace
|
||||||
|
- *restore-cache
|
||||||
|
- run:
|
||||||
|
name: Run cargo clippy
|
||||||
|
command: cargo clippy
|
||||||
|
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2.1
|
||||||
|
|
||||||
|
test:
|
||||||
|
jobs:
|
||||||
|
# - cargo_fetch
|
||||||
|
|
||||||
|
- remote_tests_rust:
|
||||||
|
filters:
|
||||||
|
tags:
|
||||||
|
only: /.*/
|
||||||
|
|
||||||
|
- remote_tests_python:
|
||||||
|
filters:
|
||||||
|
tags:
|
||||||
|
only: /.*/
|
||||||
|
|
||||||
|
- remote_python_packaging:
|
||||||
|
requires:
|
||||||
|
- remote_tests_python
|
||||||
|
- remote_tests_rust
|
||||||
|
filters:
|
||||||
|
tags:
|
||||||
|
only: /.*/
|
||||||
|
|
||||||
|
- upload_docs_wheels:
|
||||||
|
requires:
|
||||||
|
- remote_python_packaging
|
||||||
|
- build_doxygen
|
||||||
|
filters:
|
||||||
|
tags:
|
||||||
|
only: /.*/
|
||||||
|
# - rustfmt:
|
||||||
|
# requires:
|
||||||
|
# - cargo_fetch
|
||||||
|
# - clippy:
|
||||||
|
# requires:
|
||||||
|
# - cargo_fetch
|
||||||
|
|
||||||
|
- build_doxygen:
|
||||||
|
filters:
|
||||||
|
tags:
|
||||||
|
only: /.*/
|
||||||
|
|
||||||
|
# Linux Desktop 64bit
|
||||||
|
# - test_x86_64-unknown-linux-gnu:
|
||||||
|
# requires:
|
||||||
|
# - cargo_fetch
|
||||||
|
|
||||||
|
# Linux Desktop 32bit
|
||||||
|
# - test_i686-unknown-linux-gnu:
|
||||||
|
# requires:
|
||||||
|
# - cargo_fetch
|
||||||
|
|
||||||
|
# Android 64bit
|
||||||
|
# - test_aarch64-linux-android:
|
||||||
|
# requires:
|
||||||
|
# - cargo_fetch
|
||||||
|
|
||||||
|
# Desktop Apple
|
||||||
|
# - test_macos:
|
||||||
|
# requires:
|
||||||
|
# - cargo_fetch
|
||||||
3
.gitattributes
vendored
@@ -12,6 +12,3 @@ test-data/* text=false
|
|||||||
*.gif binary
|
*.gif binary
|
||||||
*.ico binary
|
*.ico binary
|
||||||
|
|
||||||
*.py diff=python
|
|
||||||
*.rs diff=rust
|
|
||||||
*.md diff=markdown
|
|
||||||
|
|||||||
9
.github/dependabot.yml
vendored
@@ -1,9 +0,0 @@
|
|||||||
version: 2
|
|
||||||
updates:
|
|
||||||
- package-ecosystem: "cargo"
|
|
||||||
directory: "/"
|
|
||||||
schedule:
|
|
||||||
interval: "monthly"
|
|
||||||
commit-message:
|
|
||||||
prefix: "cargo"
|
|
||||||
open-pull-requests-limit: 50
|
|
||||||
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/**'
|
|
||||||
required: ['CHANGELOG.md']
|
|
||||||
fail:
|
|
||||||
- do: checks
|
|
||||||
status: 'action_required'
|
|
||||||
payload:
|
|
||||||
title: Changlog might need an update
|
|
||||||
summary: "Check if CHANGELOG.md needs an update or add #skip-changelog to the PR description."
|
|
||||||
143
.github/workflows/ci.yml
vendored
@@ -1,143 +0,0 @@
|
|||||||
name: Rust CI
|
|
||||||
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- staging
|
|
||||||
- trying
|
|
||||||
|
|
||||||
env:
|
|
||||||
RUSTFLAGS: -Dwarnings
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
|
|
||||||
fmt:
|
|
||||||
name: Rustfmt
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
profile: minimal
|
|
||||||
toolchain: stable
|
|
||||||
override: true
|
|
||||||
- run: rustup component add rustfmt
|
|
||||||
- name: Cache rust cargo artifacts
|
|
||||||
uses: swatinem/rust-cache@v1
|
|
||||||
- uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: fmt
|
|
||||||
args: --all -- --check
|
|
||||||
|
|
||||||
run_clippy:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
components: clippy
|
|
||||||
override: true
|
|
||||||
- name: Cache rust cargo artifacts
|
|
||||||
uses: swatinem/rust-cache@v1
|
|
||||||
- uses: actions-rs/clippy-check@v1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
args: --workspace --tests --examples --benches
|
|
||||||
|
|
||||||
docs:
|
|
||||||
name: Rust doc comments
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
env:
|
|
||||||
RUSTDOCFLAGS: -Dwarnings
|
|
||||||
steps:
|
|
||||||
- name: Checkout sources
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Install rust stable toolchain
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable
|
|
||||||
profile: minimal
|
|
||||||
components: rust-docs
|
|
||||||
override: true
|
|
||||||
- name: Cache rust cargo artifacts
|
|
||||||
uses: swatinem/rust-cache@v1
|
|
||||||
- name: Rustdoc
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: doc
|
|
||||||
args: --document-private-items --no-deps
|
|
||||||
|
|
||||||
build_and_test:
|
|
||||||
name: Build and test
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
# Currently used Rust version, same as in `rust-toolchain` file.
|
|
||||||
- os: ubuntu-latest
|
|
||||||
rust: 1.60.0
|
|
||||||
python: 3.9
|
|
||||||
- os: windows-latest
|
|
||||||
rust: 1.60.0
|
|
||||||
python: false # Python bindings compilation on Windows is not supported.
|
|
||||||
|
|
||||||
# Minimum Supported Rust Version = 1.56.0
|
|
||||||
#
|
|
||||||
# Minimum Supported Python Version = 3.7
|
|
||||||
# This is the minimum version for which manylinux Python wheels are
|
|
||||||
# built.
|
|
||||||
- os: ubuntu-latest
|
|
||||||
rust: 1.56.0
|
|
||||||
python: 3.7
|
|
||||||
runs-on: ${{ matrix.os }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@master
|
|
||||||
|
|
||||||
- name: Install ${{ matrix.rust }}
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: ${{ matrix.rust }}
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Cache rust cargo artifacts
|
|
||||||
uses: swatinem/rust-cache@v1
|
|
||||||
|
|
||||||
- name: check
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: check
|
|
||||||
args: --all --bins --examples --tests --features repl --benches
|
|
||||||
|
|
||||||
- name: tests
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: test
|
|
||||||
args: --all
|
|
||||||
|
|
||||||
- name: install python
|
|
||||||
if: ${{ matrix.python }}
|
|
||||||
uses: actions/setup-python@v2
|
|
||||||
with:
|
|
||||||
python-version: ${{ matrix.python }}
|
|
||||||
|
|
||||||
- name: install tox
|
|
||||||
if: ${{ matrix.python }}
|
|
||||||
run: pip install tox
|
|
||||||
|
|
||||||
- name: build C library
|
|
||||||
if: ${{ matrix.python }}
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: build
|
|
||||||
args: -p deltachat_ffi
|
|
||||||
|
|
||||||
- name: run python tests
|
|
||||||
if: ${{ matrix.python }}
|
|
||||||
env:
|
|
||||||
DCC_NEW_TMP_EMAIL: ${{ secrets.DCC_NEW_TMP_EMAIL }}
|
|
||||||
DCC_RS_TARGET: debug
|
|
||||||
DCC_RS_DEV: ${{ github.workspace }}
|
|
||||||
working-directory: python
|
|
||||||
run: tox -e lint,mypy,doc,py3
|
|
||||||
48
.github/workflows/code-quality.yml
vendored
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
on: push
|
||||||
|
name: Code Quality
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
check:
|
||||||
|
name: Check
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: nightly-2019-11-06
|
||||||
|
override: true
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: check
|
||||||
|
args: --workspace --examples --tests --all-features
|
||||||
|
|
||||||
|
fmt:
|
||||||
|
name: Rustfmt
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: nightly-2019-11-06
|
||||||
|
override: true
|
||||||
|
- run: rustup component add rustfmt
|
||||||
|
- uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: fmt
|
||||||
|
args: --all -- --check
|
||||||
|
|
||||||
|
run_clippy:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v1
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly-2019-11-06
|
||||||
|
components: clippy
|
||||||
|
override: true
|
||||||
|
- uses: actions-rs/clippy-check@v1
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
args: --all-features
|
||||||
21
.github/workflows/dependabot.yml
vendored
@@ -1,21 +0,0 @@
|
|||||||
name: Dependabot auto-approve
|
|
||||||
on: pull_request
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
pull-requests: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
dependabot:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
if: ${{ github.actor == 'dependabot[bot]' }}
|
|
||||||
steps:
|
|
||||||
- name: Dependabot metadata
|
|
||||||
id: metadata
|
|
||||||
uses: dependabot/fetch-metadata@v1.1.1
|
|
||||||
with:
|
|
||||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
|
||||||
- name: Approve a PR
|
|
||||||
run: gh pr review --approve "$PR_URL"
|
|
||||||
env:
|
|
||||||
PR_URL: ${{github.event.pull_request.html_url}}
|
|
||||||
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
|
|
||||||
32
.github/workflows/repl.yml
vendored
@@ -1,32 +0,0 @@
|
|||||||
# Manually triggered action to build a Windows repl.exe which users can
|
|
||||||
# download to debug complex bugs.
|
|
||||||
|
|
||||||
name: Build Windows REPL .exe
|
|
||||||
|
|
||||||
on:
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build_repl:
|
|
||||||
name: Build REPL example
|
|
||||||
runs-on: windows-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@master
|
|
||||||
|
|
||||||
- name: Install Rust
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: 1.50.0
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: build
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: build
|
|
||||||
args: --example repl --features repl,vendored
|
|
||||||
|
|
||||||
- name: Upload binary
|
|
||||||
uses: actions/upload-artifact@v2
|
|
||||||
with:
|
|
||||||
name: repl.exe
|
|
||||||
path: 'target/debug/examples/repl.exe'
|
|
||||||
28
.github/workflows/upload-docs.yml
vendored
@@ -1,28 +0,0 @@
|
|||||||
name: Build & Deploy Documentation on rs.delta.chat
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- docs-gh-action
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v1
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
- name: Build the documentation with cargo
|
|
||||||
run: |
|
|
||||||
cargo doc --package deltachat --no-deps
|
|
||||||
- name: Upload to rs.delta.chat
|
|
||||||
uses: up9cloud/action-rsync@v1.3
|
|
||||||
env:
|
|
||||||
USER: ${{ secrets.USERNAME }}
|
|
||||||
KEY: ${{ secrets.KEY }}
|
|
||||||
HOST: "delta.chat"
|
|
||||||
SOURCE: "target/doc"
|
|
||||||
TARGET: "/var/www/html/rs/"
|
|
||||||
|
|
||||||
28
.github/workflows/upload-ffi-docs.yml
vendored
@@ -1,28 +0,0 @@
|
|||||||
name: Build & Deploy Documentation on cffi.delta.chat
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- master
|
|
||||||
- docs-gh-action
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v1
|
|
||||||
- uses: actions-rs/toolchain@v1
|
|
||||||
- name: Build the documentation with cargo
|
|
||||||
run: |
|
|
||||||
cargo doc --package deltachat_ffi --no-deps
|
|
||||||
- name: Upload to cffi.delta.chat
|
|
||||||
uses: up9cloud/action-rsync@v1.3
|
|
||||||
env:
|
|
||||||
USER: ${{ secrets.USERNAME }}
|
|
||||||
KEY: ${{ secrets.KEY }}
|
|
||||||
HOST: "delta.chat"
|
|
||||||
SOURCE: "target/doc"
|
|
||||||
TARGET: "/var/www/html/cffi/"
|
|
||||||
|
|
||||||
8
.gitignore
vendored
@@ -1,6 +1,5 @@
|
|||||||
/target
|
/target
|
||||||
**/*.rs.bk
|
**/*.rs.bk
|
||||||
/build
|
|
||||||
|
|
||||||
# ignore vi temporaries
|
# ignore vi temporaries
|
||||||
*~
|
*~
|
||||||
@@ -26,10 +25,3 @@ deltachat-ffi/html
|
|||||||
deltachat-ffi/xml
|
deltachat-ffi/xml
|
||||||
|
|
||||||
.rsynclist
|
.rsynclist
|
||||||
|
|
||||||
coverage/
|
|
||||||
.DS_Store
|
|
||||||
.vscode/launch.json
|
|
||||||
python/accounts.txt
|
|
||||||
python/all-testaccounts.txt
|
|
||||||
tmp/
|
|
||||||
|
|||||||
1190
CHANGELOG.md
@@ -1,50 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.16)
|
|
||||||
project(deltachat LANGUAGES C)
|
|
||||||
include(GNUInstallDirs)
|
|
||||||
|
|
||||||
find_program(CARGO cargo)
|
|
||||||
|
|
||||||
if(APPLE)
|
|
||||||
set(DYNAMIC_EXT "dylib")
|
|
||||||
elseif(UNIX)
|
|
||||||
set(DYNAMIC_EXT "so")
|
|
||||||
else()
|
|
||||||
set(DYNAMIC_EXT "dll")
|
|
||||||
endif()
|
|
||||||
|
|
||||||
add_custom_command(
|
|
||||||
OUTPUT
|
|
||||||
"target/release/libdeltachat.a"
|
|
||||||
"target/release/libdeltachat.${DYNAMIC_EXT}"
|
|
||||||
"target/release/pkgconfig/deltachat.pc"
|
|
||||||
COMMAND
|
|
||||||
PREFIX=${CMAKE_INSTALL_PREFIX}
|
|
||||||
LIBDIR=${CMAKE_INSTALL_FULL_LIBDIR}
|
|
||||||
INCLUDEDIR=${CMAKE_INSTALL_FULL_INCLUDEDIR}
|
|
||||||
${CARGO} build --release --no-default-features
|
|
||||||
|
|
||||||
# Build in `deltachat-ffi` directory instead of using
|
|
||||||
# `--package deltachat_ffi` to avoid feature resolver version
|
|
||||||
# "1" bug which makes `--no-default-features` affect only
|
|
||||||
# `deltachat`, but not `deltachat-ffi` package.
|
|
||||||
#
|
|
||||||
# We can't enable version "2" resolver [1] because it is not
|
|
||||||
# stable yet on rust 1.50.0.
|
|
||||||
#
|
|
||||||
# [1] https://doc.rust-lang.org/nightly/cargo/reference/features.html#resolver-version-2-command-line-flags
|
|
||||||
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/deltachat-ffi
|
|
||||||
)
|
|
||||||
|
|
||||||
add_custom_target(
|
|
||||||
lib_deltachat
|
|
||||||
ALL
|
|
||||||
DEPENDS
|
|
||||||
"target/release/libdeltachat.a"
|
|
||||||
"target/release/libdeltachat.${DYNAMIC_EXT}"
|
|
||||||
"target/release/pkgconfig/deltachat.pc"
|
|
||||||
)
|
|
||||||
|
|
||||||
install(FILES "deltachat-ffi/deltachat.h" DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
|
|
||||||
install(FILES "target/release/libdeltachat.a" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
|
||||||
install(FILES "target/release/libdeltachat.${DYNAMIC_EXT}" DESTINATION ${CMAKE_INSTALL_LIBDIR})
|
|
||||||
install(FILES "target/release/pkgconfig/deltachat.pc" DESTINATION ${CMAKE_INSTALL_LIBDIR}/pkgconfig)
|
|
||||||
4562
Cargo.lock
generated
155
Cargo.toml
@@ -1,13 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat"
|
name = "deltachat"
|
||||||
version = "1.78.0"
|
version = "1.25.0"
|
||||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||||
edition = "2021"
|
edition = "2018"
|
||||||
license = "MPL-2.0"
|
license = "MPL-2.0"
|
||||||
rust-version = "1.56"
|
|
||||||
|
|
||||||
[profile.dev]
|
|
||||||
debug = 0
|
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
lto = true
|
lto = true
|
||||||
@@ -15,76 +11,61 @@ lto = true
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
deltachat_derive = { path = "./deltachat_derive" }
|
deltachat_derive = { path = "./deltachat_derive" }
|
||||||
|
|
||||||
ansi_term = { version = "0.12.1", optional = true }
|
libc = "0.2.51"
|
||||||
anyhow = "1"
|
pgp = { version = "0.4.0", default-features = false }
|
||||||
async-imap = { git = "https://github.com/async-email/async-imap" }
|
|
||||||
async-native-tls = { version = "0.3" }
|
|
||||||
async-smtp = { git = "https://github.com/async-email/async-smtp", branch="master", default-features=false, features = ["smtp-transport", "socks5"] }
|
|
||||||
async-std-resolver = "0.21"
|
|
||||||
async-std = { version = "1" }
|
|
||||||
async-tar = { version = "0.4", default-features=false }
|
|
||||||
async-trait = "0.1"
|
|
||||||
backtrace = "0.3"
|
|
||||||
base64 = "0.13"
|
|
||||||
bitflags = "1.3"
|
|
||||||
chrono = "0.4"
|
|
||||||
dirs = { version = "4", optional=true }
|
|
||||||
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
|
||||||
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
|
||||||
escaper = "0.1"
|
|
||||||
futures = "0.3"
|
|
||||||
hex = "0.4.0"
|
hex = "0.4.0"
|
||||||
image = { version = "0.24.1", default-features=false, features = ["gif", "jpeg", "ico", "png", "pnm", "webp", "bmp"] }
|
sha2 = "0.8.0"
|
||||||
kamadak-exif = "0.5"
|
rand = "0.7.0"
|
||||||
|
smallvec = "1.0.0"
|
||||||
|
reqwest = { version = "0.10.0", features = ["blocking", "json"] }
|
||||||
|
num-derive = "0.3.0"
|
||||||
|
num-traits = "0.2.6"
|
||||||
|
async-smtp = "0.2"
|
||||||
|
email = { git = "https://github.com/deltachat/rust-email", branch = "master" }
|
||||||
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
lettre_email = { git = "https://github.com/deltachat/lettre", branch = "master" }
|
||||||
libc = "0.2"
|
async-imap = "0.2"
|
||||||
log = {version = "0.4.16", optional = true }
|
async-native-tls = "0.3.1"
|
||||||
mailparse = "0.13"
|
async-std = { version = "1.4", features = ["unstable"] }
|
||||||
native-tls = "0.2"
|
base64 = "0.11"
|
||||||
num_cpus = "1.13"
|
charset = "0.1"
|
||||||
num-derive = "0.3"
|
|
||||||
num-traits = "0.2"
|
|
||||||
once_cell = "1.10.0"
|
|
||||||
percent-encoding = "2.0"
|
percent-encoding = "2.0"
|
||||||
pgp = { version = "0.7", default-features = false }
|
|
||||||
pretty_env_logger = { version = "0.4", optional = true }
|
|
||||||
quick-xml = "0.22"
|
|
||||||
r2d2 = "0.8"
|
|
||||||
r2d2_sqlite = "0.20"
|
|
||||||
rand = "0.7"
|
|
||||||
regex = "1.5"
|
|
||||||
rusqlite = { version = "0.27", features = ["sqlcipher"] }
|
|
||||||
rust-hsluv = "0.1"
|
|
||||||
rustyline = { version = "9", optional = true }
|
|
||||||
sanitize-filename = "0.3"
|
|
||||||
serde_json = "1.0"
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
sha-1 = "0.10"
|
serde_json = "1.0"
|
||||||
sha2 = "0.10"
|
chrono = "0.4.6"
|
||||||
smallvec = "1"
|
failure = "0.1.5"
|
||||||
strum = "0.24"
|
failure_derive = "0.1.5"
|
||||||
strum_macros = "0.24"
|
indexmap = "1.3.0"
|
||||||
surf = { version = "2.3", default-features = false, features = ["h1-client"] }
|
lazy_static = "1.4.0"
|
||||||
thiserror = "1"
|
regex = "1.1.6"
|
||||||
toml = "0.5"
|
rusqlite = { version = "0.21", features = ["bundled"] }
|
||||||
url = "2"
|
r2d2_sqlite = "0.13.0"
|
||||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
r2d2 = "0.8.5"
|
||||||
fast-socks5 = "0.4"
|
strum = "0.16.0"
|
||||||
humansize = "1"
|
strum_macros = "0.16.0"
|
||||||
qrcodegen = "1.7.0"
|
thread-local-object = "0.1.0"
|
||||||
tagger = "4.3.3"
|
backtrace = "0.3.33"
|
||||||
textwrap = "0.15.0"
|
byteorder = "1.3.1"
|
||||||
zip = { version = "0.6.2", default-features = false, features = ["deflate"] }
|
itertools = "0.8.0"
|
||||||
|
image-meta = "0.1.0"
|
||||||
|
quick-xml = "0.17.1"
|
||||||
|
escaper = "0.1.0"
|
||||||
|
bitflags = "1.1.0"
|
||||||
|
debug_stub_derive = "0.3.0"
|
||||||
|
sanitize-filename = "0.2.1"
|
||||||
|
stop-token = { version = "0.1.1", features = ["unstable"] }
|
||||||
|
mailparse = "0.10.2"
|
||||||
|
encoded-words = { git = "https://github.com/async-email/encoded-words", branch="master" }
|
||||||
|
native-tls = "0.2.3"
|
||||||
|
image = { version = "0.22.4", default-features=false, features = ["gif_codec", "jpeg", "ico", "png_codec", "pnm", "webp", "bmp"] }
|
||||||
|
pretty_env_logger = "0.3.1"
|
||||||
|
|
||||||
|
rustyline = { version = "4.1.0", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
ansi_term = "0.12.0"
|
tempfile = "3.0"
|
||||||
async-std = { version = "1", features = ["unstable", "attributes"] }
|
pretty_assertions = "0.6.1"
|
||||||
criterion = { version = "0.3.4", features = ["async_std"] }
|
pretty_env_logger = "0.3.0"
|
||||||
futures-lite = "1.12"
|
proptest = "0.9.4"
|
||||||
log = "0.4"
|
|
||||||
pretty_env_logger = "0.4"
|
|
||||||
proptest = { version = "1", default-features = false, features = ["std"] }
|
|
||||||
tempfile = "3"
|
|
||||||
|
|
||||||
[workspace]
|
[workspace]
|
||||||
members = [
|
members = [
|
||||||
@@ -95,41 +76,15 @@ members = [
|
|||||||
[[example]]
|
[[example]]
|
||||||
name = "simple"
|
name = "simple"
|
||||||
path = "examples/simple.rs"
|
path = "examples/simple.rs"
|
||||||
required-features = ["repl"]
|
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "repl"
|
name = "repl"
|
||||||
path = "examples/repl/main.rs"
|
path = "examples/repl/main.rs"
|
||||||
required-features = ["repl"]
|
required-features = ["rustyline"]
|
||||||
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "create_account"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "contacts"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "search_msgs"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "receive_emails"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "get_chat_msgs"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "get_chatlist"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["nightly", "ringbuf"]
|
||||||
internals = []
|
vendored = ["async-native-tls/vendored", "reqwest/native-tls-vendored", "async-smtp/native-tls-vendored"]
|
||||||
repl = ["internals", "rustyline", "log", "pretty_env_logger", "ansi_term", "dirs"]
|
|
||||||
vendored = ["async-native-tls/vendored", "async-smtp/native-tls-vendored", "rusqlite/bundled-sqlcipher-vendored-openssl"]
|
|
||||||
nightly = ["pgp/nightly"]
|
nightly = ["pgp/nightly"]
|
||||||
|
ringbuf = ["pgp/ringbuf"]
|
||||||
|
|||||||
38
README.md
@@ -2,26 +2,23 @@
|
|||||||
|
|
||||||
> Deltachat-core written in Rust
|
> Deltachat-core written in Rust
|
||||||
|
|
||||||
[](https://github.com/deltachat/deltachat-core-rust/actions/workflows/ci.yml)
|
[![CircleCI build status][circle-shield]][circle] [![Appveyor build status][appveyor-shield]][appveyor]
|
||||||
|
|
||||||
## Installing Rust and Cargo
|
## Installing Rust and Cargo
|
||||||
|
|
||||||
To download and install the official compiler for the Rust programming language, and the Cargo package manager, run the command in your user environment:
|
To download and install the official compiler for the Rust programming language, and the Cargo package manager, run the command in your user environment:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ curl https://sh.rustup.rs -sSf | sh
|
curl https://sh.rustup.rs -sSf | sh
|
||||||
```
|
```
|
||||||
|
|
||||||
> On Windows, you may need to also install **Perl** to be able to compile deltachat-core.
|
|
||||||
|
|
||||||
## Using the CLI client
|
## Using the CLI client
|
||||||
|
|
||||||
Compile and run Delta Chat Core command line utility, using `cargo`:
|
Compile and run Delta Chat Core command line utility, using `cargo`:
|
||||||
|
|
||||||
```
|
```
|
||||||
$ RUST_LOG=repl=info cargo run --example repl --features repl -- ~/deltachat-db
|
cargo run --example repl -- /path/to/db
|
||||||
```
|
```
|
||||||
where ~/deltachat-db is the database file. Delta Chat will create it if it does not exist.
|
|
||||||
|
|
||||||
Configure your account (if not already configured):
|
Configure your account (if not already configured):
|
||||||
|
|
||||||
@@ -81,16 +78,6 @@ For more commands type:
|
|||||||
> help
|
> help
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installing libdeltachat system wide
|
|
||||||
|
|
||||||
```
|
|
||||||
$ git clone https://github.com/deltachat/deltachat-core-rust.git
|
|
||||||
$ cd deltachat-core-rust
|
|
||||||
$ cmake -B build . -DCMAKE_INSTALL_PREFIX=/usr
|
|
||||||
$ cmake --build build
|
|
||||||
$ sudo cmake --install build
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
## Development
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -107,8 +94,7 @@ $ cargo build -p deltachat_ffi --release
|
|||||||
|
|
||||||
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
|
- `DCC_MIME_DEBUG`: if set outgoing and incoming message will be printed
|
||||||
|
|
||||||
- `RUST_LOG=repl=info,async_imap=trace,async_smtp=trace`: enable IMAP and
|
|
||||||
SMTP tracing in addition to info messages.
|
|
||||||
|
|
||||||
### Expensive tests
|
### Expensive tests
|
||||||
|
|
||||||
@@ -122,16 +108,21 @@ $ cargo test -- --ignored
|
|||||||
|
|
||||||
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
- `vendored`: When using Openssl for TLS, this bundles a vendored version.
|
||||||
- `nightly`: Enable nightly only performance and security related features.
|
- `nightly`: Enable nightly only performance and security related features.
|
||||||
|
- `ringbuf`: Enable the use of [`slice_deque`](https://github.com/gnzlbg/slice_deque) in pgp.
|
||||||
|
|
||||||
|
[circle-shield]: https://img.shields.io/circleci/project/github/deltachat/deltachat-core-rust/master.svg?style=flat-square
|
||||||
|
[circle]: https://circleci.com/gh/deltachat/deltachat-core-rust/
|
||||||
|
[appveyor-shield]: https://ci.appveyor.com/api/projects/status/lqpegel3ld4ipxj8/branch/master?style=flat-square
|
||||||
|
[appveyor]: https://ci.appveyor.com/project/dignifiedquire/deltachat-core-rust/branch/master
|
||||||
|
|
||||||
## Language bindings and frontend projects
|
## Language bindings and frontend projects
|
||||||
|
|
||||||
Language bindings are available for:
|
Language bindings are available for:
|
||||||
|
|
||||||
- **C** \[[📂 source](./deltachat-ffi) | [📚 docs](https://c.delta.chat)\]
|
- [C](https://c.delta.chat)
|
||||||
- **Node.js** \[[📂 source](https://github.com/deltachat/deltachat-node) | [📦 npm](https://www.npmjs.com/package/deltachat-node) | [📚 docs](https://js.delta.chat)\]
|
- [Node.js](https://www.npmjs.com/package/deltachat-node)
|
||||||
- **Python** \[[📂 source](./python) | [📦 pypi](https://pypi.org/project/deltachat) | [📚 docs](https://py.delta.chat)\]
|
- [Python](https://py.delta.chat)
|
||||||
- **Go** \[[📂 source](https://github.com/deltachat/go-deltachat/)\]
|
- [Go](https://github.com/hugot/go-deltachat/)
|
||||||
- **Free Pascal** \[[📂 source](https://github.com/deltachat/deltachat-fp/)\]
|
|
||||||
- **Java** and **Swift** (contained in the Android/iOS repos)
|
- **Java** and **Swift** (contained in the Android/iOS repos)
|
||||||
|
|
||||||
The following "frontend" projects make use of the Rust-library
|
The following "frontend" projects make use of the Rust-library
|
||||||
@@ -141,5 +132,4 @@ or its language bindings:
|
|||||||
- [iOS](https://github.com/deltachat/deltachat-ios)
|
- [iOS](https://github.com/deltachat/deltachat-ios)
|
||||||
- [Desktop](https://github.com/deltachat/deltachat-desktop)
|
- [Desktop](https://github.com/deltachat/deltachat-desktop)
|
||||||
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
|
- [Pidgin](https://code.ur.gs/lupine/purple-plugin-delta/)
|
||||||
- [Telepathy](https://code.ur.gs/lupine/telepathy-padfoot/)
|
|
||||||
- several **Bots**
|
- several **Bots**
|
||||||
|
|||||||
19
appveyor.yml
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
environment:
|
||||||
|
matrix:
|
||||||
|
- APPVEYOR_BUILD_WORKER_IMAGE: Visual Studio 2017
|
||||||
|
|
||||||
|
install:
|
||||||
|
- appveyor DownloadFile https://win.rustup.rs/ -FileName rustup-init.exe
|
||||||
|
- rustup-init -yv --default-toolchain nightly-2019-07-10
|
||||||
|
- set PATH=%PATH%;%USERPROFILE%\.cargo\bin
|
||||||
|
- rustc -vV
|
||||||
|
- cargo -vV
|
||||||
|
|
||||||
|
build: false
|
||||||
|
|
||||||
|
test_script:
|
||||||
|
- cargo test --release --all
|
||||||
|
|
||||||
|
cache:
|
||||||
|
- target
|
||||||
|
- C:\Users\appveyor\.cargo\registry
|
||||||
|
Before Width: | Height: | Size: 4.2 KiB |
@@ -1,149 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
enable-background="new 0 0 128 128"
|
|
||||||
viewBox="0 0 60 60"
|
|
||||||
version="1.1"
|
|
||||||
id="svg878"
|
|
||||||
sodipodi:docname="icon-broadcast.svg"
|
|
||||||
width="60"
|
|
||||||
height="60"
|
|
||||||
inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
|
|
||||||
inkscape:export-filename="/Users/bpetersen/projects/deltachat-core-rust/assets/icon-broadcast.png"
|
|
||||||
inkscape:export-xdpi="409.60001"
|
|
||||||
inkscape:export-ydpi="409.60001">
|
|
||||||
<metadata
|
|
||||||
id="metadata884">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title />
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<defs
|
|
||||||
id="defs882" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1"
|
|
||||||
objecttolerance="10"
|
|
||||||
gridtolerance="10"
|
|
||||||
guidetolerance="10"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:window-width="1329"
|
|
||||||
inkscape:window-height="847"
|
|
||||||
id="namedview880"
|
|
||||||
showgrid="false"
|
|
||||||
inkscape:zoom="5.21875"
|
|
||||||
inkscape:cx="36.598802"
|
|
||||||
inkscape:cy="32.191617"
|
|
||||||
inkscape:window-x="111"
|
|
||||||
inkscape:window-y="205"
|
|
||||||
inkscape:window-maximized="0"
|
|
||||||
inkscape:current-layer="svg878"
|
|
||||||
inkscape:document-rotation="0" />
|
|
||||||
<radialGradient
|
|
||||||
id="c"
|
|
||||||
cx="65.25"
|
|
||||||
cy="89"
|
|
||||||
r="26.440001"
|
|
||||||
gradientTransform="matrix(0.77611266,0.11996647,-0.18999676,1.2286617,-11.305867,-60.065999)"
|
|
||||||
gradientUnits="userSpaceOnUse">
|
|
||||||
<stop
|
|
||||||
stop-color="#FFC107"
|
|
||||||
offset="0"
|
|
||||||
id="stop833" />
|
|
||||||
<stop
|
|
||||||
stop-color="#FFBD06"
|
|
||||||
offset=".3502"
|
|
||||||
id="stop835" />
|
|
||||||
<stop
|
|
||||||
stop-color="#FFB104"
|
|
||||||
offset=".6938"
|
|
||||||
id="stop837" />
|
|
||||||
<stop
|
|
||||||
stop-color="#FFA000"
|
|
||||||
offset="1"
|
|
||||||
id="stop839" />
|
|
||||||
</radialGradient>
|
|
||||||
<radialGradient
|
|
||||||
id="b"
|
|
||||||
cx="52.5"
|
|
||||||
cy="19.75"
|
|
||||||
r="92.975998"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
gradientTransform="rotate(45.323856,68.997115,75.979538)">
|
|
||||||
<stop
|
|
||||||
stop-color="#EF5350"
|
|
||||||
offset="0"
|
|
||||||
id="stop848" />
|
|
||||||
<stop
|
|
||||||
stop-color="#EB4F4C"
|
|
||||||
offset=".246"
|
|
||||||
id="stop850" />
|
|
||||||
<stop
|
|
||||||
stop-color="#E04341"
|
|
||||||
offset=".4878"
|
|
||||||
id="stop852" />
|
|
||||||
<stop
|
|
||||||
stop-color="#CD302F"
|
|
||||||
offset=".7272"
|
|
||||||
id="stop854" />
|
|
||||||
<stop
|
|
||||||
stop-color="#C62828"
|
|
||||||
offset=".8004"
|
|
||||||
id="stop856" />
|
|
||||||
<stop
|
|
||||||
stop-color="#C62828"
|
|
||||||
offset="1"
|
|
||||||
id="stop858" />
|
|
||||||
</radialGradient>
|
|
||||||
<radialGradient
|
|
||||||
id="a"
|
|
||||||
cx="16.979"
|
|
||||||
cy="92"
|
|
||||||
r="24.165001"
|
|
||||||
gradientUnits="userSpaceOnUse"
|
|
||||||
gradientTransform="rotate(45.323856,68.997115,75.979538)"
|
|
||||||
xlink:href="#b">
|
|
||||||
<stop
|
|
||||||
stop-color="#E0E0E0"
|
|
||||||
offset="0"
|
|
||||||
id="stop863" />
|
|
||||||
<stop
|
|
||||||
stop-color="#CFCFCF"
|
|
||||||
offset=".3112"
|
|
||||||
id="stop865" />
|
|
||||||
<stop
|
|
||||||
stop-color="#A4A4A4"
|
|
||||||
offset=".9228"
|
|
||||||
id="stop867" />
|
|
||||||
<stop
|
|
||||||
stop-color="#9E9E9E"
|
|
||||||
offset="1"
|
|
||||||
id="stop869" />
|
|
||||||
</radialGradient>
|
|
||||||
<rect
|
|
||||||
y="0"
|
|
||||||
x="0"
|
|
||||||
height="60"
|
|
||||||
width="60"
|
|
||||||
id="rect1420"
|
|
||||||
style="fill:#7cc0bc;fill-opacity:1;stroke:none;stroke-width:1.29077" />
|
|
||||||
<path
|
|
||||||
id="path872"
|
|
||||||
style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:0.336872;stroke-opacity:1"
|
|
||||||
d="m 8.6780027,35.573064 0.032831,-11.910176 c 0.00138,-0.476406 0.4881282,-0.794259 0.9235226,-0.604877 l 4.1144877,2.345752 -0.02386,8.656315 -4.1268029,2.122946 C 9.1617452,36.370003 8.6766889,36.049472 8.6780027,35.573064 Z m 5.0469633,-1.508222 0.02386,-8.656314 31.145424,-9.537653 c 0.841472,-0.219211 1.65915,0.41667 1.656755,1.283728 l -0.06929,25.139995 c -0.0024,0.867062 -0.825942,1.500799 -1.663803,1.274581 z m 3.8042,6.892234 C 16.681121,40.104348 16.315444,38.819414 16.69043,37.591308 l 2.252234,-7.347193 c 0.2644,-0.861571 0.845185,-1.567441 1.641953,-1.989251 0.796769,-0.421808 1.706956,-0.509819 2.568531,-0.245419 l 7.263888,2.225804 c 1.775518,0.543235 2.780299,2.432591 2.232297,4.208094 L 30.3971,41.790532 c -0.545627,1.777887 -2.432591,2.780297 -4.208095,2.232298 l -7.263891,-2.225804 c -0.545033,-0.165864 -1.01825,-0.460162 -1.395948,-0.83995 z m 12.377693,-7.976728 c -0.07601,-0.07642 -0.17114,-0.133864 -0.280621,-0.167516 l -7.263891,-2.225803 c -0.233244,-0.07209 -0.421626,0.0013 -0.512275,0.04861 -0.09064,0.0474 -0.25772,0.166033 -0.327435,0.396899 l -2.252234,7.347191 c -0.108166,0.354628 0.09088,0.731541 0.447888,0.842099 l 7.263891,2.225802 c 0.354626,0.108174 0.731539,-0.09088 0.842099,-0.447888 l 2.249845,-7.344814 c 0.07453,-0.245145 0.0014,-0.504991 -0.167267,-0.67458 z" />
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 5.1 KiB |
|
Before Width: | Height: | Size: 3.8 KiB After Width: | Height: | Size: 4.8 KiB |
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 6.9 KiB |
@@ -1,181 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="80mm"
|
|
||||||
height="297mm"
|
|
||||||
viewBox="0 0 80 297"
|
|
||||||
version="1.1"
|
|
||||||
id="svg71"
|
|
||||||
inkscape:version="1.0.2 (e86c8708, 2021-01-15)"
|
|
||||||
sodipodi:docname="icon-webxdc.svg"
|
|
||||||
inkscape:export-filename="C:\Users\user\OneDrive - BFW-Leipzig\Documents\LogoDC\finalohnerand.png"
|
|
||||||
inkscape:export-xdpi="96"
|
|
||||||
inkscape:export-ydpi="96">
|
|
||||||
<metadata
|
|
||||||
id="metadata856">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="namedview73"
|
|
||||||
pagecolor="#767676"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pagecheckerboard="true"
|
|
||||||
inkscape:document-units="mm"
|
|
||||||
showgrid="false"
|
|
||||||
showborder="false"
|
|
||||||
inkscape:snap-bbox="true"
|
|
||||||
inkscape:bbox-paths="true"
|
|
||||||
inkscape:bbox-nodes="true"
|
|
||||||
inkscape:snap-bbox-edge-midpoints="true"
|
|
||||||
inkscape:snap-bbox-midpoints="true"
|
|
||||||
inkscape:object-paths="true"
|
|
||||||
inkscape:snap-intersection-paths="true"
|
|
||||||
inkscape:zoom="1.4142136"
|
|
||||||
inkscape:cx="-90.271136"
|
|
||||||
inkscape:cy="-1233.1209"
|
|
||||||
inkscape:window-width="1864"
|
|
||||||
inkscape:window-height="1027"
|
|
||||||
inkscape:window-x="56"
|
|
||||||
inkscape:window-y="25"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
inkscape:snap-global="false"
|
|
||||||
showguides="false"
|
|
||||||
inkscape:guide-bbox="true"
|
|
||||||
inkscape:document-rotation="0"
|
|
||||||
units="px">
|
|
||||||
<sodipodi:guide
|
|
||||||
position="-154.76097,641.11689"
|
|
||||||
orientation="0,-1"
|
|
||||||
id="guide21118" />
|
|
||||||
<sodipodi:guide
|
|
||||||
position="-60.286487,633.36619"
|
|
||||||
orientation="0,-1"
|
|
||||||
id="guide21120" />
|
|
||||||
</sodipodi:namedview>
|
|
||||||
<defs
|
|
||||||
id="defs68">
|
|
||||||
<linearGradient
|
|
||||||
id="linearGradient4375">
|
|
||||||
<stop
|
|
||||||
style="stop-color:#364e59;stop-opacity:1;"
|
|
||||||
offset="0"
|
|
||||||
id="stop4377" />
|
|
||||||
<stop
|
|
||||||
style="stop-color:#364e59;stop-opacity:0;"
|
|
||||||
offset="1"
|
|
||||||
id="stop4379" />
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
<g
|
|
||||||
inkscape:label="Ebene 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1">
|
|
||||||
<rect
|
|
||||||
style="fill:#1a1a1a;stroke:#000000;stroke-width:0.167903"
|
|
||||||
id="rect880"
|
|
||||||
width="79.8321"
|
|
||||||
height="79.8321"
|
|
||||||
x="-64.03286"
|
|
||||||
y="-375.9097"
|
|
||||||
ry="0" />
|
|
||||||
<path
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
id="path3799-2"
|
|
||||||
d="m -24.089585,-372.59579 c -19.986026,0.24336 -36.196903,16.666 -36.196903,36.67011 0,20.00409 16.210877,36.03233 36.196903,35.78912 19.0024236,-0.076 14.5340713,-10.6146 35.538854,-0.85693 -11.50627538,-17.97454 0.390097,-20.36737 0.658079,-35.81316 0,-20.00411 -16.2108788,-36.03235 -36.196911,-35.78914 z"
|
|
||||||
style="fill:#364e59;fill-opacity:1;stroke:none;stroke-width:1.93355"
|
|
||||||
sodipodi:nodetypes="sscccs" />
|
|
||||||
<path
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="M -54.193871,-325.26419 Z"
|
|
||||||
id="path3846" />
|
|
||||||
<path
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="M -49.397951,-326.67773 Z"
|
|
||||||
id="path3848" />
|
|
||||||
<path
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="m -49.397951,-326.67773 v 0 0"
|
|
||||||
id="path3850" />
|
|
||||||
<path
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:0.01;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="m -51.35133,-325.0334 -7.964067,5.98895 z"
|
|
||||||
id="path3965" />
|
|
||||||
<path
|
|
||||||
inkscape:connector-curvature="0"
|
|
||||||
id="path11037"
|
|
||||||
d="m -24.089585,-372.19891 c -19.986026,0.24156 -36.196903,16.54352 -36.196903,36.40062 0,7.86524 2.543315,15.1113 6.857155,20.97971 6.577146,8.94734 11.123515,9.77363 11.123515,9.77363 1.343237,1.78324 10.270932,4.3223 10.270932,4.3223 l 16.791727,-70.86654 -0.468369,-0.33457 c 0.458597,0.26445 0.428277,-0.27515 -8.378035,-0.27515 z"
|
|
||||||
style="fill:#7cc5cc;fill-opacity:1;stroke:none;stroke-width:1.92643"
|
|
||||||
sodipodi:nodetypes="sssccccss" />
|
|
||||||
<path
|
|
||||||
style="fill:none;stroke:#000000;stroke-width:0.264583px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
|
|
||||||
d="M -49.944239,-310.69957 Z"
|
|
||||||
id="path13674" />
|
|
||||||
<g
|
|
||||||
id="g15178"
|
|
||||||
transform="matrix(0.79975737,0,0,0.79975737,53.088959,-63.716396)">
|
|
||||||
<rect
|
|
||||||
style="fill:#364e59;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
|
|
||||||
id="rect15072"
|
|
||||||
width="29.897917"
|
|
||||||
height="6.8791666"
|
|
||||||
x="-334.4964"
|
|
||||||
y="-154.51025"
|
|
||||||
transform="rotate(45)" />
|
|
||||||
<rect
|
|
||||||
style="fill:#364e59;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
|
|
||||||
id="rect15072-5"
|
|
||||||
width="29.897917"
|
|
||||||
height="6.8791666"
|
|
||||||
x="147.63107"
|
|
||||||
y="-334.4964"
|
|
||||||
transform="rotate(-45)"
|
|
||||||
inkscape:transform-center-x="-0.74835017"
|
|
||||||
inkscape:transform-center-y="0.37417525" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
id="g22468"
|
|
||||||
transform="translate(3.3033974)">
|
|
||||||
<g
|
|
||||||
id="g15178-0"
|
|
||||||
transform="matrix(-0.79975737,0,0,0.79975737,-103.11028,-63.716404)"
|
|
||||||
style="fill:#7cc5cc;fill-opacity:1">
|
|
||||||
<rect
|
|
||||||
style="fill:#7cc5cc;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
|
|
||||||
id="rect15072-2"
|
|
||||||
width="29.897917"
|
|
||||||
height="6.8791666"
|
|
||||||
x="-334.4964"
|
|
||||||
y="-154.51025"
|
|
||||||
transform="rotate(45)" />
|
|
||||||
<rect
|
|
||||||
style="fill:#7cc5cc;fill-opacity:1;stroke-width:0.01;stroke-miterlimit:4;stroke-dasharray:none"
|
|
||||||
id="rect15072-5-5"
|
|
||||||
width="29.897917"
|
|
||||||
height="6.8791666"
|
|
||||||
x="147.63107"
|
|
||||||
y="-334.4964"
|
|
||||||
transform="rotate(-45)"
|
|
||||||
inkscape:transform-center-x="-0.74835017"
|
|
||||||
inkscape:transform-center-y="0.37417525" />
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 6.5 KiB |
@@ -1,10 +0,0 @@
|
|||||||
<text
|
|
||||||
xml:space="preserve"
|
|
||||||
style="font-weight:bold;font-size:24.4118px;line-height:1.25;font-family:sans-serif;fill:#aaaaaa;fill-opacity:1;stroke:none;stroke-width:0.915439"
|
|
||||||
x="42.325161"
|
|
||||||
y="23.32255"
|
|
||||||
id="text72398">get.delta.chat</text>
|
|
||||||
<path
|
|
||||||
id="path84310"
|
|
||||||
style="opacity:0.25;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.915439"
|
|
||||||
d="M 17.13769,0.00129321 C 7.6753075,0.11650893 0,7.8915283 0,17.362467 c 0,9.47094 7.6753075,17.059745 17.13769,16.944599 8.99669,-0.03598 6.880074,-5.025654 16.824785,-0.405885 -5.447648,-8.510047 0.184241,-9.642482 0.311117,-16.955289 0,-9.4709395 -7.673512,-17.0597453 -17.135895,-16.94459879 z M 17.0769,4.9986797 c 1.84214,0 3.447355,0.253959 4.815003,0.7616693 1.381603,0.5076411 2.072253,1.207862 2.072253,2.0990711 0,0.4286855 -0.167495,0.7836052 -0.50242,1.0656242 -0.334921,0.2819844 -0.724544,0.4237724 -1.171121,0.4237724 -0.641952,0 -1.396532,-0.3909376 -2.261778,-1.169353 C 19.14963,7.3898036 18.402555,6.83791 17.788507,6.5220182 17.188416,6.1950547 16.484552,6.0321266 15.675129,6.0321266 c -1.032717,0 -1.883352,0.1854523 -2.553215,0.5578447 -0.655913,0.372254 -0.98517,0.8460916 -0.98517,1.4214436 0,0.5414792 0.272815,1.0495355 0.817093,1.5233385 0.544275,0.4738026 1.946291,1.3367446 4.207097,2.5889976 2.414319,1.342419 4.117377,2.390985 5.108232,3.146807 1.004795,0.755857 1.821505,1.675853 2.449514,2.758846 0.628002,1.082993 0.942253,2.227607 0.942253,3.434674 0,2.120834 -0.929555,3.993314 -2.785656,5.617786 -1.84214,1.613228 -3.99694,2.41915 -6.467082,2.41915 -2.246845,0 -4.145607,-0.647976 -5.694677,-1.945312 -1.5490699,-1.297336 -2.3225722,-3.028063 -2.3225722,-5.194049 0,-2.087031 0.8506345,-3.83094 2.5532182,-5.229825 1.716541,-1.398884 3.824203,-2.245599 6.322256,-2.538897 -0.697774,-0.631749 -1.668763,-1.387225 -2.910816,-2.267155 -1.367648,-0.970199 -2.287914,-1.73045 -2.762402,-2.283243 -0.474491,-0.5640381 -0.711618,-1.1795944 -0.711618,-1.8451814 0,-0.9927581 0.572093,-1.7710351 1.716451,-2.3351077 1.144362,-0.5753173 2.636724,-0.8635642 4.478865,-0.8635642 z m 1.110327,10.3738083 c -4.005262,0.5302 -6.007576,2.75279 -6.007576,6.667322 0,2.01932 0.49495,3.587291 1.485805,4.704157 1.004806,1.116832 2.169696,1.675299 3.495479,1.675299 1.381602,0 2.520072,-0.535632 3.413229,-1.60738 0.893168,-1.082959 1.339187,-2.545264 1.339187,-4.384079 0,-2.662348 -1.242022,-5.013441 -3.726124,-7.055319 z" />
|
|
||||||
|
Before Width: | Height: | Size: 112 KiB After Width: | Height: | Size: 113 KiB |
@@ -1,37 +0,0 @@
|
|||||||
use async_std::task::block_on;
|
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
|
||||||
use deltachat::contact::Contact;
|
|
||||||
use deltachat::context::Context;
|
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
async fn address_book_benchmark(n: u32, read_count: u32) {
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
let dbfile = dir.path().join("db.sqlite");
|
|
||||||
let id = 100;
|
|
||||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
|
||||||
|
|
||||||
let book = (0..n)
|
|
||||||
.map(|i| format!("Name {}\naddr{}@example.org\n", i, i))
|
|
||||||
.collect::<Vec<String>>()
|
|
||||||
.join("");
|
|
||||||
|
|
||||||
Contact::add_address_book(&context, &book).await.unwrap();
|
|
||||||
|
|
||||||
let query: Option<&str> = None;
|
|
||||||
for _ in 0..read_count {
|
|
||||||
Contact::get_all(&context, 0, query).await.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn criterion_benchmark(c: &mut Criterion) {
|
|
||||||
c.bench_function("create 500 contacts", |b| {
|
|
||||||
b.iter(|| block_on(async { address_book_benchmark(black_box(500), black_box(0)).await }))
|
|
||||||
});
|
|
||||||
|
|
||||||
c.bench_function("create 100 contacts and read it 1000 times", |b| {
|
|
||||||
b.iter(|| block_on(async { address_book_benchmark(black_box(100), black_box(1000)).await }))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
criterion_group!(benches, criterion_benchmark);
|
|
||||||
criterion_main!(benches);
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
use async_std::path::PathBuf;
|
|
||||||
use async_std::task::block_on;
|
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
|
||||||
use deltachat::accounts::Accounts;
|
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
async fn create_accounts(n: u32) {
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
let p: PathBuf = dir.path().join("accounts").into();
|
|
||||||
|
|
||||||
let mut accounts = Accounts::new(p.clone()).await.unwrap();
|
|
||||||
|
|
||||||
for expected_id in 2..n {
|
|
||||||
let id = accounts.add_account().await.unwrap();
|
|
||||||
assert_eq!(id, expected_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn criterion_benchmark(c: &mut Criterion) {
|
|
||||||
c.bench_function("create 1 account", |b| {
|
|
||||||
b.iter(|| block_on(async { create_accounts(black_box(1)).await }))
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
criterion_group!(benches, criterion_benchmark);
|
|
||||||
criterion_main!(benches);
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
use async_std::path::Path;
|
|
||||||
|
|
||||||
use criterion::async_executor::AsyncStdExecutor;
|
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
|
||||||
|
|
||||||
use deltachat::chat::{self, ChatId};
|
|
||||||
use deltachat::chatlist::Chatlist;
|
|
||||||
use deltachat::context::Context;
|
|
||||||
|
|
||||||
async fn get_chat_msgs_benchmark(dbfile: &Path, chats: &[ChatId]) {
|
|
||||||
let id = 100;
|
|
||||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
|
||||||
|
|
||||||
for c in chats.iter().take(10) {
|
|
||||||
black_box(chat::get_chat_msgs(&context, *c, 0, None).await.ok());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn criterion_benchmark(c: &mut Criterion) {
|
|
||||||
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
|
|
||||||
// messages, such as your primary account.
|
|
||||||
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
|
|
||||||
let chats: Vec<_> = async_std::task::block_on(async {
|
|
||||||
let context = Context::new((&path).into(), 100).await.unwrap();
|
|
||||||
let chatlist = Chatlist::try_load(&context, 0, None, None).await.unwrap();
|
|
||||||
let len = chatlist.len();
|
|
||||||
(0..len).map(|i| chatlist.get_chat_id(i).unwrap()).collect()
|
|
||||||
});
|
|
||||||
|
|
||||||
c.bench_function("chat::get_chat_msgs (load messages from 10 chats)", |b| {
|
|
||||||
b.to_async(AsyncStdExecutor)
|
|
||||||
.iter(|| get_chat_msgs_benchmark(black_box(path.as_ref()), black_box(&chats)))
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
criterion_group!(benches, criterion_benchmark);
|
|
||||||
criterion_main!(benches);
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
use criterion::async_executor::AsyncStdExecutor;
|
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
|
||||||
|
|
||||||
use deltachat::chatlist::Chatlist;
|
|
||||||
use deltachat::context::Context;
|
|
||||||
|
|
||||||
async fn get_chat_list_benchmark(context: &Context) {
|
|
||||||
Chatlist::try_load(context, 0, None, None).await.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn criterion_benchmark(c: &mut Criterion) {
|
|
||||||
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
|
|
||||||
// messages, such as your primary account.
|
|
||||||
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
|
|
||||||
let context =
|
|
||||||
async_std::task::block_on(async { Context::new(path.into(), 100).await.unwrap() });
|
|
||||||
c.bench_function("chatlist:try_load (Get Chatlist)", |b| {
|
|
||||||
b.to_async(AsyncStdExecutor)
|
|
||||||
.iter(|| get_chat_list_benchmark(black_box(&context)))
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
criterion_group!(benches, criterion_benchmark);
|
|
||||||
criterion_main!(benches);
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
use async_std::{path::PathBuf, task::block_on};
|
|
||||||
use criterion::{
|
|
||||||
async_executor::AsyncStdExecutor, black_box, criterion_group, criterion_main, BatchSize,
|
|
||||||
Criterion,
|
|
||||||
};
|
|
||||||
use deltachat::{
|
|
||||||
config::Config,
|
|
||||||
context::Context,
|
|
||||||
dc_receive_imf::dc_receive_imf,
|
|
||||||
imex::{imex, ImexMode},
|
|
||||||
};
|
|
||||||
use tempfile::tempdir;
|
|
||||||
|
|
||||||
async fn recv_all_emails(context: Context) -> Context {
|
|
||||||
for i in 0..100 {
|
|
||||||
let imf_raw = format!(
|
|
||||||
"Subject: Benchmark
|
|
||||||
Message-ID: Mr.OssSYnOFkhR.{i}@testrun.org
|
|
||||||
Date: Sat, 07 Dec 2019 19:00:27 +0000
|
|
||||||
To: alice@example.com
|
|
||||||
From: sender@testrun.org
|
|
||||||
Chat-Version: 1.0
|
|
||||||
Chat-Disposition-Notification-To: sender@testrun.org
|
|
||||||
Chat-User-Avatar: 0
|
|
||||||
In-Reply-To: Mr.OssSYnOFkhR.{i_dec}@testrun.org
|
|
||||||
MIME-Version: 1.0
|
|
||||||
|
|
||||||
Content-Type: text/plain; charset=utf-8; format=flowed; delsp=no
|
|
||||||
|
|
||||||
Hello {i}",
|
|
||||||
i = i,
|
|
||||||
i_dec = i - 1,
|
|
||||||
);
|
|
||||||
dc_receive_imf(&context, black_box(imf_raw.as_bytes()), false)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
context
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn create_context() -> Context {
|
|
||||||
let dir = tempdir().unwrap();
|
|
||||||
let dbfile = dir.path().join("db.sqlite");
|
|
||||||
let id = 100;
|
|
||||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
|
||||||
|
|
||||||
let backup: PathBuf = std::env::current_dir()
|
|
||||||
.unwrap()
|
|
||||||
.join("delta-chat-backup.tar")
|
|
||||||
.into();
|
|
||||||
if backup.exists().await {
|
|
||||||
println!("Importing backup");
|
|
||||||
imex(&context, ImexMode::ImportBackup, &backup, None)
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
let addr = "alice@example.com";
|
|
||||||
context.set_config(Config::Addr, Some(addr)).await.unwrap();
|
|
||||||
context
|
|
||||||
.set_config(Config::ConfiguredAddr, Some(addr))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
context
|
|
||||||
.set_config(Config::Configured, Some("1"))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
context
|
|
||||||
}
|
|
||||||
|
|
||||||
fn criterion_benchmark(c: &mut Criterion) {
|
|
||||||
let mut group = c.benchmark_group("Receive messages");
|
|
||||||
group.bench_function("Receive 100 simple text msgs", |b| {
|
|
||||||
b.to_async(AsyncStdExecutor).iter_batched(
|
|
||||||
|| block_on(create_context()),
|
|
||||||
|context| recv_all_emails(black_box(context)),
|
|
||||||
BatchSize::LargeInput,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
group.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
criterion_group!(benches, criterion_benchmark);
|
|
||||||
criterion_main!(benches);
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
use async_std::task::block_on;
|
|
||||||
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
|
||||||
use deltachat::context::Context;
|
|
||||||
use std::path::Path;
|
|
||||||
|
|
||||||
async fn search_benchmark(path: impl AsRef<Path>) {
|
|
||||||
let dbfile = path.as_ref();
|
|
||||||
let id = 100;
|
|
||||||
let context = Context::new(dbfile.into(), id).await.unwrap();
|
|
||||||
|
|
||||||
for _ in 0..10u32 {
|
|
||||||
context.search_msgs(None, "hello").await.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn criterion_benchmark(c: &mut Criterion) {
|
|
||||||
// To enable this benchmark, set `DELTACHAT_BENCHMARK_DATABASE` to some large database with many
|
|
||||||
// messages, such as your primary account.
|
|
||||||
if let Ok(path) = std::env::var("DELTACHAT_BENCHMARK_DATABASE") {
|
|
||||||
c.bench_function("search hello", |b| {
|
|
||||||
b.iter(|| block_on(async { search_benchmark(black_box(&path)).await }))
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
println!("env var not set: DELTACHAT_BENCHMARK_DATABASE");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
criterion_group!(benches, criterion_benchmark);
|
|
||||||
criterion_main!(benches);
|
|
||||||
@@ -1,12 +1,11 @@
|
|||||||
# Continuous Integration Scripts for Delta Chat
|
# Continuous Integration Scripts for Delta Chat
|
||||||
|
|
||||||
Continuous Integration, run through [GitHub
|
Continuous Integration, run through CircleCI and an own build machine.
|
||||||
Actions](https://docs.github.com/actions)
|
|
||||||
and an own build machine.
|
|
||||||
|
|
||||||
## Description of scripts
|
## Description of scripts
|
||||||
|
|
||||||
- `../.github/workflows` contains jobs run by GitHub Actions.
|
- `../.circleci/config.yml` describing the build jobs that are run
|
||||||
|
by Circle-CI
|
||||||
|
|
||||||
- `remote_tests_python.sh` rsyncs to a build machine and runs
|
- `remote_tests_python.sh` rsyncs to a build machine and runs
|
||||||
`run-python-test.sh` remotely on the build machine.
|
`run-python-test.sh` remotely on the build machine.
|
||||||
@@ -27,8 +26,8 @@ There is experimental support for triggering a remote Python or Rust test run
|
|||||||
from your local checkout/branch. You will need to be authorized to login to
|
from your local checkout/branch. You will need to be authorized to login to
|
||||||
the build machine (ask your friendly sysadmin on #deltachat freenode) to type::
|
the build machine (ask your friendly sysadmin on #deltachat freenode) to type::
|
||||||
|
|
||||||
scripts/manual_remote_tests.sh rust
|
ci_scripts/manual_remote_tests.sh rust
|
||||||
scripts/manual_remote_tests.sh python
|
ci_scripts/manual_remote_tests.sh python
|
||||||
|
|
||||||
This will **rsync** your current checkout to the remote build machine
|
This will **rsync** your current checkout to the remote build machine
|
||||||
(no need to commit before) and then run either rust or python tests.
|
(no need to commit before) and then run either rust or python tests.
|
||||||
@@ -42,10 +41,6 @@ python tests and build wheels (binary packages for Python)
|
|||||||
You can build the docker images yourself locally
|
You can build the docker images yourself locally
|
||||||
to avoid the relatively large download::
|
to avoid the relatively large download::
|
||||||
|
|
||||||
cd scripts # where all CI things are
|
cd ci_scripts # where all CI things are
|
||||||
docker build -t deltachat/coredeps docker-coredeps
|
docker build -t deltachat/coredeps docker-coredeps
|
||||||
docker build -t deltachat/doxygen docker-doxygen
|
docker build -t deltachat/doxygen docker-doxygen
|
||||||
|
|
||||||
Additionally, you can install qemu and build arm64 docker image:
|
|
||||||
apt-get install qemu binfmt-support qemu-user-static
|
|
||||||
docker build -t deltachat/coredeps-arm64 docker-coredeps-arm64
|
|
||||||
57
ci_scripts/ci_upload.sh
Executable file
@@ -0,0 +1,57 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ -z "$DEVPI_LOGIN" ] ; then
|
||||||
|
echo "required: password for 'dc' user on https://m.devpi/net/dc index"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -xe
|
||||||
|
|
||||||
|
PYDOCDIR=${1:?directory with python docs}
|
||||||
|
WHEELHOUSEDIR=${2:?directory with pre-built wheels}
|
||||||
|
DOXYDOCDIR=${3:?directory where doxygen docs to be found}
|
||||||
|
|
||||||
|
export BRANCH=${CIRCLE_BRANCH:?specify branch for uploading purposes}
|
||||||
|
|
||||||
|
|
||||||
|
# python docs to py.delta.chat
|
||||||
|
ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@py.delta.chat mkdir -p build/${BRANCH}
|
||||||
|
rsync -avz \
|
||||||
|
--delete \
|
||||||
|
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
||||||
|
"$PYDOCDIR/html/" \
|
||||||
|
delta@py.delta.chat:build/${BRANCH}
|
||||||
|
|
||||||
|
# C docs to c.delta.chat
|
||||||
|
ssh -o BatchMode=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null delta@c.delta.chat mkdir -p build-c/${BRANCH}
|
||||||
|
rsync -avz \
|
||||||
|
--delete \
|
||||||
|
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
||||||
|
"$DOXYDOCDIR/html/" \
|
||||||
|
delta@c.delta.chat:build-c/${BRANCH}
|
||||||
|
|
||||||
|
echo -----------------------
|
||||||
|
echo upload wheels
|
||||||
|
echo -----------------------
|
||||||
|
|
||||||
|
# Bundle external shared libraries into the wheels
|
||||||
|
pushd $WHEELHOUSEDIR
|
||||||
|
|
||||||
|
pip3 install -U pip
|
||||||
|
pip3 install devpi-client
|
||||||
|
devpi use https://m.devpi.net
|
||||||
|
devpi login dc --password $DEVPI_LOGIN
|
||||||
|
|
||||||
|
N_BRANCH=${BRANCH//[\/]}
|
||||||
|
|
||||||
|
devpi use dc/$N_BRANCH || {
|
||||||
|
devpi index -c $N_BRANCH
|
||||||
|
devpi use dc/$N_BRANCH
|
||||||
|
}
|
||||||
|
devpi index $N_BRANCH bases=/root/pypi
|
||||||
|
devpi upload deltachat*
|
||||||
|
|
||||||
|
popd
|
||||||
|
|
||||||
|
# remove devpi non-master dc indices if thy are too old
|
||||||
|
python ci_scripts/cleanup_devpi_indices.py
|
||||||
@@ -48,7 +48,7 @@ def run():
|
|||||||
projectnames = get_projectnames(baseurl, username, indexname)
|
projectnames = get_projectnames(baseurl, username, indexname)
|
||||||
if indexname == "master" or not indexname:
|
if indexname == "master" or not indexname:
|
||||||
continue
|
continue
|
||||||
clear_index = not projectnames
|
assert projectnames == ["deltachat"]
|
||||||
for projectname in projectnames:
|
for projectname in projectnames:
|
||||||
dates = get_release_dates(baseurl, username, indexname, projectname)
|
dates = get_release_dates(baseurl, username, indexname, projectname)
|
||||||
if not dates:
|
if not dates:
|
||||||
@@ -60,11 +60,8 @@ def run():
|
|||||||
date = datetime.datetime(*max(dates))
|
date = datetime.datetime(*max(dates))
|
||||||
if (datetime.datetime.now() - date) > datetime.timedelta(days=MAXDAYS):
|
if (datetime.datetime.now() - date) > datetime.timedelta(days=MAXDAYS):
|
||||||
assert username and indexname
|
assert username and indexname
|
||||||
clear_index = True
|
url = baseurl + username + "/" + indexname
|
||||||
break
|
subprocess.check_call(["devpi", "index", "-y", "--delete", url])
|
||||||
if clear_index:
|
|
||||||
url = baseurl + username + "/" + indexname
|
|
||||||
subprocess.check_call(["devpi", "index", "-y", "--delete", url])
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
FROM quay.io/pypa/manylinux2014_aarch64
|
FROM quay.io/pypa/manylinux1_x86_64
|
||||||
|
|
||||||
# Configure ld.so/ldconfig and pkg-config
|
# Configure ld.so/ldconfig and pkg-config
|
||||||
RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
|
RUN echo /usr/local/lib64 > /etc/ld.so.conf.d/local.conf && \
|
||||||
echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf
|
echo /usr/local/lib >> /etc/ld.so.conf.d/local.conf
|
||||||
ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig
|
ENV PKG_CONFIG_PATH /usr/local/lib64/pkgconfig:/usr/local/lib/pkgconfig
|
||||||
|
|
||||||
# Install a recent Perl, needed to install the openssl crate
|
# Install a recent Perl, needed to install the openssl crate
|
||||||
ADD deps/build_perl.sh /builder/build_perl.sh
|
ADD deps/build_perl.sh /builder/build_perl.sh
|
||||||
RUN rm /usr/bin/perl
|
RUN rm /usr/bin/perl
|
||||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
|
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
|
||||||
@@ -16,6 +16,6 @@ ENV PIP_DISABLE_PIP_VERSION_CHECK 1
|
|||||||
ADD deps/build_python.sh /builder/build_python.sh
|
ADD deps/build_python.sh /builder/build_python.sh
|
||||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_python.sh && cd .. && rm -r tmp1
|
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_python.sh && cd .. && rm -r tmp1
|
||||||
|
|
||||||
# Install Rust
|
# Install Rust nightly
|
||||||
ADD deps/build_rust.sh /builder/build_rust.sh
|
ADD deps/build_rust.sh /builder/build_rust.sh
|
||||||
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1
|
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
PERL_VERSION=5.34.0
|
PERL_VERSION=5.30.0
|
||||||
# PERL_SHA256=551efc818b968b05216024fb0b727ef2ad4c100f8cb6b43fab615fa78ae5be9a
|
# PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
|
||||||
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
|
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
|
||||||
# echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
|
# echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
|
||||||
tar -xzf perl-${PERL_VERSION}.tar.gz
|
tar -xzf perl-${PERL_VERSION}.tar.gz
|
||||||
14
ci_scripts/docker-coredeps/deps/build_python.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -x -e
|
||||||
|
|
||||||
|
# we use the python3.5 environment as the base environment
|
||||||
|
/opt/python/cp35-cp35m/bin/pip install tox devpi-client auditwheel
|
||||||
|
|
||||||
|
pushd /usr/bin
|
||||||
|
|
||||||
|
ln -s /opt/_internal/cpython-3.5.*/bin/tox
|
||||||
|
ln -s /opt/_internal/cpython-3.5.*/bin/devpi
|
||||||
|
ln -s /opt/_internal/cpython-3.5.*/bin/auditwheel
|
||||||
|
|
||||||
|
popd
|
||||||
11
ci_scripts/docker-coredeps/deps/build_rust.sh
Executable file
@@ -0,0 +1,11 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e -x
|
||||||
|
|
||||||
|
# Install Rust
|
||||||
|
curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly-2019-11-06 -y
|
||||||
|
export PATH=/root/.cargo/bin:$PATH
|
||||||
|
rustc --version
|
||||||
|
|
||||||
|
# remove some 300-400 MB that we don't need for automated builds
|
||||||
|
rm -rf /root/.rustup/toolchains/nightly-2019-11-06-x86_64-unknown-linux-gnu/share/
|
||||||
9
ci_scripts/manual_remote_tests.sh
Executable file
@@ -0,0 +1,9 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -xe
|
||||||
|
export CIRCLE_JOB=remote_tests_${1:?need to specify 'rust' or 'python'}
|
||||||
|
export CIRCLE_BUILD_NUM=$USER
|
||||||
|
export CIRCLE_BRANCH=`git branch | grep \* | cut -d ' ' -f2`
|
||||||
|
export CIRCLE_PROJECT_REPONAME=$(basename `git rev-parse --show-toplevel`)
|
||||||
|
|
||||||
|
time bash ci_scripts/$CIRCLE_JOB.sh
|
||||||
77
ci_scripts/old/gh-actions-rust.yml
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
pull_request:
|
||||||
|
push:
|
||||||
|
|
||||||
|
env:
|
||||||
|
RUSTFLAGS: -Dwarnings
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build_and_test:
|
||||||
|
name: Build and test
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
os: [ubuntu-latest, windows-latest, macOS-latest]
|
||||||
|
rust: [nightly]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
|
||||||
|
- name: Install ${{ matrix.rust }}
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: ${{ matrix.rust }}
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: check
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
if: matrix.rust == 'nightly'
|
||||||
|
with:
|
||||||
|
command: check
|
||||||
|
args: --all --bins --examples --tests
|
||||||
|
|
||||||
|
- name: tests
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
|
args: --all
|
||||||
|
|
||||||
|
- name: tests ignored
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: test
|
||||||
|
args: --all --release -- --ignored
|
||||||
|
|
||||||
|
check_fmt:
|
||||||
|
name: Checking fmt and docs
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@master
|
||||||
|
|
||||||
|
- uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
profile: minimal
|
||||||
|
toolchain: nightly
|
||||||
|
override: true
|
||||||
|
components: rustfmt
|
||||||
|
|
||||||
|
- name: fmt
|
||||||
|
run: cargo fmt --all -- --check
|
||||||
|
|
||||||
|
# clippy_check:
|
||||||
|
# name: Clippy check
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
#
|
||||||
|
# steps:
|
||||||
|
# - uses: actions/checkout@v1
|
||||||
|
# - uses: actions-rs/toolchain@v1
|
||||||
|
# with:
|
||||||
|
# profile: minimal
|
||||||
|
# toolchain: nightly
|
||||||
|
# override: true
|
||||||
|
# components: clippy
|
||||||
|
#
|
||||||
|
# - name: clippy
|
||||||
|
# run: cargo clippy --all
|
||||||
60
ci_scripts/old/run-python.sh
Executable file
@@ -0,0 +1,60 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Build the Delta Chat C/Rust library typically run in a docker
|
||||||
|
# container that contains all library deps but should also work
|
||||||
|
# outside if you have the dependencies installed on your system.
|
||||||
|
|
||||||
|
set -e -x
|
||||||
|
|
||||||
|
# Perform clean build of core and install.
|
||||||
|
export TOXWORKDIR=.docker-tox
|
||||||
|
|
||||||
|
# install core lib
|
||||||
|
|
||||||
|
export PATH=/root/.cargo/bin:$PATH
|
||||||
|
cargo build --release -p deltachat_ffi
|
||||||
|
# cargo test --all --all-features
|
||||||
|
|
||||||
|
# Statically link against libdeltachat.a.
|
||||||
|
export DCC_RS_DEV=$(pwd)
|
||||||
|
|
||||||
|
# Configure access to a base python and to several python interpreters
|
||||||
|
# needed by tox below.
|
||||||
|
export PATH=$PATH:/opt/python/cp35-cp35m/bin
|
||||||
|
export PYTHONDONTWRITEBYTECODE=1
|
||||||
|
pushd /bin
|
||||||
|
ln -s /opt/python/cp27-cp27m/bin/python2.7
|
||||||
|
ln -s /opt/python/cp36-cp36m/bin/python3.6
|
||||||
|
ln -s /opt/python/cp37-cp37m/bin/python3.7
|
||||||
|
popd
|
||||||
|
|
||||||
|
if [ -n "$TESTS" ]; then
|
||||||
|
|
||||||
|
pushd python
|
||||||
|
# prepare a clean tox run
|
||||||
|
rm -rf tests/__pycache__
|
||||||
|
rm -rf src/deltachat/__pycache__
|
||||||
|
export PYTHONDONTWRITEBYTECODE=1
|
||||||
|
|
||||||
|
# run tox. The circle-ci project env-var-setting DCC_PY_LIVECONFIG
|
||||||
|
# allows running of "liveconfig" tests but for speed reasons
|
||||||
|
# we run them only for the highest python version we support
|
||||||
|
|
||||||
|
# we split out qr-tests run to minimize likelyness of flaky tests
|
||||||
|
# (some qr tests are pretty heavy in terms of send/received
|
||||||
|
# messages and rust's imap code likely has concurrency problems)
|
||||||
|
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "not qr"
|
||||||
|
tox --workdir "$TOXWORKDIR" -e py37 -- --reruns 3 -k "qr"
|
||||||
|
unset DCC_PY_LIVECONFIG
|
||||||
|
tox --workdir "$TOXWORKDIR" -p4 -e lint,py35,py36,doc
|
||||||
|
tox --workdir "$TOXWORKDIR" -e auditwheels
|
||||||
|
popd
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
# if [ -n "$DOCS" ]; then
|
||||||
|
# echo -----------------------
|
||||||
|
# echo generating python docs
|
||||||
|
# echo -----------------------
|
||||||
|
# (cd python && tox --workdir "$TOXWORKDIR" -e doc)
|
||||||
|
# fi
|
||||||
51
ci_scripts/remote_python_packaging.sh
Executable file
@@ -0,0 +1,51 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export BRANCH=${CIRCLE_BRANCH:?branch to build}
|
||||||
|
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
|
||||||
|
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||||
|
|
||||||
|
# we construct the BUILDDIR such that we can easily share the
|
||||||
|
# CARGO_TARGET_DIR between runs ("..")
|
||||||
|
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
|
||||||
|
|
||||||
|
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
||||||
|
|
||||||
|
set -xe
|
||||||
|
|
||||||
|
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
|
||||||
|
git ls-files >.rsynclist
|
||||||
|
# we seem to need .git for setuptools_scm versioning
|
||||||
|
find .git >>.rsynclist
|
||||||
|
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
||||||
|
|
||||||
|
set +x
|
||||||
|
|
||||||
|
# we have to create a remote file for the remote-docker run
|
||||||
|
# so we can do a simple ssh command with a TTY
|
||||||
|
# so that when our job dies, all container-runs are aborted.
|
||||||
|
# sidenote: the circle-ci machinery will kill ongoing jobs
|
||||||
|
# if there are new commits and we want to ensure that
|
||||||
|
# everything is terminated/cleaned up and we have no orphaned
|
||||||
|
# useless still-running docker-containers consuming resources.
|
||||||
|
|
||||||
|
ssh $SSHTARGET bash -c "cat >$BUILDDIR/exec_docker_run" <<_HERE
|
||||||
|
set +x -e
|
||||||
|
cd $BUILDDIR
|
||||||
|
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
|
||||||
|
|
||||||
|
set -x
|
||||||
|
|
||||||
|
# run everything else inside docker
|
||||||
|
docker run -e DCC_PY_LIVECONFIG \
|
||||||
|
--rm -it -v \$(pwd):/mnt -w /mnt \
|
||||||
|
deltachat/coredeps ci_scripts/run_all.sh
|
||||||
|
|
||||||
|
_HERE
|
||||||
|
|
||||||
|
echo "--- Running $CIRCLE_JOB remotely"
|
||||||
|
|
||||||
|
ssh -t $SSHTARGET bash "$BUILDDIR/exec_docker_run"
|
||||||
|
mkdir -p workspace
|
||||||
|
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/wheelhouse/*manylinux1*" workspace/wheelhouse/
|
||||||
|
rsync -avz "$SSHTARGET:$BUILDDIR/python/.docker-tox/dist/*" workspace/wheelhouse/
|
||||||
|
rsync -avz "$SSHTARGET:$BUILDDIR/python/doc/_build/" workspace/py-docs
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
|
|
||||||
BUILD_ID=${1:?specify build ID}
|
export BRANCH=${CIRCLE_BRANCH:?branch to build}
|
||||||
|
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
|
||||||
|
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||||
|
|
||||||
SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
# we construct the BUILDDIR such that we can easily share the
|
||||||
BUILDDIR=ci_builds/$BUILD_ID
|
# CARGO_TARGET_DIR between runs ("..")
|
||||||
|
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
|
||||||
|
|
||||||
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
||||||
|
|
||||||
@@ -17,18 +20,16 @@ rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
|||||||
|
|
||||||
set +x
|
set +x
|
||||||
|
|
||||||
echo "--- Running Python tests remotely"
|
echo "--- Running $CIRCLE_JOB remotely"
|
||||||
|
|
||||||
ssh $SSHTARGET <<_HERE
|
ssh $SSHTARGET <<_HERE
|
||||||
set +x -e
|
set +x -e
|
||||||
|
|
||||||
# make sure all processes exit when ssh dies
|
|
||||||
shopt -s huponexit
|
|
||||||
|
|
||||||
export RUSTC_WRAPPER=\`which sccache\`
|
|
||||||
cd $BUILDDIR
|
cd $BUILDDIR
|
||||||
|
# let's share the target dir with our last run on this branch/job-type
|
||||||
|
# cargo will make sure to block/unblock us properly
|
||||||
|
export CARGO_TARGET_DIR=\`pwd\`/../target
|
||||||
export TARGET=release
|
export TARGET=release
|
||||||
export DCC_NEW_TMP_EMAIL=$DCC_NEW_TMP_EMAIL
|
export DCC_PY_LIVECONFIG=$DCC_PY_LIVECONFIG
|
||||||
|
|
||||||
#we rely on tox/virtualenv being available in the host
|
#we rely on tox/virtualenv being available in the host
|
||||||
#rm -rf virtualenv venv
|
#rm -rf virtualenv venv
|
||||||
@@ -41,5 +42,5 @@ ssh $SSHTARGET <<_HERE
|
|||||||
source \$HOME/venv/bin/activate
|
source \$HOME/venv/bin/activate
|
||||||
which python
|
which python
|
||||||
|
|
||||||
bash scripts/run-python-test.sh
|
bash ci_scripts/run-python-test.sh
|
||||||
_HERE
|
_HERE
|
||||||
32
ci_scripts/remote_tests_rust.sh
Executable file
@@ -0,0 +1,32 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export BRANCH=${CIRCLE_BRANCH:?branch to build}
|
||||||
|
export REPONAME=${CIRCLE_PROJECT_REPONAME:?repository name}
|
||||||
|
export SSHTARGET=${SSHTARGET-ci@b1.delta.chat}
|
||||||
|
|
||||||
|
# we construct the BUILDDIR such that we can easily share the
|
||||||
|
# CARGO_TARGET_DIR between runs ("..")
|
||||||
|
export BUILDDIR=ci_builds/$REPONAME/$BRANCH/${CIRCLE_JOB:?jobname}/${CIRCLE_BUILD_NUM:?circle-build-number}
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
echo "--- Copying files to $SSHTARGET:$BUILDDIR"
|
||||||
|
|
||||||
|
ssh -oBatchMode=yes -oStrictHostKeyChecking=no $SSHTARGET mkdir -p "$BUILDDIR"
|
||||||
|
git ls-files >.rsynclist
|
||||||
|
rsync --delete --files-from=.rsynclist -az ./ "$SSHTARGET:$BUILDDIR"
|
||||||
|
|
||||||
|
echo "--- Running $CIRCLE_JOB remotely"
|
||||||
|
|
||||||
|
ssh $SSHTARGET <<_HERE
|
||||||
|
set +x -e
|
||||||
|
cd $BUILDDIR
|
||||||
|
# let's share the target dir with our last run on this branch/job-type
|
||||||
|
# cargo will make sure to block/unblock us properly
|
||||||
|
export CARGO_TARGET_DIR=\`pwd\`/../target
|
||||||
|
export TARGET=x86_64-unknown-linux-gnu
|
||||||
|
export RUSTC_WRAPPER=sccache
|
||||||
|
|
||||||
|
bash ci_scripts/run-rust-test.sh
|
||||||
|
_HERE
|
||||||
|
|
||||||
6
ci_scripts/run-doxygen.sh
Executable file
@@ -0,0 +1,6 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
cd deltachat-ffi
|
||||||
|
PROJECT_NUMBER=$(git log -1 --format "%h (%cd)") doxygen
|
||||||
@@ -4,7 +4,6 @@
|
|||||||
# and tox/pytest.
|
# and tox/pytest.
|
||||||
|
|
||||||
set -e -x
|
set -e -x
|
||||||
shopt -s huponexit
|
|
||||||
|
|
||||||
# for core-building and python install step
|
# for core-building and python install step
|
||||||
export DCC_RS_TARGET=debug
|
export DCC_RS_TARGET=debug
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
set -ex
|
set -ex
|
||||||
shopt -s huponexit
|
|
||||||
|
|
||||||
#export RUST_TEST_THREADS=1
|
#export RUST_TEST_THREADS=1
|
||||||
export RUST_BACKTRACE=1
|
export RUST_BACKTRACE=1
|
||||||
@@ -15,19 +15,16 @@ cargo build --release -p deltachat_ffi
|
|||||||
|
|
||||||
# Statically link against libdeltachat.a.
|
# Statically link against libdeltachat.a.
|
||||||
export DCC_RS_DEV=$(pwd)
|
export DCC_RS_DEV=$(pwd)
|
||||||
export DCC_RS_TARGET=release
|
|
||||||
|
|
||||||
# Configure access to a base python and to several python interpreters
|
# Configure access to a base python and to several python interpreters
|
||||||
# needed by tox below.
|
# needed by tox below.
|
||||||
export PATH=$PATH:/opt/python/cp37-cp37m/bin
|
export PATH=$PATH:/opt/python/cp35-cp35m/bin
|
||||||
export PYTHONDONTWRITEBYTECODE=1
|
export PYTHONDONTWRITEBYTECODE=1
|
||||||
pushd /bin
|
pushd /bin
|
||||||
rm -f python3.7
|
ln -s /opt/python/cp27-cp27m/bin/python2.7
|
||||||
|
ln -s /opt/python/cp36-cp36m/bin/python3.6
|
||||||
ln -s /opt/python/cp37-cp37m/bin/python3.7
|
ln -s /opt/python/cp37-cp37m/bin/python3.7
|
||||||
rm -f python3.8
|
|
||||||
ln -s /opt/python/cp38-cp38/bin/python3.8
|
ln -s /opt/python/cp38-cp38/bin/python3.8
|
||||||
rm -f python3.9
|
|
||||||
ln -s /opt/python/cp39-cp39/bin/python3.9
|
|
||||||
popd
|
popd
|
||||||
|
|
||||||
pushd python
|
pushd python
|
||||||
@@ -40,8 +37,8 @@ mkdir -p $TOXWORKDIR
|
|||||||
# XXX we may switch on some live-tests on for better ensurances
|
# XXX we may switch on some live-tests on for better ensurances
|
||||||
# Note that the independent remote_tests_python step does all kinds of
|
# Note that the independent remote_tests_python step does all kinds of
|
||||||
# live-testing already.
|
# live-testing already.
|
||||||
unset DCC_NEW_TMP_EMAIL
|
unset DCC_PY_LIVECONFIG
|
||||||
tox --workdir "$TOXWORKDIR" -e py37,py38,py39,auditwheels
|
tox --workdir "$TOXWORKDIR" -e py35,py36,py37,py38,auditwheels
|
||||||
popd
|
popd
|
||||||
|
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "deltachat_ffi"
|
name = "deltachat_ffi"
|
||||||
version = "1.78.0"
|
version = "1.25.0"
|
||||||
description = "Deltachat FFI"
|
description = "Deltachat FFI"
|
||||||
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
authors = ["Delta Chat Developers (ML) <delta@codespeak.net>"]
|
||||||
edition = "2018"
|
edition = "2018"
|
||||||
@@ -17,16 +17,13 @@ crate-type = ["cdylib", "staticlib"]
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
deltachat = { path = "../", default-features = false }
|
deltachat = { path = "../", default-features = false }
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
human-panic = "1"
|
human-panic = "1.0.1"
|
||||||
num-traits = "0.2"
|
num-traits = "0.2.6"
|
||||||
|
failure = "0.1.6"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
async-std = "1"
|
|
||||||
anyhow = "1"
|
|
||||||
thiserror = "1"
|
|
||||||
rand = "0.7"
|
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
default = ["vendored"]
|
default = ["vendored", "nightly", "ringbuf"]
|
||||||
vendored = ["deltachat/vendored"]
|
vendored = ["deltachat/vendored"]
|
||||||
nightly = ["deltachat/nightly"]
|
nightly = ["deltachat/nightly"]
|
||||||
|
ringbuf = ["deltachat/ringbuf"]
|
||||||
|
|||||||
@@ -236,6 +236,12 @@ TAB_SIZE = 4
|
|||||||
|
|
||||||
ALIASES =
|
ALIASES =
|
||||||
|
|
||||||
|
# This tag can be used to specify a number of word-keyword mappings (TCL only).
|
||||||
|
# A mapping has the form "name=value". For example adding "class=itcl::class"
|
||||||
|
# will allow you to use the command class in the itcl::class meaning.
|
||||||
|
|
||||||
|
TCL_SUBST =
|
||||||
|
|
||||||
# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
|
# Set the OPTIMIZE_OUTPUT_FOR_C tag to YES if your project consists of C sources
|
||||||
# only. Doxygen will then generate output that is more tailored for C. For
|
# only. Doxygen will then generate output that is more tailored for C. For
|
||||||
# instance, some of the names that are used will be different. The list of all
|
# instance, some of the names that are used will be different. The list of all
|
||||||
@@ -583,7 +589,7 @@ SORT_MEMBERS_CTORS_1ST = NO
|
|||||||
# appear in their defined order.
|
# appear in their defined order.
|
||||||
# The default value is: NO.
|
# The default value is: NO.
|
||||||
|
|
||||||
SORT_GROUP_NAMES = YES
|
SORT_GROUP_NAMES = NO
|
||||||
|
|
||||||
# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
|
# If the SORT_BY_SCOPE_NAME tag is set to YES, the class list will be sorted by
|
||||||
# fully-qualified names, including namespaces. If set to NO, the class list will
|
# fully-qualified names, including namespaces. If set to NO, the class list will
|
||||||
|
|||||||
@@ -4,16 +4,4 @@ div.fragment {
|
|||||||
background-color: #e0e0e0;
|
background-color: #e0e0e0;
|
||||||
border: 0;
|
border: 0;
|
||||||
padding: 1em;
|
padding: 1em;
|
||||||
border-radius: 6px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
code {
|
|
||||||
background-color: #e0e0e0;
|
|
||||||
padding-left: .5em;
|
|
||||||
padding-right: .5em;
|
|
||||||
border-radius: 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
li {
|
|
||||||
margin-bottom: .5em;
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
<doxygenlayout version="1.0">
|
|
||||||
<!-- Generated by doxygen 1.8.20 -->
|
|
||||||
<!-- Navigation index tabs for HTML output -->
|
|
||||||
<navindex>
|
|
||||||
<tab type="mainpage" visible="yes" title=""/>
|
|
||||||
<tab type="classes" visible="yes" title="">
|
|
||||||
<tab type="classlist" visible="no" title="" intro=""/>
|
|
||||||
<tab type="classindex" visible="no" title=""/>
|
|
||||||
<tab type="hierarchy" visible="no" title="" intro=""/>
|
|
||||||
<tab type="classmembers" visible="no" title="" intro=""/>
|
|
||||||
</tab>
|
|
||||||
<tab type="modules" visible="yes" title="Constants" intro="Here is a list of constants:"/>
|
|
||||||
<tab type="pages" visible="yes" title="" intro=""/>
|
|
||||||
<tab type="namespaces" visible="yes" title="">
|
|
||||||
<tab type="namespacelist" visible="yes" title="" intro=""/>
|
|
||||||
<tab type="namespacemembers" visible="yes" title="" intro=""/>
|
|
||||||
</tab>
|
|
||||||
<tab type="interfaces" visible="yes" title="">
|
|
||||||
<tab type="interfacelist" visible="yes" title="" intro=""/>
|
|
||||||
<tab type="interfaceindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
|
||||||
<tab type="interfacehierarchy" visible="yes" title="" intro=""/>
|
|
||||||
</tab>
|
|
||||||
<tab type="structs" visible="yes" title="">
|
|
||||||
<tab type="structlist" visible="yes" title="" intro=""/>
|
|
||||||
<tab type="structindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
|
||||||
</tab>
|
|
||||||
<tab type="exceptions" visible="yes" title="">
|
|
||||||
<tab type="exceptionlist" visible="yes" title="" intro=""/>
|
|
||||||
<tab type="exceptionindex" visible="$ALPHABETICAL_INDEX" title=""/>
|
|
||||||
<tab type="exceptionhierarchy" visible="yes" title="" intro=""/>
|
|
||||||
</tab>
|
|
||||||
<tab type="files" visible="yes" title="">
|
|
||||||
<tab type="filelist" visible="yes" title="" intro=""/>
|
|
||||||
<tab type="globals" visible="yes" title="" intro=""/>
|
|
||||||
</tab>
|
|
||||||
<tab type="examples" visible="yes" title="" intro=""/>
|
|
||||||
</navindex>
|
|
||||||
</doxygenlayout>
|
|
||||||
@@ -1,9 +1,5 @@
|
|||||||
# Delta Chat C Interface
|
# Delta Chat C Interface
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
see `Installing libdeltachat system wide` in [../README.md](../README.md)
|
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
To generate the C Interface documentation,
|
To generate the C Interface documentation,
|
||||||
|
|||||||
@@ -19,17 +19,15 @@ fn main() {
|
|||||||
include_str!("deltachat.pc.in"),
|
include_str!("deltachat.pc.in"),
|
||||||
name = "deltachat",
|
name = "deltachat",
|
||||||
description = env::var("CARGO_PKG_DESCRIPTION").unwrap(),
|
description = env::var("CARGO_PKG_DESCRIPTION").unwrap(),
|
||||||
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or_else(|_| "".to_string()),
|
url = env::var("CARGO_PKG_HOMEPAGE").unwrap_or("".to_string()),
|
||||||
version = env::var("CARGO_PKG_VERSION").unwrap(),
|
version = env::var("CARGO_PKG_VERSION").unwrap(),
|
||||||
libs_priv = libs_priv,
|
libs_priv = libs_priv,
|
||||||
prefix = env::var("PREFIX").unwrap_or_else(|_| "/usr/local".to_string()),
|
prefix = env::var("PREFIX").unwrap_or("/usr/local".to_string()),
|
||||||
libdir = env::var("LIBDIR").unwrap_or_else(|_| "/usr/local/lib".to_string()),
|
|
||||||
includedir = env::var("INCLUDEDIR").unwrap_or_else(|_| "/usr/local/include".to_string()),
|
|
||||||
);
|
);
|
||||||
|
|
||||||
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
|
fs::create_dir_all(target_path.join("pkgconfig")).unwrap();
|
||||||
fs::File::create(target_path.join("pkgconfig").join("deltachat.pc"))
|
fs::File::create(target_path.join("pkgconfig").join("deltachat.pc"))
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.write_all(pkg_config.as_bytes())
|
.write_all(&pkg_config.as_bytes())
|
||||||
.unwrap();
|
.unwrap();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
prefix={prefix}
|
prefix={prefix}
|
||||||
libdir={libdir}
|
libdir=${{prefix}}/lib
|
||||||
includedir={includedir}
|
includedir=${{prefix}}/include
|
||||||
|
|
||||||
Name: {name}
|
Name: {name}
|
||||||
Description: {description}
|
Description: {description}
|
||||||
|
|||||||
@@ -1,56 +1,46 @@
|
|||||||
use crate::chat::ChatItem;
|
|
||||||
use crate::constants::{DC_MSG_ID_DAYMARKER, DC_MSG_ID_MARKER1};
|
|
||||||
use crate::location::Location;
|
use crate::location::Location;
|
||||||
use crate::message::MsgId;
|
|
||||||
|
|
||||||
/* * the structure behind dc_array_t */
|
/* * the structure behind dc_array_t */
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub enum dc_array_t {
|
pub enum dc_array_t {
|
||||||
MsgIds(Vec<MsgId>),
|
|
||||||
Chat(Vec<ChatItem>),
|
|
||||||
Locations(Vec<Location>),
|
Locations(Vec<Location>),
|
||||||
Uint(Vec<u32>),
|
Uint(Vec<u32>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl dc_array_t {
|
impl dc_array_t {
|
||||||
pub(crate) fn get_id(&self, index: usize) -> u32 {
|
pub fn new(capacity: usize) -> Self {
|
||||||
|
dc_array_t::Uint(Vec::with_capacity(capacity))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Constructs a new, empty `dc_array_t` holding locations with specified `capacity`.
|
||||||
|
pub fn new_locations(capacity: usize) -> Self {
|
||||||
|
dc_array_t::Locations(Vec::with_capacity(capacity))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_id(&mut self, item: u32) {
|
||||||
|
if let Self::Uint(array) = self {
|
||||||
|
array.push(item);
|
||||||
|
} else {
|
||||||
|
panic!("Attempt to add id to array of other type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_location(&mut self, location: Location) {
|
||||||
|
if let Self::Locations(array) = self {
|
||||||
|
array.push(location)
|
||||||
|
} else {
|
||||||
|
panic!("Attempt to add a location to array of other type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_id(&self, index: usize) -> u32 {
|
||||||
match self {
|
match self {
|
||||||
Self::MsgIds(array) => array[index].to_u32(),
|
|
||||||
Self::Chat(array) => match array[index] {
|
|
||||||
ChatItem::Message { msg_id } => msg_id.to_u32(),
|
|
||||||
ChatItem::Marker1 => DC_MSG_ID_MARKER1,
|
|
||||||
ChatItem::DayMarker { .. } => DC_MSG_ID_DAYMARKER,
|
|
||||||
},
|
|
||||||
Self::Locations(array) => array[index].location_id,
|
Self::Locations(array) => array[index].location_id,
|
||||||
Self::Uint(array) => array[index],
|
Self::Uint(array) => array[index] as u32,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn get_timestamp(&self, index: usize) -> Option<i64> {
|
pub fn get_location(&self, index: usize) -> &Location {
|
||||||
match self {
|
|
||||||
Self::MsgIds(_) => None,
|
|
||||||
Self::Chat(array) => array.get(index).and_then(|item| match item {
|
|
||||||
ChatItem::Message { .. } => None,
|
|
||||||
ChatItem::Marker1 { .. } => None,
|
|
||||||
ChatItem::DayMarker { timestamp } => Some(*timestamp),
|
|
||||||
}),
|
|
||||||
Self::Locations(array) => array.get(index).map(|location| location.timestamp),
|
|
||||||
Self::Uint(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_marker(&self, index: usize) -> Option<&str> {
|
|
||||||
match self {
|
|
||||||
Self::MsgIds(_) => None,
|
|
||||||
Self::Chat(_) => None,
|
|
||||||
Self::Locations(array) => array
|
|
||||||
.get(index)
|
|
||||||
.and_then(|location| location.marker.as_deref()),
|
|
||||||
Self::Uint(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_location(&self, index: usize) -> &Location {
|
|
||||||
if let Self::Locations(array) = self {
|
if let Self::Locations(array) = self {
|
||||||
&array[index]
|
&array[index]
|
||||||
} else {
|
} else {
|
||||||
@@ -58,18 +48,55 @@ impl dc_array_t {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the number of elements in the array.
|
pub fn is_empty(&self) -> bool {
|
||||||
pub(crate) fn len(&self) -> usize {
|
match self {
|
||||||
|
Self::Locations(array) => array.is_empty(),
|
||||||
|
Self::Uint(array) => array.is_empty(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the number of elements in the array.
|
||||||
|
pub fn len(&self) -> usize {
|
||||||
match self {
|
match self {
|
||||||
Self::MsgIds(array) => array.len(),
|
|
||||||
Self::Chat(array) => array.len(),
|
|
||||||
Self::Locations(array) => array.len(),
|
Self::Locations(array) => array.len(),
|
||||||
Self::Uint(array) => array.len(),
|
Self::Uint(array) => array.len(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn search_id(&self, needle: u32) -> Option<usize> {
|
pub fn clear(&mut self) {
|
||||||
(0..self.len()).find(|i| self.get_id(*i) == needle)
|
match self {
|
||||||
|
Self::Locations(array) => array.clear(),
|
||||||
|
Self::Uint(array) => array.clear(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn search_id(&self, needle: u32) -> Option<usize> {
|
||||||
|
if let Self::Uint(array) = self {
|
||||||
|
for (i, &u) in array.iter().enumerate() {
|
||||||
|
if u == needle {
|
||||||
|
return Some(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
panic!("Attempt to search for id in array of other type");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn sort_ids(&mut self) {
|
||||||
|
if let dc_array_t::Uint(v) = self {
|
||||||
|
v.sort();
|
||||||
|
} else {
|
||||||
|
panic!("Attempt to sort array of something other than uints");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_ptr(&self) -> *const u32 {
|
||||||
|
if let dc_array_t::Uint(v) = self {
|
||||||
|
v.as_ptr()
|
||||||
|
} else {
|
||||||
|
panic!("Attempt to convert array of something other than uints to raw");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,18 +106,6 @@ impl From<Vec<u32>> for dc_array_t {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Vec<MsgId>> for dc_array_t {
|
|
||||||
fn from(array: Vec<MsgId>) -> Self {
|
|
||||||
dc_array_t::MsgIds(array)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Vec<ChatItem>> for dc_array_t {
|
|
||||||
fn from(array: Vec<ChatItem>) -> Self {
|
|
||||||
dc_array_t::Chat(array)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Vec<Location>> for dc_array_t {
|
impl From<Vec<Location>> for dc_array_t {
|
||||||
fn from(array: Vec<Location>) -> Self {
|
fn from(array: Vec<Location>) -> Self {
|
||||||
dc_array_t::Locations(array)
|
dc_array_t::Locations(array)
|
||||||
@@ -103,11 +118,12 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dc_array() {
|
fn test_dc_array() {
|
||||||
let arr: dc_array_t = Vec::<u32>::new().into();
|
let mut arr = dc_array_t::new(7);
|
||||||
assert!(arr.len() == 0);
|
assert!(arr.is_empty());
|
||||||
|
|
||||||
let ids: Vec<u32> = (2..1002).collect();
|
for i in 0..1000 {
|
||||||
let arr: dc_array_t = ids.into();
|
arr.add_id(i + 2);
|
||||||
|
}
|
||||||
|
|
||||||
assert_eq!(arr.len(), 1000);
|
assert_eq!(arr.len(), 1000);
|
||||||
|
|
||||||
@@ -115,15 +131,31 @@ mod tests {
|
|||||||
assert_eq!(arr.get_id(i), (i + 2) as u32);
|
assert_eq!(arr.get_id(i), (i + 2) as u32);
|
||||||
}
|
}
|
||||||
|
|
||||||
assert_eq!(arr.search_id(10), Some(8));
|
arr.clear();
|
||||||
assert_eq!(arr.search_id(1), None);
|
|
||||||
|
assert!(arr.is_empty());
|
||||||
|
|
||||||
|
arr.add_id(13);
|
||||||
|
arr.add_id(7);
|
||||||
|
arr.add_id(666);
|
||||||
|
arr.add_id(0);
|
||||||
|
arr.add_id(5000);
|
||||||
|
|
||||||
|
arr.sort_ids();
|
||||||
|
|
||||||
|
assert_eq!(arr.get_id(0), 0);
|
||||||
|
assert_eq!(arr.get_id(1), 7);
|
||||||
|
assert_eq!(arr.get_id(2), 13);
|
||||||
|
assert_eq!(arr.get_id(3), 666);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
#[should_panic]
|
#[should_panic]
|
||||||
fn test_dc_array_out_of_bounds() {
|
fn test_dc_array_out_of_bounds() {
|
||||||
let ids: Vec<u32> = (2..1002).collect();
|
let mut arr = dc_array_t::new(7);
|
||||||
let arr: dc_array_t = ids.into();
|
for i in 0..1000 {
|
||||||
|
arr.add_id(i + 2);
|
||||||
|
}
|
||||||
arr.get_id(1000);
|
arr.get_id(1000);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,245 +0,0 @@
|
|||||||
//! # Legacy generic return values for C API.
|
|
||||||
|
|
||||||
use crate::message::MessageState;
|
|
||||||
use crate::qr::Qr;
|
|
||||||
use crate::summary::{Summary, SummaryPrefix};
|
|
||||||
use anyhow::Error;
|
|
||||||
use std::borrow::Cow;
|
|
||||||
|
|
||||||
/// An object containing a set of values.
|
|
||||||
/// The meaning of the values is defined by the function returning the object.
|
|
||||||
/// Lot objects are created
|
|
||||||
/// eg. by chatlist.get_summary() or dc_msg_get_summary().
|
|
||||||
///
|
|
||||||
/// *Lot* is used in the meaning *heap* here.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Lot {
|
|
||||||
Summary(Summary),
|
|
||||||
Qr(Qr),
|
|
||||||
Error(String),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(u8)]
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum Meaning {
|
|
||||||
None = 0,
|
|
||||||
Text1Draft = 1,
|
|
||||||
Text1Username = 2,
|
|
||||||
Text1Self = 3,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Meaning {
|
|
||||||
fn default() -> Self {
|
|
||||||
Meaning::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Lot {
|
|
||||||
pub fn get_text1(&self) -> Option<&str> {
|
|
||||||
match self {
|
|
||||||
Self::Summary(summary) => match &summary.prefix {
|
|
||||||
None => None,
|
|
||||||
Some(SummaryPrefix::Draft(text)) => Some(text),
|
|
||||||
Some(SummaryPrefix::Username(username)) => Some(username),
|
|
||||||
Some(SummaryPrefix::Me(text)) => Some(text),
|
|
||||||
},
|
|
||||||
Self::Qr(qr) => match qr {
|
|
||||||
Qr::AskVerifyContact { .. } => None,
|
|
||||||
Qr::AskVerifyGroup { grpname, .. } => Some(grpname),
|
|
||||||
Qr::FprOk { .. } => None,
|
|
||||||
Qr::FprMismatch { .. } => None,
|
|
||||||
Qr::FprWithoutAddr { fingerprint, .. } => Some(fingerprint),
|
|
||||||
Qr::Account { domain } => Some(domain),
|
|
||||||
Qr::WebrtcInstance { domain, .. } => Some(domain),
|
|
||||||
Qr::Addr { .. } => None,
|
|
||||||
Qr::Url { url } => Some(url),
|
|
||||||
Qr::Text { text } => Some(text),
|
|
||||||
Qr::WithdrawVerifyContact { .. } => None,
|
|
||||||
Qr::WithdrawVerifyGroup { grpname, .. } => Some(grpname),
|
|
||||||
Qr::ReviveVerifyContact { .. } => None,
|
|
||||||
Qr::ReviveVerifyGroup { grpname, .. } => Some(grpname),
|
|
||||||
},
|
|
||||||
Self::Error(err) => Some(err),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_text2(&self) -> Option<Cow<str>> {
|
|
||||||
match self {
|
|
||||||
Self::Summary(summary) => Some(summary.truncated_text(160)),
|
|
||||||
Self::Qr(_) => None,
|
|
||||||
Self::Error(_) => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_text1_meaning(&self) -> Meaning {
|
|
||||||
match self {
|
|
||||||
Self::Summary(summary) => match &summary.prefix {
|
|
||||||
None => Meaning::None,
|
|
||||||
Some(SummaryPrefix::Draft(_text)) => Meaning::Text1Draft,
|
|
||||||
Some(SummaryPrefix::Username(_username)) => Meaning::Text1Username,
|
|
||||||
Some(SummaryPrefix::Me(_text)) => Meaning::Text1Self,
|
|
||||||
},
|
|
||||||
Self::Qr(_qr) => Meaning::None,
|
|
||||||
Self::Error(_err) => Meaning::None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_state(&self) -> LotState {
|
|
||||||
match self {
|
|
||||||
Self::Summary(summary) => summary.state.into(),
|
|
||||||
Self::Qr(qr) => match qr {
|
|
||||||
Qr::AskVerifyContact { .. } => LotState::QrAskVerifyContact,
|
|
||||||
Qr::AskVerifyGroup { .. } => LotState::QrAskVerifyGroup,
|
|
||||||
Qr::FprOk { .. } => LotState::QrFprOk,
|
|
||||||
Qr::FprMismatch { .. } => LotState::QrFprMismatch,
|
|
||||||
Qr::FprWithoutAddr { .. } => LotState::QrFprWithoutAddr,
|
|
||||||
Qr::Account { .. } => LotState::QrAccount,
|
|
||||||
Qr::WebrtcInstance { .. } => LotState::QrWebrtcInstance,
|
|
||||||
Qr::Addr { .. } => LotState::QrAddr,
|
|
||||||
Qr::Url { .. } => LotState::QrUrl,
|
|
||||||
Qr::Text { .. } => LotState::QrText,
|
|
||||||
Qr::WithdrawVerifyContact { .. } => LotState::QrWithdrawVerifyContact,
|
|
||||||
Qr::WithdrawVerifyGroup { .. } => LotState::QrWithdrawVerifyGroup,
|
|
||||||
Qr::ReviveVerifyContact { .. } => LotState::QrReviveVerifyContact,
|
|
||||||
Qr::ReviveVerifyGroup { .. } => LotState::QrReviveVerifyGroup,
|
|
||||||
},
|
|
||||||
Self::Error(_err) => LotState::QrError,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_id(&self) -> u32 {
|
|
||||||
match self {
|
|
||||||
Self::Summary(_) => Default::default(),
|
|
||||||
Self::Qr(qr) => match qr {
|
|
||||||
Qr::AskVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
|
||||||
Qr::AskVerifyGroup { .. } => Default::default(),
|
|
||||||
Qr::FprOk { contact_id } => contact_id.to_u32(),
|
|
||||||
Qr::FprMismatch { contact_id } => contact_id.unwrap_or_default().to_u32(),
|
|
||||||
Qr::FprWithoutAddr { .. } => Default::default(),
|
|
||||||
Qr::Account { .. } => Default::default(),
|
|
||||||
Qr::WebrtcInstance { .. } => Default::default(),
|
|
||||||
Qr::Addr { contact_id } => contact_id.to_u32(),
|
|
||||||
Qr::Url { .. } => Default::default(),
|
|
||||||
Qr::Text { .. } => Default::default(),
|
|
||||||
Qr::WithdrawVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
|
||||||
Qr::WithdrawVerifyGroup { .. } => Default::default(),
|
|
||||||
Qr::ReviveVerifyContact { contact_id, .. } => contact_id.to_u32(),
|
|
||||||
Qr::ReviveVerifyGroup { .. } => Default::default(),
|
|
||||||
},
|
|
||||||
Self::Error(_) => Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_timestamp(&self) -> i64 {
|
|
||||||
match self {
|
|
||||||
Self::Summary(summary) => summary.timestamp,
|
|
||||||
Self::Qr(_) => Default::default(),
|
|
||||||
Self::Error(_) => Default::default(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(u32)]
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
||||||
pub enum LotState {
|
|
||||||
// Default
|
|
||||||
Undefined = 0,
|
|
||||||
|
|
||||||
// Qr States
|
|
||||||
/// id=contact
|
|
||||||
QrAskVerifyContact = 200,
|
|
||||||
|
|
||||||
/// text1=groupname
|
|
||||||
QrAskVerifyGroup = 202,
|
|
||||||
|
|
||||||
/// id=contact
|
|
||||||
QrFprOk = 210,
|
|
||||||
|
|
||||||
/// id=contact
|
|
||||||
QrFprMismatch = 220,
|
|
||||||
|
|
||||||
/// text1=formatted fingerprint
|
|
||||||
QrFprWithoutAddr = 230,
|
|
||||||
|
|
||||||
/// text1=domain
|
|
||||||
QrAccount = 250,
|
|
||||||
|
|
||||||
/// text1=domain, text2=instance pattern
|
|
||||||
QrWebrtcInstance = 260,
|
|
||||||
|
|
||||||
/// id=contact
|
|
||||||
QrAddr = 320,
|
|
||||||
|
|
||||||
/// text1=text
|
|
||||||
QrText = 330,
|
|
||||||
|
|
||||||
/// text1=URL
|
|
||||||
QrUrl = 332,
|
|
||||||
|
|
||||||
/// text1=error string
|
|
||||||
QrError = 400,
|
|
||||||
|
|
||||||
QrWithdrawVerifyContact = 500,
|
|
||||||
|
|
||||||
/// text1=groupname
|
|
||||||
QrWithdrawVerifyGroup = 502,
|
|
||||||
|
|
||||||
QrReviveVerifyContact = 510,
|
|
||||||
|
|
||||||
/// text1=groupname
|
|
||||||
QrReviveVerifyGroup = 512,
|
|
||||||
|
|
||||||
// Message States
|
|
||||||
MsgInFresh = 10,
|
|
||||||
MsgInNoticed = 13,
|
|
||||||
MsgInSeen = 16,
|
|
||||||
MsgOutPreparing = 18,
|
|
||||||
MsgOutDraft = 19,
|
|
||||||
MsgOutPending = 20,
|
|
||||||
MsgOutFailed = 24,
|
|
||||||
MsgOutDelivered = 26,
|
|
||||||
MsgOutMdnRcvd = 28,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for LotState {
|
|
||||||
fn default() -> Self {
|
|
||||||
LotState::Undefined
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<MessageState> for LotState {
|
|
||||||
fn from(s: MessageState) -> Self {
|
|
||||||
use MessageState::*;
|
|
||||||
match s {
|
|
||||||
Undefined => LotState::Undefined,
|
|
||||||
InFresh => LotState::MsgInFresh,
|
|
||||||
InNoticed => LotState::MsgInNoticed,
|
|
||||||
InSeen => LotState::MsgInSeen,
|
|
||||||
OutPreparing => LotState::MsgOutPreparing,
|
|
||||||
OutDraft => LotState::MsgOutDraft,
|
|
||||||
OutPending => LotState::MsgOutPending,
|
|
||||||
OutFailed => LotState::MsgOutFailed,
|
|
||||||
OutDelivered => LotState::MsgOutDelivered,
|
|
||||||
OutMdnRcvd => LotState::MsgOutMdnRcvd,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Summary> for Lot {
|
|
||||||
fn from(summary: Summary) -> Self {
|
|
||||||
Lot::Summary(summary)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Qr> for Lot {
|
|
||||||
fn from(qr: Qr) -> Self {
|
|
||||||
Lot::Qr(qr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make it easy to convert errors into the final `Lot`.
|
|
||||||
impl From<Error> for Lot {
|
|
||||||
fn from(error: Error) -> Self {
|
|
||||||
Lot::Error(error.to_string())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use failure::Fail;
|
||||||
use std::ffi::{CStr, CString};
|
use std::ffi::{CStr, CString};
|
||||||
use std::ptr;
|
use std::ptr;
|
||||||
|
|
||||||
@@ -8,7 +9,7 @@ use std::ptr;
|
|||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// ```rust,norun
|
/// ```rust,norun
|
||||||
/// use crate::string::{dc_strdup, to_string_lossy};
|
/// use deltachat::dc_tools::{dc_strdup, to_string_lossy};
|
||||||
/// unsafe {
|
/// unsafe {
|
||||||
/// let str_a = b"foobar\x00" as *const u8 as *const libc::c_char;
|
/// let str_a = b"foobar\x00" as *const u8 as *const libc::c_char;
|
||||||
/// let str_a_copy = dc_strdup(str_a);
|
/// let str_a_copy = dc_strdup(str_a);
|
||||||
@@ -16,24 +17,27 @@ use std::ptr;
|
|||||||
/// assert_ne!(str_a, str_a_copy);
|
/// assert_ne!(str_a, str_a_copy);
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
pub unsafe fn dc_strdup(s: *const libc::c_char) -> *mut libc::c_char {
|
||||||
let ret: *mut libc::c_char = if !s.is_null() {
|
let ret: *mut libc::c_char;
|
||||||
libc::strdup(s)
|
if !s.is_null() {
|
||||||
|
ret = libc::strdup(s);
|
||||||
|
assert!(!ret.is_null());
|
||||||
} else {
|
} else {
|
||||||
libc::calloc(1, 1) as *mut libc::c_char
|
ret = libc::calloc(1, 1) as *mut libc::c_char;
|
||||||
};
|
assert!(!ret.is_null());
|
||||||
assert!(!ret.is_null());
|
}
|
||||||
|
|
||||||
ret
|
ret
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Error type for the [OsStrExt] trait
|
/// Error type for the [OsStrExt] trait
|
||||||
#[derive(Debug, PartialEq, thiserror::Error)]
|
#[derive(Debug, Fail, PartialEq)]
|
||||||
pub(crate) enum CStringError {
|
pub enum CStringError {
|
||||||
/// The string contains an interior null byte
|
/// The string contains an interior null byte
|
||||||
#[error("String contains an interior null byte")]
|
#[fail(display = "String contains an interior null byte")]
|
||||||
InteriorNullByte,
|
InteriorNullByte,
|
||||||
/// The string is not valid Unicode
|
/// The string is not valid Unicode
|
||||||
#[error("String is not valid unicode")]
|
#[fail(display = "String is not valid unicode")]
|
||||||
NotUnicode,
|
NotUnicode,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +66,7 @@ pub(crate) enum CStringError {
|
|||||||
/// let mut c_ptr: *mut libc::c_char = dc_strdup(path_c.as_ptr());
|
/// let mut c_ptr: *mut libc::c_char = dc_strdup(path_c.as_ptr());
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
pub(crate) trait OsStrExt {
|
pub trait OsStrExt {
|
||||||
/// Convert a [std::ffi::OsStr] to an [std::ffi::CString]
|
/// Convert a [std::ffi::OsStr] to an [std::ffi::CString]
|
||||||
///
|
///
|
||||||
/// This is useful to convert e.g. a [std::path::Path] to
|
/// This is useful to convert e.g. a [std::path::Path] to
|
||||||
@@ -102,9 +106,8 @@ impl<T: AsRef<std::ffi::OsStr>> OsStrExt for T {
|
|||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
fn to_c_string(&self) -> Result<CString, CStringError> {
|
fn to_c_string(&self) -> Result<CString, CStringError> {
|
||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
CString::new(self.as_ref().as_bytes()).map_err(|err| {
|
CString::new(self.as_ref().as_bytes()).map_err(|err| match err {
|
||||||
let std::ffi::NulError { .. } = err;
|
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
|
||||||
CStringError::InteriorNullByte
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,25 +123,23 @@ fn os_str_to_c_string_unicode(
|
|||||||
os_str: &dyn AsRef<std::ffi::OsStr>,
|
os_str: &dyn AsRef<std::ffi::OsStr>,
|
||||||
) -> Result<CString, CStringError> {
|
) -> Result<CString, CStringError> {
|
||||||
match os_str.as_ref().to_str() {
|
match os_str.as_ref().to_str() {
|
||||||
Some(val) => CString::new(val.as_bytes()).map_err(|err| {
|
Some(val) => CString::new(val.as_bytes()).map_err(|err| match err {
|
||||||
let std::ffi::NulError { .. } = err;
|
std::ffi::NulError { .. } => CStringError::InteriorNullByte,
|
||||||
CStringError::InteriorNullByte
|
|
||||||
}),
|
}),
|
||||||
None => Err(CStringError::NotUnicode),
|
None => Err(CStringError::NotUnicode),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience methods/associated functions for working with [CString]
|
/// Convenience methods/associated functions for working with [CString]
|
||||||
trait CStringExt {
|
///
|
||||||
/// Create a new [CString], best effort
|
/// This is helps transitioning from unsafe code.
|
||||||
|
pub trait CStringExt {
|
||||||
|
/// Create a new [CString], yolo style
|
||||||
///
|
///
|
||||||
/// Like the [to_string_lossy] this doesn't give up in the face of
|
/// This unwrap the result, panicking when there are embedded NULL
|
||||||
/// bad input (embedded null bytes in this case) instead it does
|
/// bytes.
|
||||||
/// the best it can by stripping the embedded null bytes.
|
fn yolo<T: Into<Vec<u8>>>(t: T) -> CString {
|
||||||
fn new_lossy<T: Into<Vec<u8>>>(t: T) -> CString {
|
CString::new(t).expect("String contains null byte, can not be CString")
|
||||||
let mut s = t.into();
|
|
||||||
s.retain(|&c| c != 0);
|
|
||||||
CString::new(s).unwrap_or_default()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -150,7 +151,7 @@ impl CStringExt for CString {}
|
|||||||
/// Rust strings to raw C strings. This can be clumsy to do correctly
|
/// Rust strings to raw C strings. This can be clumsy to do correctly
|
||||||
/// and the compiler sometimes allows it in an unsafe way. These
|
/// and the compiler sometimes allows it in an unsafe way. These
|
||||||
/// methods make it more succinct and help you get it right.
|
/// methods make it more succinct and help you get it right.
|
||||||
pub(crate) trait Strdup {
|
pub trait StrExt {
|
||||||
/// Allocate a new raw C `*char` version of this string.
|
/// Allocate a new raw C `*char` version of this string.
|
||||||
///
|
///
|
||||||
/// This allocates a new raw C string which must be freed using
|
/// This allocates a new raw C string which must be freed using
|
||||||
@@ -167,56 +168,35 @@ pub(crate) trait Strdup {
|
|||||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Strdup for str {
|
impl<T: AsRef<str>> StrExt for T {
|
||||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||||
let tmp = CString::new_lossy(self);
|
let tmp = CString::yolo(self.as_ref());
|
||||||
dc_strdup(tmp.as_ptr())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Strdup for String {
|
|
||||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
|
||||||
let s: &str = self;
|
|
||||||
s.strdup()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Strdup for std::path::Path {
|
|
||||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
|
||||||
let tmp = self.to_c_string().unwrap_or_else(|_| CString::default());
|
|
||||||
dc_strdup(tmp.as_ptr())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Strdup for [u8] {
|
|
||||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
|
||||||
let tmp = CString::new_lossy(self);
|
|
||||||
dc_strdup(tmp.as_ptr())
|
dc_strdup(tmp.as_ptr())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convenience methods to turn optional strings into C strings.
|
/// Convenience methods to turn optional strings into C strings.
|
||||||
///
|
///
|
||||||
/// This is the same as the [Strdup] trait but a different trait name
|
/// This is the same as the [StrExt] trait but a different trait name
|
||||||
/// to work around the type system not allowing to implement [Strdup]
|
/// to work around the type system not allowing to implement [StrExt]
|
||||||
/// for `Option<impl Strdup>` When we already have an [Strdup] impl
|
/// for `Option<impl StrExt>` When we already have an [StrExt] impl
|
||||||
/// for `AsRef<&str>`.
|
/// for `AsRef<&str>`.
|
||||||
///
|
///
|
||||||
/// When the [Option] is [Option::Some] this behaves just like
|
/// When the [Option] is [Option::Some] this behaves just like
|
||||||
/// [Strdup::strdup], when it is [Option::None] a null pointer is
|
/// [StrExt::strdup], when it is [Option::None] a null pointer is
|
||||||
/// returned.
|
/// returned.
|
||||||
pub(crate) trait OptStrdup {
|
pub trait OptStrExt {
|
||||||
/// Allocate a new raw C `*char` version of this string, or NULL.
|
/// Allocate a new raw C `*char` version of this string, or NULL.
|
||||||
///
|
///
|
||||||
/// See [Strdup::strdup] for details.
|
/// See [StrExt::strdup] for details.
|
||||||
unsafe fn strdup(&self) -> *mut libc::c_char;
|
unsafe fn strdup(&self) -> *mut libc::c_char;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: AsRef<str>> OptStrdup for Option<T> {
|
impl<T: AsRef<str>> OptStrExt for Option<T> {
|
||||||
unsafe fn strdup(&self) -> *mut libc::c_char {
|
unsafe fn strdup(&self) -> *mut libc::c_char {
|
||||||
match self {
|
match self {
|
||||||
Some(s) => {
|
Some(s) => {
|
||||||
let tmp = CString::new_lossy(s.as_ref());
|
let tmp = CString::yolo(s.as_ref());
|
||||||
dc_strdup(tmp.as_ptr())
|
dc_strdup(tmp.as_ptr())
|
||||||
}
|
}
|
||||||
None => ptr::null_mut(),
|
None => ptr::null_mut(),
|
||||||
@@ -224,7 +204,7 @@ impl<T: AsRef<str>> OptStrdup for Option<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn to_string_lossy(s: *const libc::c_char) -> String {
|
pub fn to_string_lossy(s: *const libc::c_char) -> String {
|
||||||
if s.is_null() {
|
if s.is_null() {
|
||||||
return "".into();
|
return "".into();
|
||||||
}
|
}
|
||||||
@@ -234,7 +214,7 @@ pub(crate) fn to_string_lossy(s: *const libc::c_char) -> String {
|
|||||||
cstr.to_string_lossy().to_string()
|
cstr.to_string_lossy().to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
pub fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
||||||
if s.is_null() {
|
if s.is_null() {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
@@ -250,12 +230,12 @@ pub(crate) fn to_opt_string_lossy(s: *const libc::c_char) -> Option<String> {
|
|||||||
/// [OsStrExt::to_c_string] requires valid Unicode on Windows, this
|
/// [OsStrExt::to_c_string] requires valid Unicode on Windows, this
|
||||||
/// requires that the pointer contains valid UTF-8 on Windows.
|
/// requires that the pointer contains valid UTF-8 on Windows.
|
||||||
///
|
///
|
||||||
/// Because this returns a reference the [Path] slice can not outlive
|
/// Because this returns a reference the [Path] silce can not outlive
|
||||||
/// the original pointer.
|
/// the original pointer.
|
||||||
///
|
///
|
||||||
/// [Path]: std::path::Path
|
/// [Path]: std::path::Path
|
||||||
#[cfg(not(target_os = "windows"))]
|
#[cfg(not(target_os = "windows"))]
|
||||||
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||||
assert!(!s.is_null(), "cannot be used on null pointers");
|
assert!(!s.is_null(), "cannot be used on null pointers");
|
||||||
use std::os::unix::ffi::OsStrExt;
|
use std::os::unix::ffi::OsStrExt;
|
||||||
unsafe {
|
unsafe {
|
||||||
@@ -267,7 +247,7 @@ pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
|||||||
|
|
||||||
// as_path() implementation for windows, documented above.
|
// as_path() implementation for windows, documented above.
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
pub(crate) fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
pub fn as_path<'a>(s: *const libc::c_char) -> &'a std::path::Path {
|
||||||
as_path_unicode(s)
|
as_path_unicode(s)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -374,14 +354,8 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cstring_new_lossy() {
|
fn test_cstring_yolo() {
|
||||||
assert!(CString::new("hel\x00lo").is_err());
|
assert_eq!(CString::new("hello").unwrap(), CString::yolo("hello"));
|
||||||
assert!(CString::new(String::from("hel\x00o")).is_err());
|
|
||||||
let r = CString::new("hello").unwrap();
|
|
||||||
assert_eq!(CString::new_lossy("hello"), r);
|
|
||||||
assert_eq!(CString::new_lossy("hel\x00lo"), r);
|
|
||||||
assert_eq!(CString::new_lossy(String::from("hello")), r);
|
|
||||||
assert_eq!(CString::new_lossy(String::from("hel\x00lo")), r);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -9,5 +9,5 @@ license = "MPL-2.0"
|
|||||||
proc-macro = true
|
proc-macro = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
syn = "1"
|
syn = "1.0.13"
|
||||||
quote = "1"
|
quote = "1.0.2"
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ extern crate proc_macro;
|
|||||||
|
|
||||||
use crate::proc_macro::TokenStream;
|
use crate::proc_macro::TokenStream;
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
use syn;
|
||||||
|
|
||||||
// For now, assume (not check) that these macroses are applied to enum without
|
// For now, assume (not check) that these macroses are applied to enum without
|
||||||
// data. If this assumption is violated, compiler error will point to
|
// data. If this assumption is violated, compiler error will point to
|
||||||
@@ -35,11 +36,7 @@ pub fn from_sql_derive(input: TokenStream) -> TokenStream {
|
|||||||
impl rusqlite::types::FromSql for #name {
|
impl rusqlite::types::FromSql for #name {
|
||||||
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
fn column_result(col: rusqlite::types::ValueRef) -> rusqlite::types::FromSqlResult<Self> {
|
||||||
let inner = rusqlite::types::FromSql::column_result(col)?;
|
let inner = rusqlite::types::FromSql::column_result(col)?;
|
||||||
if let Some(value) = num_traits::FromPrimitive::from_i64(inner) {
|
Ok(num_traits::FromPrimitive::from_i64(inner).unwrap_or_default())
|
||||||
Ok(value)
|
|
||||||
} else {
|
|
||||||
Err(rusqlite::types::FromSqlError::OutOfRange(inner))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,33 +0,0 @@
|
|||||||
AEAP MVP
|
|
||||||
========
|
|
||||||
|
|
||||||
Changes to the UIs
|
|
||||||
------------------
|
|
||||||
|
|
||||||
- The secondary self addresses (see below) are shown in the UI, but not editable.
|
|
||||||
|
|
||||||
- When the user changed the email address in the configure screen, show a dialog to the user, either directly explaining things or with a link to the FAQ (see "Other" below)
|
|
||||||
|
|
||||||
Changes in the core
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
- We have one primary self address and any number of secondary self addresses. `is_self_addr()` checks all of them.
|
|
||||||
|
|
||||||
- If the user does a reconfigure and changes the email address, the previous address is added as a secondary self address.
|
|
||||||
|
|
||||||
- don't forget to deduplicate secondary self addresses in case the user switches back and forth between addresses).
|
|
||||||
|
|
||||||
- The key stays the same.
|
|
||||||
|
|
||||||
- No changes for 1:1 chats, there simply is a new one
|
|
||||||
|
|
||||||
- When we send a message to a group, and the primary address is not a member of a group, but a secondary address is:
|
|
||||||
|
|
||||||
Add Chat-Group-Member-Removed=<old address> and Chat-Group-Member-Added=<new address> headers to this message
|
|
||||||
|
|
||||||
- On the receiving side, make sure that we accept this (even in verified groups) if the message is signed and the key stayed the same
|
|
||||||
|
|
||||||
Other
|
|
||||||
-----
|
|
||||||
|
|
||||||
- The user is responsible that messages to the old address arrive at the new address, for example by configuring the old provider to forward all emails to the new one.
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
|
|
||||||
Problem: missing eventual group consistency
|
|
||||||
--------------------------------------------
|
|
||||||
|
|
||||||
If group members are concurrently adding new members,
|
|
||||||
the new members will miss each other's additions, example:
|
|
||||||
|
|
||||||
1. Alice and Bob are in a two-member group
|
|
||||||
|
|
||||||
2. Then Alice adds Carol, while concurrently Bob adds Doris
|
|
||||||
|
|
||||||
Right now, the group has inconsistent memberships:
|
|
||||||
|
|
||||||
- Alice and Carol see a (Alice, Carol, Bob) group
|
|
||||||
|
|
||||||
- Bob and Doris see a (Bob, Doris, Alice)
|
|
||||||
|
|
||||||
This then leads to "sender is unknown" messages in the chat,
|
|
||||||
for example when Alice receives a message from Doris,
|
|
||||||
or when Bob receives a message from Carol.
|
|
||||||
|
|
||||||
There are also other sources for group membership inconsistency:
|
|
||||||
|
|
||||||
- leaving/deleting/adding in larger groups, while being offline,
|
|
||||||
increases chances for inconsistent group membership
|
|
||||||
|
|
||||||
- dropped group-membership messages
|
|
||||||
|
|
||||||
- group-membership messages landing in "Spam"
|
|
||||||
|
|
||||||
|
|
||||||
Note that all these problems (can) also happen with verified groups,
|
|
||||||
then raising "false alarms" which could lure people to ignore such issues.
|
|
||||||
|
|
||||||
IOW, it's clear we need to do something about it to improve overall
|
|
||||||
reliability in group-settings.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Solution: replay group modification messages on inconsistencies
|
|
||||||
------------------------------------------------------------------
|
|
||||||
|
|
||||||
For brevity let's abbreviate "group membership modification" as **GMM**.
|
|
||||||
|
|
||||||
Delta chat has explicit GMM messages, typically encrypted to the group members
|
|
||||||
as seen by the device that sends the GMM. The `Spec <https://github.com/deltachat/deltachat-core-rust/blob/master/spec.md#add-and-remove-members>`_ details the Mime headers and format.
|
|
||||||
|
|
||||||
If we detect membership inconsistencies we can resend relevant GMM messages
|
|
||||||
to the respective chat. The receiving devices can process those GMM messages
|
|
||||||
as if it would be an incoming message. If for example they have already seen
|
|
||||||
the Message-ID of the GMM message, they will ignore the message. It's
|
|
||||||
probably useful to record GMM message in their original MIME-format and
|
|
||||||
not invent a new recording format. Few notes on three aspects:
|
|
||||||
|
|
||||||
- **group-membership-tracking**: All valid GMM messages are persisted in
|
|
||||||
their full raw (signed/encrypted?) MIME-format in the DB. Note that GMM messages
|
|
||||||
already are in the msgs table, and there is a mime_header column which we could
|
|
||||||
extend to contain the raw Mime GMM message.
|
|
||||||
|
|
||||||
- **consistency_checking**: If an incoming GMM has a member list which is
|
|
||||||
not consistent with our own view, broadcast a "Group-Member-Correction"
|
|
||||||
message to all members containing a multipart list of GMMs.
|
|
||||||
|
|
||||||
- **correcting_memberships**: Upon receiving a Group-Member-Correction
|
|
||||||
message we pass the contained GMMs to the "incoming mail pipeline"
|
|
||||||
(without **consistency_checking** them, to avoid recursion issues)
|
|
||||||
|
|
||||||
|
|
||||||
Alice/Carol and Bob/Doris getting on the same page
|
|
||||||
++++++++++++++++++++++++++++++++++++++++++++++++++
|
|
||||||
|
|
||||||
Recall that Alice/Carol and Bob/Doris had a differening view of
|
|
||||||
group membership. With the proposed solution, when Bob receives
|
|
||||||
Alice's "Carol added" message, he will notice that Alice (and thus
|
|
||||||
also carol) did not know about Doris. Bob's device sends a
|
|
||||||
"Chat-Group-Member-Correction" message containing his own GMM
|
|
||||||
when adding Doris. Therefore, the group's membership is healed
|
|
||||||
for everyone in a single broadcast message.
|
|
||||||
|
|
||||||
Alice might also send a Group-member-Correction message,
|
|
||||||
so there is a second chance that the group gets to know all GMMs.
|
|
||||||
|
|
||||||
Note, for example, that if for some reason Bobs and Carols provider
|
|
||||||
drop GMM messages between them (spam) that Alice and Doris can heal
|
|
||||||
it by resending GMM messages whenever they detect them to be out of sync.
|
|
||||||
|
|
||||||
|
|
||||||
Discussions of variants
|
|
||||||
++++++++++++++++++++++++
|
|
||||||
|
|
||||||
- instead of acting on GMM messages we could send corrections
|
|
||||||
for any received message that addresses inconsistent group members but
|
|
||||||
a) this could delay group-membership healing
|
|
||||||
b) could lead to a lot of members sending corrections
|
|
||||||
c) means we might rely on "To-Addresses" which we also like to strike
|
|
||||||
at least for protected chats.
|
|
||||||
|
|
||||||
- instead of broadcasting correction messages we could only send it to
|
|
||||||
the sender of the inconsistent member-added/removed message.
|
|
||||||
A receiver of such a correction message would then need to forward
|
|
||||||
the message to the members it thinks also have an inconsistent view.
|
|
||||||
This sounds complicated and error-prone. Concretely, if Alice
|
|
||||||
receives Bob's "Member-added: Doris" message, then Alice
|
|
||||||
broadcasting the correction message with "Member-added: Carol"
|
|
||||||
would reach all four members, healing group consistency in one step.
|
|
||||||
If Bob meanwhile receives Alice's "Member-Added: Carol" message,
|
|
||||||
Bob would broadcast a correction message to all four members as well.
|
|
||||||
(Imagine a situation where Alice/Bob added Carol/Doris
|
|
||||||
while both being in an offline or bad-connection situation).
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
|
|
||||||
simplify/streamline mark-seen/delete/move/send-mdn job handling
|
|
||||||
---------------------------------------------------------------
|
|
||||||
|
|
||||||
Idea: Introduce a new "msgwork" sql table that looks very
|
|
||||||
much like the jobs table but has a primary key "msgid"
|
|
||||||
and no job id and no foreign-id anymore. This opens up
|
|
||||||
bulk-processing by looking at the whole table and combining
|
|
||||||
flag-setting to reduce imap-roundtrips and select-folder calls.
|
|
||||||
|
|
||||||
Concretely, these IMAP jobs:
|
|
||||||
|
|
||||||
DeleteMsgOnImap
|
|
||||||
MarkseenMsgOnImap
|
|
||||||
MoveMsg
|
|
||||||
|
|
||||||
Would be replaced by a few per-message columns in the new msgwork table:
|
|
||||||
|
|
||||||
- needs_mark_seen: (bool) message shall be marked as seen on imap
|
|
||||||
- needs_to_move: (bool) message should be moved to mvbox_folder
|
|
||||||
- deletion_time: (target_time or 0) message shall be deleted at specified time
|
|
||||||
- needs_send_mdn: (bool) MDN shall be sent
|
|
||||||
|
|
||||||
The various places that currently add the (replaced) jobs
|
|
||||||
would now add/modify the respective message record in the message-work table.
|
|
||||||
|
|
||||||
Looking at a single message-work entry conceptually looks like this::
|
|
||||||
|
|
||||||
if msg.server_uid==0:
|
|
||||||
return RetryLater # nothing can be done without server_uid
|
|
||||||
|
|
||||||
if msg.deletion_time > current_time:
|
|
||||||
imap.mark_delete(msg) # might trigger early exit with a RetryLater/Failed
|
|
||||||
clear(needs_deletion)
|
|
||||||
clear(mark_seen)
|
|
||||||
|
|
||||||
if needs_mark_seen:
|
|
||||||
imap.mark_seen(msg) # might trigger early exit with a RetryLater/Failed
|
|
||||||
clear(needs_mark_seen)
|
|
||||||
|
|
||||||
if needs_send_mdn:
|
|
||||||
schedule_smtp_send_mdn(msg)
|
|
||||||
clear(needs_send_mdn)
|
|
||||||
|
|
||||||
if any_flag_set():
|
|
||||||
retrylater
|
|
||||||
# remove msgwork entry from table
|
|
||||||
|
|
||||||
|
|
||||||
Notes/Questions:
|
|
||||||
|
|
||||||
- it's unclear how much we need per-message retry-time tracking/backoff
|
|
||||||
|
|
||||||
- drafting bulk processing algo is useful before
|
|
||||||
going for the implementation, i.e. including select_folder calls etc.
|
|
||||||
|
|
||||||
- maybe it's better to not have bools for the flags but
|
|
||||||
|
|
||||||
0 (no change)
|
|
||||||
1 (set the imap flag)
|
|
||||||
2 (clear the imap flag)
|
|
||||||
|
|
||||||
and design such that we can cover all imap flags.
|
|
||||||
|
|
||||||
- It might not be neccessary to keep needs_send_mdn state in this table
|
|
||||||
if this can be decided rather when we succeed with mark_seen/mark_delete.
|
|
||||||
@@ -1,185 +0,0 @@
|
|||||||
# Webxdc Developer Reference
|
|
||||||
|
|
||||||
## Webxdc File Format
|
|
||||||
|
|
||||||
- a **Webxdc app** is a **ZIP-file** with the extension `.xdc`
|
|
||||||
- the ZIP-file must use the default compression methods as of RFC 1950,
|
|
||||||
this is "Deflate" or "Store"
|
|
||||||
- the ZIP-file must contain at least the file `index.html`
|
|
||||||
- if the Webxdc app is started, `index.html` is opened in a restricted webview
|
|
||||||
that allow accessing resources only from the ZIP-file
|
|
||||||
|
|
||||||
|
|
||||||
## Webxdc API
|
|
||||||
|
|
||||||
There are some additional APIs available once `webxdc.js` is included
|
|
||||||
(the file will be provided by the concrete implementations,
|
|
||||||
no need to add `webxdc.js` to your ZIP-file):
|
|
||||||
|
|
||||||
```html
|
|
||||||
<script src="webxdc.js"></script>
|
|
||||||
```
|
|
||||||
|
|
||||||
### sendUpdate()
|
|
||||||
|
|
||||||
```js
|
|
||||||
window.webxdc.sendUpdate(update, descr);
|
|
||||||
```
|
|
||||||
|
|
||||||
Webxdc apps are usually shared in a chat and run independently on each peer.
|
|
||||||
To get a shared state, the peers use `sendUpdate()` to send updates to each other.
|
|
||||||
|
|
||||||
- `update`: an object with the following properties:
|
|
||||||
- `update.payload`: any javascript primitive, array or object.
|
|
||||||
- `update.info`: optional, short, informational message that will be added to the chat,
|
|
||||||
eg. "Alice voted" or "Bob scored 123 in MyGame";
|
|
||||||
usually only one line of text is shown,
|
|
||||||
use this option sparingly to not spam the chat.
|
|
||||||
- `update.summary`: optional, short text, shown beside app icon;
|
|
||||||
it is recommended to use some aggregated value, eg. "8 votes", "Highscore: 123"
|
|
||||||
|
|
||||||
- `descr`: short, human-readable description what this update is about.
|
|
||||||
this is shown eg. as a fallback text in an email program.
|
|
||||||
|
|
||||||
All peers, including the sending one,
|
|
||||||
will receive the update by the callback given to `setUpdateListener()`.
|
|
||||||
|
|
||||||
There are situations where the user cannot send messages to a chat,
|
|
||||||
eg. if the webxdc instance comes as a contact request or if the user has left a group.
|
|
||||||
In these cases, you can still call `sendUpdate()`,
|
|
||||||
however, the update won't be sent to other peers
|
|
||||||
and you won't get the update by `setUpdateListener()`.
|
|
||||||
|
|
||||||
|
|
||||||
### setUpdateListener()
|
|
||||||
|
|
||||||
```js
|
|
||||||
let promise = window.webxdc.setUpdateListener((update) => {}, serial);
|
|
||||||
```
|
|
||||||
|
|
||||||
With `setUpdateListener()` you define a callback that receives the updates
|
|
||||||
sent by `sendUpdate()`. The callback is called for updates sent by you or other peers.
|
|
||||||
The `serial` specifies the last serial that you know about (defaults to 0).
|
|
||||||
The returned promise resolves when the listener has processed all the update messages known at the time when `setUpdateListener` was called.
|
|
||||||
|
|
||||||
Each `update` which is passed to the callback comes with the following properties:
|
|
||||||
|
|
||||||
- `update.payload`: equals the payload given to `sendUpdate()`
|
|
||||||
|
|
||||||
- `update.serial`: the serial number of this update.
|
|
||||||
Serials are larger `0` and newer serials have higher numbers.
|
|
||||||
There may be gaps in the serials
|
|
||||||
and it is not guaranteed that the next serial is exactly incremented by one.
|
|
||||||
|
|
||||||
- `update.max_serial`: the maximum serial currently known.
|
|
||||||
If `max_serial` equals `serial` this update is the last update (until new network messages arrive).
|
|
||||||
|
|
||||||
- `update.info`: optional, short, informational message (see `send_update`)
|
|
||||||
|
|
||||||
- `update.summary`: optional, short text, shown beside app icon (see `send_update`)
|
|
||||||
|
|
||||||
|
|
||||||
### selfAddr
|
|
||||||
|
|
||||||
```js
|
|
||||||
window.webxdc.selfAddr
|
|
||||||
```
|
|
||||||
|
|
||||||
Property with the peer's own address.
|
|
||||||
This is esp. useful if you want to differ between different peers -
|
|
||||||
just send the address along with the payload,
|
|
||||||
and, if needed, compare the payload addresses against selfAddr() later on.
|
|
||||||
|
|
||||||
|
|
||||||
### selfName
|
|
||||||
|
|
||||||
```js
|
|
||||||
window.webxdc.selfName
|
|
||||||
```
|
|
||||||
|
|
||||||
Property with the peer's own name.
|
|
||||||
This is name chosen by the user in their settings,
|
|
||||||
if there is nothing set, that defaults to the peer's address.
|
|
||||||
|
|
||||||
|
|
||||||
## manifest.toml
|
|
||||||
|
|
||||||
If the ZIP-file contains a `manifest.toml` in its root directory,
|
|
||||||
some basic information are read and used from there.
|
|
||||||
|
|
||||||
the `manifest.toml` has the following format
|
|
||||||
|
|
||||||
```toml
|
|
||||||
name = "My App Name"
|
|
||||||
```
|
|
||||||
|
|
||||||
- **name** - The name of the app.
|
|
||||||
If no name is set or if there is no manifest, the filename is used as the app name.
|
|
||||||
|
|
||||||
|
|
||||||
## App Icon
|
|
||||||
|
|
||||||
If the ZIP-root contains an `icon.png` or `icon.jpg`,
|
|
||||||
these files are used as the icon for the app.
|
|
||||||
The icon should be a square at reasonable width/height;
|
|
||||||
round corners etc. will be added by the implementations as needed.
|
|
||||||
If no icon is set, a default icon will be used.
|
|
||||||
|
|
||||||
|
|
||||||
## Webxdc Examples
|
|
||||||
|
|
||||||
The following example shows an input field and every input is show on all peers.
|
|
||||||
|
|
||||||
```html
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8"/>
|
|
||||||
<script src="webxdc.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<input id="input" type="text"/>
|
|
||||||
<a href="" onclick="sendMsg(); return false;">Send</a>
|
|
||||||
<p id="output"></p>
|
|
||||||
<script>
|
|
||||||
|
|
||||||
function sendMsg() {
|
|
||||||
msg = document.getElementById("input").value;
|
|
||||||
window.webxdc.sendUpdate({payload: msg}, 'Someone typed "'+msg+'".');
|
|
||||||
}
|
|
||||||
|
|
||||||
function receiveUpdate(update) {
|
|
||||||
document.getElementById('output').innerHTML += update.payload + "<br>";
|
|
||||||
}
|
|
||||||
|
|
||||||
window.webxdc.setUpdateListener(receiveUpdate, 0);
|
|
||||||
</script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
```
|
|
||||||
|
|
||||||
[Webxdc Development Tool](https://github.com/deltachat/webxdc-dev)
|
|
||||||
offers an **Webxdc Simulator** that can be used in many browsers without any installation needed.
|
|
||||||
You can also use that repository as a template for your own app -
|
|
||||||
just clone and start adapting things to your need.
|
|
||||||
|
|
||||||
|
|
||||||
### Advanced Examples
|
|
||||||
|
|
||||||
- [2048](https://github.com/adbenitez/2048.xdc)
|
|
||||||
- [Draw](https://github.com/adbenitez/draw.xdc)
|
|
||||||
- [Poll](https://github.com/r10s/webxdc-poll/)
|
|
||||||
- [Tic Tac Toe](https://github.com/Simon-Laux/tictactoe.xdc)
|
|
||||||
- Even more with [Topic #webxdc on Github](https://github.com/topics/webxdc)
|
|
||||||
|
|
||||||
|
|
||||||
## Closing Remarks
|
|
||||||
|
|
||||||
- older devices might not have the newest js features in their webview,
|
|
||||||
you may want to transpile your code down to an older js version eg. with https://babeljs.io
|
|
||||||
- viewport and scaling features are implementation specific,
|
|
||||||
if you want to have an explicit behavior, you can add eg.
|
|
||||||
`<meta name="viewport" content="initial-scale=1; user-scalable=no">` to your Webxdc
|
|
||||||
- there are tons of ideas for enhancements of the API and the file format,
|
|
||||||
eg. in the future, we will may define icon- and manifest-files,
|
|
||||||
allow to aggregate the state or add metadata.
|
|
||||||
@@ -6,120 +6,212 @@
|
|||||||
|
|
||||||
#[macro_use]
|
#[macro_use]
|
||||||
extern crate deltachat;
|
extern crate deltachat;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate failure;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate lazy_static;
|
||||||
|
#[macro_use]
|
||||||
|
extern crate rusqlite;
|
||||||
|
|
||||||
use std::borrow::Cow::{self, Borrowed, Owned};
|
use std::borrow::Cow::{self, Borrowed, Owned};
|
||||||
use std::io::{self, Write};
|
use std::io::{self, Write};
|
||||||
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::{Arc, Mutex, RwLock};
|
||||||
|
|
||||||
use ansi_term::Color;
|
|
||||||
use anyhow::{bail, Error};
|
|
||||||
use async_std::path::Path;
|
|
||||||
use deltachat::chat::ChatId;
|
use deltachat::chat::ChatId;
|
||||||
use deltachat::config;
|
use deltachat::config;
|
||||||
use deltachat::context::*;
|
use deltachat::context::*;
|
||||||
|
use deltachat::job::*;
|
||||||
use deltachat::oauth2::*;
|
use deltachat::oauth2::*;
|
||||||
use deltachat::securejoin::*;
|
use deltachat::securejoin::*;
|
||||||
use deltachat::EventType;
|
use deltachat::Event;
|
||||||
use log::{error, info, warn};
|
|
||||||
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
use rustyline::completion::{Completer, FilenameCompleter, Pair};
|
||||||
use rustyline::config::OutputStreamType;
|
use rustyline::config::OutputStreamType;
|
||||||
use rustyline::error::ReadlineError;
|
use rustyline::error::ReadlineError;
|
||||||
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
|
use rustyline::highlight::{Highlighter, MatchingBracketHighlighter};
|
||||||
use rustyline::hint::{Hinter, HistoryHinter};
|
use rustyline::hint::{Hinter, HistoryHinter};
|
||||||
use rustyline::validate::Validator;
|
|
||||||
use rustyline::{
|
use rustyline::{
|
||||||
Cmd, CompletionType, Config, Context as RustyContext, EditMode, Editor, Helper, KeyEvent,
|
Cmd, CompletionType, Config, Context as RustyContext, EditMode, Editor, Helper, KeyPress,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod cmdline;
|
mod cmdline;
|
||||||
use self::cmdline::*;
|
use self::cmdline::*;
|
||||||
use deltachat::qr_code_generator::get_securejoin_qr_svg;
|
|
||||||
use std::fs;
|
|
||||||
|
|
||||||
/// Event Handler
|
// Event Handler
|
||||||
fn receive_event(event: EventType) {
|
|
||||||
let yellow = Color::Yellow.normal();
|
fn receive_event(_context: &Context, event: Event) {
|
||||||
match event {
|
match event {
|
||||||
EventType::Info(msg) => {
|
Event::Info(msg) => {
|
||||||
/* do not show the event as this would fill the screen */
|
/* do not show the event as this would fill the screen */
|
||||||
info!("{}", msg);
|
println!("{}", msg);
|
||||||
}
|
}
|
||||||
EventType::SmtpConnected(msg) => {
|
Event::SmtpConnected(msg) => {
|
||||||
info!("[SMTP_CONNECTED] {}", msg);
|
println!("[DC_EVENT_SMTP_CONNECTED] {}", msg);
|
||||||
}
|
}
|
||||||
EventType::ImapConnected(msg) => {
|
Event::ImapConnected(msg) => {
|
||||||
info!("[IMAP_CONNECTED] {}", msg);
|
println!("[DC_EVENT_IMAP_CONNECTED] {}", msg);
|
||||||
}
|
}
|
||||||
EventType::SmtpMessageSent(msg) => {
|
Event::SmtpMessageSent(msg) => {
|
||||||
info!("[SMTP_MESSAGE_SENT] {}", msg);
|
println!("[DC_EVENT_SMTP_MESSAGE_SENT] {}", msg);
|
||||||
}
|
}
|
||||||
EventType::Warning(msg) => {
|
Event::Warning(msg) => {
|
||||||
warn!("{}", msg);
|
println!("[Warning] {}", msg);
|
||||||
}
|
}
|
||||||
EventType::Error(msg) => {
|
Event::Error(msg) => {
|
||||||
error!("{}", msg);
|
println!("\x1b[31m[DC_EVENT_ERROR] {}\x1b[0m", msg);
|
||||||
}
|
}
|
||||||
EventType::ErrorSelfNotInGroup(msg) => {
|
Event::ErrorNetwork(msg) => {
|
||||||
error!("[SELF_NOT_IN_GROUP] {}", msg);
|
println!("\x1b[31m[DC_EVENT_ERROR_NETWORK] msg={}\x1b[0m", msg);
|
||||||
}
|
}
|
||||||
EventType::MsgsChanged { chat_id, msg_id } => {
|
Event::ErrorSelfNotInGroup(msg) => {
|
||||||
info!(
|
println!("\x1b[31m[DC_EVENT_ERROR_SELF_NOT_IN_GROUP] {}\x1b[0m", msg);
|
||||||
"{}",
|
}
|
||||||
yellow.paint(format!(
|
Event::MsgsChanged { chat_id, msg_id } => {
|
||||||
"Received MSGS_CHANGED(chat_id={}, msg_id={})",
|
print!(
|
||||||
chat_id, msg_id,
|
"\x1b[33m{{Received DC_EVENT_MSGS_CHANGED(chat_id={}, msg_id={})}}\n\x1b[0m",
|
||||||
))
|
chat_id, msg_id,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
EventType::ContactsChanged(_) => {
|
Event::ContactsChanged(_) => {
|
||||||
info!("{}", yellow.paint("Received CONTACTS_CHANGED()"));
|
print!("\x1b[33m{{Received DC_EVENT_CONTACTS_CHANGED()}}\n\x1b[0m");
|
||||||
}
|
}
|
||||||
EventType::LocationChanged(contact) => {
|
Event::LocationChanged(contact) => {
|
||||||
info!(
|
print!(
|
||||||
"{}",
|
"\x1b[33m{{Received DC_EVENT_LOCATION_CHANGED(contact={:?})}}\n\x1b[0m",
|
||||||
yellow.paint(format!("Received LOCATION_CHANGED(contact={:?})", contact))
|
contact,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
EventType::ConfigureProgress { progress, comment } => {
|
Event::ConfigureProgress(progress) => {
|
||||||
if let Some(comment) = comment {
|
print!(
|
||||||
info!(
|
"\x1b[33m{{Received DC_EVENT_CONFIGURE_PROGRESS({} ‰)}}\n\x1b[0m",
|
||||||
"{}",
|
progress,
|
||||||
yellow.paint(format!(
|
|
||||||
"Received CONFIGURE_PROGRESS({} ‰, {})",
|
|
||||||
progress, comment
|
|
||||||
))
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
info!(
|
|
||||||
"{}",
|
|
||||||
yellow.paint(format!("Received CONFIGURE_PROGRESS({} ‰)", progress))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
EventType::ImexProgress(progress) => {
|
|
||||||
info!(
|
|
||||||
"{}",
|
|
||||||
yellow.paint(format!("Received IMEX_PROGRESS({} ‰)", progress))
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
EventType::ImexFileWritten(file) => {
|
Event::ImexProgress(progress) => {
|
||||||
info!(
|
print!(
|
||||||
"{}",
|
"\x1b[33m{{Received DC_EVENT_IMEX_PROGRESS({} ‰)}}\n\x1b[0m",
|
||||||
yellow.paint(format!("Received IMEX_FILE_WRITTEN({})", file.display()))
|
progress,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
EventType::ChatModified(chat) => {
|
Event::ImexFileWritten(file) => {
|
||||||
info!(
|
print!(
|
||||||
"{}",
|
"\x1b[33m{{Received DC_EVENT_IMEX_FILE_WRITTEN({})}}\n\x1b[0m",
|
||||||
yellow.paint(format!("Received CHAT_MODIFIED({})", chat))
|
file.display()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
Event::ChatModified(chat) => {
|
||||||
|
print!(
|
||||||
|
"\x1b[33m{{Received DC_EVENT_CHAT_MODIFIED({})}}\n\x1b[0m",
|
||||||
|
chat
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
info!("Received {:?}", event);
|
print!("\x1b[33m{{Received {:?}}}\n\x1b[0m", event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Threads for waiting for messages and for jobs
|
||||||
|
|
||||||
|
lazy_static! {
|
||||||
|
static ref HANDLE: Arc<Mutex<Option<Handle>>> = Arc::new(Mutex::new(None));
|
||||||
|
static ref IS_RUNNING: AtomicBool = AtomicBool::new(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
struct Handle {
|
||||||
|
handle_imap: Option<std::thread::JoinHandle<()>>,
|
||||||
|
handle_mvbox: Option<std::thread::JoinHandle<()>>,
|
||||||
|
handle_sentbox: Option<std::thread::JoinHandle<()>>,
|
||||||
|
handle_smtp: Option<std::thread::JoinHandle<()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! while_running {
|
||||||
|
($code:block) => {
|
||||||
|
if IS_RUNNING.load(Ordering::Relaxed) {
|
||||||
|
$code
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_threads(c: Arc<RwLock<Context>>) {
|
||||||
|
if HANDLE.clone().lock().unwrap().is_some() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Starting threads");
|
||||||
|
IS_RUNNING.store(true, Ordering::Relaxed);
|
||||||
|
|
||||||
|
let ctx = c.clone();
|
||||||
|
let handle_imap = std::thread::spawn(move || loop {
|
||||||
|
while_running!({
|
||||||
|
perform_inbox_jobs(&ctx.read().unwrap());
|
||||||
|
perform_inbox_fetch(&ctx.read().unwrap());
|
||||||
|
while_running!({
|
||||||
|
let context = ctx.read().unwrap();
|
||||||
|
perform_inbox_idle(&context);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let ctx = c.clone();
|
||||||
|
let handle_mvbox = std::thread::spawn(move || loop {
|
||||||
|
while_running!({
|
||||||
|
perform_mvbox_fetch(&ctx.read().unwrap());
|
||||||
|
while_running!({
|
||||||
|
perform_mvbox_idle(&ctx.read().unwrap());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let ctx = c.clone();
|
||||||
|
let handle_sentbox = std::thread::spawn(move || loop {
|
||||||
|
while_running!({
|
||||||
|
perform_sentbox_fetch(&ctx.read().unwrap());
|
||||||
|
while_running!({
|
||||||
|
perform_sentbox_idle(&ctx.read().unwrap());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let ctx = c;
|
||||||
|
let handle_smtp = std::thread::spawn(move || loop {
|
||||||
|
while_running!({
|
||||||
|
perform_smtp_jobs(&ctx.read().unwrap());
|
||||||
|
while_running!({
|
||||||
|
perform_smtp_idle(&ctx.read().unwrap());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
*HANDLE.clone().lock().unwrap() = Some(Handle {
|
||||||
|
handle_imap: Some(handle_imap),
|
||||||
|
handle_mvbox: Some(handle_mvbox),
|
||||||
|
handle_sentbox: Some(handle_sentbox),
|
||||||
|
handle_smtp: Some(handle_smtp),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_threads(context: &Context) {
|
||||||
|
if let Some(ref mut handle) = *HANDLE.clone().lock().unwrap() {
|
||||||
|
println!("Stopping threads");
|
||||||
|
IS_RUNNING.store(false, Ordering::Relaxed);
|
||||||
|
|
||||||
|
interrupt_inbox_idle(context);
|
||||||
|
interrupt_mvbox_idle(context);
|
||||||
|
interrupt_sentbox_idle(context);
|
||||||
|
interrupt_smtp_idle(context);
|
||||||
|
|
||||||
|
handle.handle_imap.take().unwrap().join().unwrap();
|
||||||
|
handle.handle_mvbox.take().unwrap().join().unwrap();
|
||||||
|
handle.handle_sentbox.take().unwrap().join().unwrap();
|
||||||
|
handle.handle_smtp.take().unwrap().join().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// === The main loop
|
// === The main loop
|
||||||
|
|
||||||
struct DcHelper {
|
struct DcHelper {
|
||||||
@@ -156,27 +248,28 @@ const IMEX_COMMANDS: [&str; 12] = [
|
|||||||
"stop",
|
"stop",
|
||||||
];
|
];
|
||||||
|
|
||||||
const DB_COMMANDS: [&str; 10] = [
|
const DB_COMMANDS: [&str; 11] = [
|
||||||
"info",
|
"info",
|
||||||
|
"open",
|
||||||
|
"close",
|
||||||
"set",
|
"set",
|
||||||
"get",
|
"get",
|
||||||
"oauth2",
|
"oauth2",
|
||||||
"configure",
|
"configure",
|
||||||
"connect",
|
"connect",
|
||||||
"disconnect",
|
"disconnect",
|
||||||
"connectivity",
|
|
||||||
"maybenetwork",
|
"maybenetwork",
|
||||||
"housekeeping",
|
"housekeeping",
|
||||||
];
|
];
|
||||||
|
|
||||||
const CHAT_COMMANDS: [&str; 36] = [
|
const CHAT_COMMANDS: [&str; 26] = [
|
||||||
"listchats",
|
"listchats",
|
||||||
"listarchived",
|
"listarchived",
|
||||||
"chat",
|
"chat",
|
||||||
"createchat",
|
"createchat",
|
||||||
|
"createchatbymsg",
|
||||||
"creategroup",
|
"creategroup",
|
||||||
"createbroadcast",
|
"createverified",
|
||||||
"createprotected",
|
|
||||||
"addmember",
|
"addmember",
|
||||||
"removemember",
|
"removemember",
|
||||||
"groupname",
|
"groupname",
|
||||||
@@ -189,63 +282,38 @@ const CHAT_COMMANDS: [&str; 36] = [
|
|||||||
"send",
|
"send",
|
||||||
"sendimage",
|
"sendimage",
|
||||||
"sendfile",
|
"sendfile",
|
||||||
"sendhtml",
|
|
||||||
"sendsyncmsg",
|
|
||||||
"sendupdate",
|
|
||||||
"videochat",
|
|
||||||
"draft",
|
"draft",
|
||||||
"listmedia",
|
"listmedia",
|
||||||
"archive",
|
"archive",
|
||||||
"unarchive",
|
"unarchive",
|
||||||
"pin",
|
"pin",
|
||||||
"unpin",
|
"unpin",
|
||||||
"mute",
|
|
||||||
"unmute",
|
|
||||||
"protect",
|
|
||||||
"unprotect",
|
|
||||||
"delchat",
|
"delchat",
|
||||||
"accept",
|
|
||||||
"blockchat",
|
|
||||||
];
|
];
|
||||||
const MESSAGE_COMMANDS: [&str; 8] = [
|
const MESSAGE_COMMANDS: [&str; 8] = [
|
||||||
"listmsgs",
|
"listmsgs",
|
||||||
"msginfo",
|
"msginfo",
|
||||||
"listfresh",
|
"listfresh",
|
||||||
"forward",
|
"forward",
|
||||||
"resend",
|
|
||||||
"markseen",
|
"markseen",
|
||||||
|
"star",
|
||||||
|
"unstar",
|
||||||
"delmsg",
|
"delmsg",
|
||||||
"download",
|
|
||||||
];
|
];
|
||||||
const CONTACT_COMMANDS: [&str; 9] = [
|
const CONTACT_COMMANDS: [&str; 6] = [
|
||||||
"listcontacts",
|
"listcontacts",
|
||||||
"listverified",
|
"listverified",
|
||||||
"addcontact",
|
"addcontact",
|
||||||
"contactinfo",
|
"contactinfo",
|
||||||
"delcontact",
|
"delcontact",
|
||||||
"cleanupcontacts",
|
"cleanupcontacts",
|
||||||
"block",
|
|
||||||
"unblock",
|
|
||||||
"listblocked",
|
|
||||||
];
|
];
|
||||||
const MISC_COMMANDS: [&str; 11] = [
|
const MISC_COMMANDS: [&str; 9] = [
|
||||||
"getqr",
|
"getqr", "getbadqr", "checkqr", "event", "fileinfo", "clear", "exit", "quit", "help",
|
||||||
"getqrsvg",
|
|
||||||
"getbadqr",
|
|
||||||
"checkqr",
|
|
||||||
"joinqr",
|
|
||||||
"fileinfo",
|
|
||||||
"clear",
|
|
||||||
"exit",
|
|
||||||
"quit",
|
|
||||||
"help",
|
|
||||||
"estimatedeletion",
|
|
||||||
];
|
];
|
||||||
|
|
||||||
impl Hinter for DcHelper {
|
impl Hinter for DcHelper {
|
||||||
type Hint = String;
|
fn hint(&self, line: &str, pos: usize, ctx: &RustyContext<'_>) -> Option<String> {
|
||||||
|
|
||||||
fn hint(&self, line: &str, pos: usize, ctx: &RustyContext<'_>) -> Option<Self::Hint> {
|
|
||||||
if !line.is_empty() {
|
if !line.is_empty() {
|
||||||
for &cmds in &[
|
for &cmds in &[
|
||||||
&IMEX_COMMANDS[..],
|
&IMEX_COMMANDS[..],
|
||||||
@@ -267,10 +335,11 @@ impl Hinter for DcHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static COLORED_PROMPT: &str = "\x1b[1;32m> \x1b[0m";
|
static COLORED_PROMPT: &str = "\x1b[1;32m> \x1b[0m";
|
||||||
|
static PROMPT: &str = "> ";
|
||||||
|
|
||||||
impl Highlighter for DcHelper {
|
impl Highlighter for DcHelper {
|
||||||
fn highlight_prompt<'b, 's: 'b, 'p: 'b>(&self, prompt: &'p str, default: bool) -> Cow<'b, str> {
|
fn highlight_prompt<'p>(&self, prompt: &'p str) -> Cow<'p, str> {
|
||||||
if default {
|
if prompt == PROMPT {
|
||||||
Borrowed(COLORED_PROMPT)
|
Borrowed(COLORED_PROMPT)
|
||||||
} else {
|
} else {
|
||||||
Borrowed(prompt)
|
Borrowed(prompt)
|
||||||
@@ -291,83 +360,70 @@ impl Highlighter for DcHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Helper for DcHelper {}
|
impl Helper for DcHelper {}
|
||||||
impl Validator for DcHelper {}
|
|
||||||
|
|
||||||
async fn start(args: Vec<String>) -> Result<(), Error> {
|
fn main_0(args: Vec<String>) -> Result<(), failure::Error> {
|
||||||
if args.len() < 2 {
|
if args.len() < 2 {
|
||||||
println!("Error: Bad arguments, expected [db-name].");
|
println!("Error: Bad arguments, expected [db-name].");
|
||||||
bail!("No db-name specified");
|
return Err(format_err!("No db-name specified"));
|
||||||
}
|
}
|
||||||
let context = Context::new(Path::new(&args[1]).to_path_buf(), 0).await?;
|
let context = Context::new(
|
||||||
|
Box::new(receive_event),
|
||||||
let events = context.get_event_emitter();
|
"CLI".into(),
|
||||||
async_std::task::spawn(async move {
|
Path::new(&args[1]).to_path_buf(),
|
||||||
while let Some(event) = events.recv().await {
|
)?;
|
||||||
receive_event(event.typ);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
println!("Delta Chat Core is awaiting your commands.");
|
println!("Delta Chat Core is awaiting your commands.");
|
||||||
|
|
||||||
|
let ctx = Arc::new(RwLock::new(context));
|
||||||
|
|
||||||
let config = Config::builder()
|
let config = Config::builder()
|
||||||
.history_ignore_space(true)
|
.history_ignore_space(true)
|
||||||
.completion_type(CompletionType::List)
|
.completion_type(CompletionType::List)
|
||||||
.edit_mode(EditMode::Emacs)
|
.edit_mode(EditMode::Emacs)
|
||||||
.output_stream(OutputStreamType::Stdout)
|
.output_stream(OutputStreamType::Stdout)
|
||||||
.build();
|
.build();
|
||||||
let mut selected_chat = ChatId::default();
|
let h = DcHelper {
|
||||||
let (reader_s, reader_r) = async_std::channel::bounded(100);
|
completer: FilenameCompleter::new(),
|
||||||
let input_loop = async_std::task::spawn_blocking(move || {
|
highlighter: MatchingBracketHighlighter::new(),
|
||||||
let h = DcHelper {
|
hinter: HistoryHinter {},
|
||||||
completer: FilenameCompleter::new(),
|
};
|
||||||
highlighter: MatchingBracketHighlighter::new(),
|
let mut rl = Editor::with_config(config);
|
||||||
hinter: HistoryHinter {},
|
rl.set_helper(Some(h));
|
||||||
};
|
rl.bind_sequence(KeyPress::Meta('N'), Cmd::HistorySearchForward);
|
||||||
let mut rl = Editor::with_config(config);
|
rl.bind_sequence(KeyPress::Meta('P'), Cmd::HistorySearchBackward);
|
||||||
rl.set_helper(Some(h));
|
if rl.load_history(".dc-history.txt").is_err() {
|
||||||
rl.bind_sequence(KeyEvent::alt('N'), Cmd::HistorySearchForward);
|
println!("No previous history.");
|
||||||
rl.bind_sequence(KeyEvent::alt('P'), Cmd::HistorySearchBackward);
|
}
|
||||||
if rl.load_history(".dc-history.txt").is_err() {
|
|
||||||
println!("No previous history.");
|
|
||||||
}
|
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
let p = "> ";
|
let p = "> ";
|
||||||
let readline = rl.readline(p);
|
let readline = rl.readline(&p);
|
||||||
|
match readline {
|
||||||
match readline {
|
Ok(line) => {
|
||||||
Ok(line) => {
|
// TODO: ignore "set mail_pw"
|
||||||
// TODO: ignore "set mail_pw"
|
rl.add_history_entry(line.as_str());
|
||||||
rl.add_history_entry(line.as_str());
|
let ctx = ctx.clone();
|
||||||
async_std::task::block_on(reader_s.send(line)).unwrap();
|
match handle_cmd(line.trim(), ctx) {
|
||||||
}
|
Ok(ExitResult::Continue) => {}
|
||||||
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
|
Ok(ExitResult::Exit) => break,
|
||||||
println!("Exiting...");
|
Err(err) => println!("Error: {}", err),
|
||||||
drop(reader_s);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
println!("Error: {}", err);
|
|
||||||
drop(reader_s);
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
Err(ReadlineError::Interrupted) | Err(ReadlineError::Eof) => {
|
||||||
|
println!("Exiting...");
|
||||||
rl.save_history(".dc-history.txt")?;
|
break;
|
||||||
println!("history saved");
|
}
|
||||||
Ok::<_, Error>(())
|
Err(err) => {
|
||||||
});
|
println!("Error: {}", err);
|
||||||
|
break;
|
||||||
while let Ok(line) = reader_r.recv().await {
|
}
|
||||||
match handle_cmd(line.trim(), context.clone(), &mut selected_chat).await {
|
|
||||||
Ok(ExitResult::Continue) => {}
|
|
||||||
Ok(ExitResult::Exit) => break,
|
|
||||||
Err(err) => println!("Error: {}", err),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
context.stop_io().await;
|
rl.save_history(".dc-history.txt")?;
|
||||||
input_loop.await?;
|
println!("history saved");
|
||||||
|
{
|
||||||
|
stop_threads(&ctx.read().unwrap());
|
||||||
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -378,29 +434,43 @@ enum ExitResult {
|
|||||||
Exit,
|
Exit,
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_cmd(
|
fn handle_cmd(line: &str, ctx: Arc<RwLock<Context>>) -> Result<ExitResult, failure::Error> {
|
||||||
line: &str,
|
|
||||||
ctx: Context,
|
|
||||||
selected_chat: &mut ChatId,
|
|
||||||
) -> Result<ExitResult, Error> {
|
|
||||||
let mut args = line.splitn(2, ' ');
|
let mut args = line.splitn(2, ' ');
|
||||||
let arg0 = args.next().unwrap_or_default();
|
let arg0 = args.next().unwrap_or_default();
|
||||||
let arg1 = args.next().unwrap_or_default();
|
let arg1 = args.next().unwrap_or_default();
|
||||||
|
|
||||||
match arg0 {
|
match arg0 {
|
||||||
"connect" => {
|
"connect" => {
|
||||||
ctx.start_io().await;
|
start_threads(ctx);
|
||||||
}
|
}
|
||||||
"disconnect" => {
|
"disconnect" => {
|
||||||
ctx.stop_io().await;
|
stop_threads(&ctx.read().unwrap());
|
||||||
|
}
|
||||||
|
"smtp-jobs" => {
|
||||||
|
if HANDLE.clone().lock().unwrap().is_some() {
|
||||||
|
println!("smtp-jobs are already running in a thread.",);
|
||||||
|
} else {
|
||||||
|
perform_smtp_jobs(&ctx.read().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"imap-jobs" => {
|
||||||
|
if HANDLE.clone().lock().unwrap().is_some() {
|
||||||
|
println!("inbox-jobs are already running in a thread.");
|
||||||
|
} else {
|
||||||
|
perform_inbox_jobs(&ctx.read().unwrap());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
"configure" => {
|
"configure" => {
|
||||||
ctx.configure().await?;
|
start_threads(ctx.clone());
|
||||||
|
ctx.read().unwrap().configure();
|
||||||
}
|
}
|
||||||
"oauth2" => {
|
"oauth2" => {
|
||||||
if let Some(addr) = ctx.get_config(config::Config::Addr).await? {
|
if let Some(addr) = ctx.read().unwrap().get_config(config::Config::Addr) {
|
||||||
let oauth2_url =
|
let oauth2_url = dc_get_oauth2_url(
|
||||||
dc_get_oauth2_url(&ctx, &addr, "chat.delta:/com.b44t.messenger").await?;
|
&ctx.read().unwrap(),
|
||||||
|
&addr,
|
||||||
|
"chat.delta:/com.b44t.messenger",
|
||||||
|
);
|
||||||
if oauth2_url.is_none() {
|
if oauth2_url.is_none() {
|
||||||
println!("OAuth2 not available for {}.", &addr);
|
println!("OAuth2 not available for {}.", &addr);
|
||||||
} else {
|
} else {
|
||||||
@@ -415,54 +485,43 @@ async fn handle_cmd(
|
|||||||
print!("\x1b[1;1H\x1b[2J");
|
print!("\x1b[1;1H\x1b[2J");
|
||||||
}
|
}
|
||||||
"getqr" | "getbadqr" => {
|
"getqr" | "getbadqr" => {
|
||||||
ctx.start_io().await;
|
start_threads(ctx.clone());
|
||||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
if let Some(mut qr) = dc_get_securejoin_qr(
|
||||||
let mut qr = dc_get_securejoin_qr(&ctx, group).await?;
|
&ctx.read().unwrap(),
|
||||||
if !qr.is_empty() {
|
ChatId::new(arg1.parse().unwrap_or_default()),
|
||||||
if arg0 == "getbadqr" && qr.len() > 40 {
|
) {
|
||||||
qr.replace_range(12..22, "0000000000")
|
if !qr.is_empty() {
|
||||||
}
|
if arg0 == "getbadqr" && qr.len() > 40 {
|
||||||
println!("{}", qr);
|
qr.replace_range(12..22, "0000000000")
|
||||||
let output = Command::new("qrencode")
|
}
|
||||||
.args(&["-t", "ansiutf8", qr.as_str(), "-o", "-"])
|
println!("{}", qr);
|
||||||
.output()
|
let output = Command::new("qrencode")
|
||||||
.expect("failed to execute process");
|
.args(&["-t", "ansiutf8", qr.as_str(), "-o", "-"])
|
||||||
io::stdout().write_all(&output.stdout).unwrap();
|
.output()
|
||||||
io::stderr().write_all(&output.stderr).unwrap();
|
.expect("failed to execute process");
|
||||||
}
|
io::stdout().write_all(&output.stdout).unwrap();
|
||||||
}
|
io::stderr().write_all(&output.stderr).unwrap();
|
||||||
"getqrsvg" => {
|
|
||||||
ctx.start_io().await;
|
|
||||||
let group = arg1.parse::<u32>().ok().map(ChatId::new);
|
|
||||||
let file = dirs::home_dir().unwrap_or_default().join("qr.svg");
|
|
||||||
match get_securejoin_qr_svg(&ctx, group).await {
|
|
||||||
Ok(svg) => {
|
|
||||||
fs::write(&file, svg)?;
|
|
||||||
println!("QR code svg written to: {:#?}", file);
|
|
||||||
}
|
|
||||||
Err(err) => {
|
|
||||||
bail!("Failed to get QR code svg: {}", err);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"joinqr" => {
|
"joinqr" => {
|
||||||
ctx.start_io().await;
|
start_threads(ctx.clone());
|
||||||
if !arg0.is_empty() {
|
if !arg0.is_empty() {
|
||||||
dc_join_securejoin(&ctx, arg1).await?;
|
dc_join_securejoin(&ctx.read().unwrap(), arg1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"exit" | "quit" => return Ok(ExitResult::Exit),
|
"exit" | "quit" => return Ok(ExitResult::Exit),
|
||||||
_ => cmdline(ctx.clone(), line, selected_chat).await?,
|
_ => dc_cmdline(&ctx.read().unwrap(), line)?,
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ExitResult::Continue)
|
Ok(ExitResult::Continue)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> Result<(), Error> {
|
pub fn main() -> Result<(), failure::Error> {
|
||||||
let _ = pretty_env_logger::try_init();
|
let _ = pretty_env_logger::try_init();
|
||||||
|
|
||||||
let args = std::env::args().collect();
|
let args: Vec<String> = std::env::args().collect();
|
||||||
async_std::task::block_on(async move { start(args).await })?;
|
main_0(args)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,100 +1,111 @@
|
|||||||
|
extern crate deltachat;
|
||||||
|
|
||||||
|
use std::sync::{Arc, RwLock};
|
||||||
|
use std::{thread, time};
|
||||||
use tempfile::tempdir;
|
use tempfile::tempdir;
|
||||||
|
|
||||||
use deltachat::chat::{self, ChatId};
|
use deltachat::chat;
|
||||||
use deltachat::chatlist::*;
|
use deltachat::chatlist::*;
|
||||||
use deltachat::config;
|
use deltachat::config;
|
||||||
use deltachat::contact::*;
|
use deltachat::contact::*;
|
||||||
use deltachat::context::*;
|
use deltachat::context::*;
|
||||||
use deltachat::message::Message;
|
use deltachat::job::{
|
||||||
use deltachat::EventType;
|
perform_inbox_fetch, perform_inbox_idle, perform_inbox_jobs, perform_smtp_idle,
|
||||||
|
perform_smtp_jobs,
|
||||||
|
};
|
||||||
|
use deltachat::Event;
|
||||||
|
|
||||||
|
fn cb(_ctx: &Context, event: Event) {
|
||||||
|
print!("[{:?}]", event);
|
||||||
|
|
||||||
fn cb(event: EventType) {
|
|
||||||
match event {
|
match event {
|
||||||
EventType::ConfigureProgress { progress, .. } => {
|
Event::ConfigureProgress(progress) => {
|
||||||
log::info!("progress: {}", progress);
|
println!(" progress: {}", progress);
|
||||||
}
|
}
|
||||||
EventType::Info(msg) => {
|
Event::Info(msg) | Event::Warning(msg) | Event::Error(msg) | Event::ErrorNetwork(msg) => {
|
||||||
log::info!("{}", msg);
|
println!(" {}", msg);
|
||||||
}
|
}
|
||||||
EventType::Warning(msg) => {
|
_ => {
|
||||||
log::warn!("{}", msg);
|
println!();
|
||||||
}
|
|
||||||
EventType::Error(msg) => {
|
|
||||||
log::error!("{}", msg);
|
|
||||||
}
|
|
||||||
event => {
|
|
||||||
log::info!("{:?}", event);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run with `RUST_LOG=simple=info cargo run --release --example simple --features repl -- email pw`.
|
fn main() {
|
||||||
#[async_std::main]
|
|
||||||
async fn main() {
|
|
||||||
pretty_env_logger::try_init_timed().ok();
|
|
||||||
|
|
||||||
let dir = tempdir().unwrap();
|
let dir = tempdir().unwrap();
|
||||||
let dbfile = dir.path().join("db.sqlite");
|
let dbfile = dir.path().join("db.sqlite");
|
||||||
log::info!("creating database {:?}", dbfile);
|
println!("creating database {:?}", dbfile);
|
||||||
let ctx = Context::new(dbfile.into(), 0)
|
let ctx =
|
||||||
.await
|
Context::new(Box::new(cb), "FakeOs".into(), dbfile).expect("Failed to create context");
|
||||||
.expect("Failed to create context");
|
let running = Arc::new(RwLock::new(true));
|
||||||
let info = ctx.get_info().await;
|
let info = ctx.get_info();
|
||||||
log::info!("info: {:#?}", info);
|
let duration = time::Duration::from_millis(4000);
|
||||||
|
println!("info: {:#?}", info);
|
||||||
|
|
||||||
let events = ctx.get_event_emitter();
|
let ctx = Arc::new(ctx);
|
||||||
let events_spawn = async_std::task::spawn(async move {
|
let ctx1 = ctx.clone();
|
||||||
while let Some(event) = events.recv().await {
|
let r1 = running.clone();
|
||||||
cb(event.typ);
|
let t1 = thread::spawn(move || {
|
||||||
|
while *r1.read().unwrap() {
|
||||||
|
perform_inbox_jobs(&ctx1);
|
||||||
|
if *r1.read().unwrap() {
|
||||||
|
perform_inbox_fetch(&ctx1);
|
||||||
|
|
||||||
|
if *r1.read().unwrap() {
|
||||||
|
perform_inbox_idle(&ctx1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
log::info!("configuring");
|
let ctx1 = ctx.clone();
|
||||||
|
let r1 = running.clone();
|
||||||
|
let t2 = thread::spawn(move || {
|
||||||
|
while *r1.read().unwrap() {
|
||||||
|
perform_smtp_jobs(&ctx1);
|
||||||
|
if *r1.read().unwrap() {
|
||||||
|
perform_smtp_idle(&ctx1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
println!("configuring");
|
||||||
let args = std::env::args().collect::<Vec<String>>();
|
let args = std::env::args().collect::<Vec<String>>();
|
||||||
assert_eq!(args.len(), 3, "requires email password");
|
assert_eq!(args.len(), 2, "missing password");
|
||||||
let email = args[1].clone();
|
let pw = args[1].clone();
|
||||||
let pw = args[2].clone();
|
ctx.set_config(config::Config::Addr, Some("d@testrun.org"))
|
||||||
ctx.set_config(config::Config::Addr, Some(&email))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
ctx.set_config(config::Config::MailPw, Some(&pw))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
ctx.set_config(config::Config::MailPw, Some(&pw)).unwrap();
|
||||||
|
ctx.configure();
|
||||||
|
|
||||||
ctx.configure().await.unwrap();
|
thread::sleep(duration);
|
||||||
|
|
||||||
log::info!("------ RUN ------");
|
println!("sending a message");
|
||||||
ctx.start_io().await;
|
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com").unwrap();
|
||||||
log::info!("--- SENDING A MESSAGE ---");
|
let chat_id = chat::create_by_contact_id(&ctx, contact_id).unwrap();
|
||||||
|
chat::send_text_msg(&ctx, chat_id, "Hi, here is my first message!".into()).unwrap();
|
||||||
|
|
||||||
let contact_id = Contact::create(&ctx, "dignifiedquire", "dignifiedquire@gmail.com")
|
println!("fetching chats..");
|
||||||
.await
|
let chats = Chatlist::try_load(&ctx, 0, None, None).unwrap();
|
||||||
.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 {}nth message!", i))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// wait for the message to be sent out
|
|
||||||
async_std::task::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() {
|
for i in 0..chats.len() {
|
||||||
let msg = Message::load_from_db(&ctx, chats.get_msg_id(i).unwrap().unwrap())
|
let summary = chats.get_summary(&ctx, 0, None);
|
||||||
.await
|
let text1 = summary.get_text1();
|
||||||
.unwrap();
|
let text2 = summary.get_text2();
|
||||||
log::info!("[{}] msg: {:?}", i, msg);
|
println!("chat: {} - {:?} - {:?}", i, text1, text2,);
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("stopping");
|
thread::sleep(duration);
|
||||||
ctx.stop_io().await;
|
|
||||||
log::info!("closing");
|
println!("stopping threads");
|
||||||
drop(ctx);
|
|
||||||
events_spawn.await;
|
*running.write().unwrap() = false;
|
||||||
|
deltachat::job::interrupt_inbox_idle(&ctx);
|
||||||
|
deltachat::job::interrupt_smtp_idle(&ctx);
|
||||||
|
|
||||||
|
println!("joining");
|
||||||
|
t1.join().unwrap();
|
||||||
|
t2.join().unwrap();
|
||||||
|
|
||||||
|
println!("closing");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,5 +7,3 @@
|
|||||||
cc c310754465ee0261807b96fa9bcc4861ff9aa286e94667524b5960c69f9b6620 # shrinks to buf = "", approx_chars = 0, do_unwrap = false
|
cc c310754465ee0261807b96fa9bcc4861ff9aa286e94667524b5960c69f9b6620 # shrinks to buf = "", approx_chars = 0, do_unwrap = false
|
||||||
cc 5fd8d730b0a9cdf7308ce58818ca9aefc0255c9ba2a0878944fc48d43a67315b # shrinks to buf = "𑒀ὐ¢🜀\u{1e01b}A a🟠", approx_chars = 0, do_unwrap = false
|
cc 5fd8d730b0a9cdf7308ce58818ca9aefc0255c9ba2a0878944fc48d43a67315b # shrinks to buf = "𑒀ὐ¢🜀\u{1e01b}A a🟠", approx_chars = 0, do_unwrap = false
|
||||||
cc c6a0029a54137a4b9efc9ef2ea6d9a7dd1d60d1c937bb472b66a174618ba8013 # shrinks to buf = "𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ ", approx_chars = 0, do_unwrap = false
|
cc c6a0029a54137a4b9efc9ef2ea6d9a7dd1d60d1c937bb472b66a174618ba8013 # shrinks to buf = "𐠈0Aᝮa𫝀®!ꫛa¡0A𐢧00𐹠®A 丽ⷐએ ", approx_chars = 0, do_unwrap = false
|
||||||
cc 9796807baeda701227dcdcfc9fdaa93ddd556da2bb1630381bfe2e037bee73f6 # shrinks to buf = " ꫛ®a\u{11300}a", approx_chars = 0
|
|
||||||
cc 063a4c42ac1ec9aa37af54521b210ba9cd82dcc9cc3be296ca2fedf8240072d4 # shrinks to buf = "a᪠ 0A", approx_chars = 0
|
|
||||||
|
|||||||
@@ -1,39 +1,3 @@
|
|||||||
1.51.0
|
|
||||||
------
|
|
||||||
|
|
||||||
- adapt python bindings and APIs to core51 release
|
|
||||||
(see CHANGELOG of https://github.com/deltachat/deltachat-core-rust/blob/1.51.0/CHANGELOG.md#1510
|
|
||||||
for more details on all core changes)
|
|
||||||
|
|
||||||
1.44.0
|
|
||||||
------
|
|
||||||
|
|
||||||
- fix Chat.get_mute_duration()
|
|
||||||
|
|
||||||
1.40.1
|
|
||||||
---------------
|
|
||||||
|
|
||||||
- emit "ac_member_removed" event (with 'actor' being the removed contact)
|
|
||||||
for when a user leaves a group.
|
|
||||||
|
|
||||||
- fix create_contact(addr) when addr is the self-contact.
|
|
||||||
|
|
||||||
|
|
||||||
1.40.0
|
|
||||||
---------------
|
|
||||||
|
|
||||||
- uses latest 1.40+ Delta Chat core
|
|
||||||
|
|
||||||
- refactored internals to use plugin-approach
|
|
||||||
|
|
||||||
- introduced PerAccount and Global hooks that plugins can implement
|
|
||||||
|
|
||||||
- introduced `ac_member_added()` and `ac_member_removed()` plugin events.
|
|
||||||
|
|
||||||
- introduced two documented examples for an echo and a group-membership
|
|
||||||
tracking plugin.
|
|
||||||
|
|
||||||
|
|
||||||
0.800.0
|
0.800.0
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
|||||||
@@ -7,14 +7,76 @@ which implements IMAP/SMTP/MIME/PGP e-mail standards and offers
|
|||||||
a low-level Chat/Contact/Message API to user interfaces and bots.
|
a low-level Chat/Contact/Message API to user interfaces and bots.
|
||||||
|
|
||||||
|
|
||||||
|
Installing bindings from source (Updated: 20-Jan-2020)
|
||||||
|
=========================================================
|
||||||
|
|
||||||
|
Install Rust and Cargo first. Deltachat needs a specific nightly
|
||||||
|
version, the easiest is probably to first install Rust stable from
|
||||||
|
rustup and then use this to install the correct nightly version.
|
||||||
|
|
||||||
|
Bootstrap Rust and Cargo by using rustup::
|
||||||
|
|
||||||
|
curl https://sh.rustup.rs -sSf | sh
|
||||||
|
|
||||||
|
Then GIT clone the deltachat-core-rust repo and get the actual
|
||||||
|
rust- and cargo-toolchain needed by deltachat::
|
||||||
|
|
||||||
|
git clone https://github.com/deltachat/deltachat-core-rust
|
||||||
|
cd deltachat-core-rust
|
||||||
|
rustup show
|
||||||
|
|
||||||
|
To install the Delta Chat Python bindings make sure you have Python3 installed.
|
||||||
|
E.g. on Debian-based systems `apt install python3 python3-pip
|
||||||
|
python3-venv` should give you a usable python installation.
|
||||||
|
|
||||||
|
Ensure you are in the deltachat-core-rust/python directory, create the
|
||||||
|
virtual environment and activate it in your shell::
|
||||||
|
|
||||||
|
cd python
|
||||||
|
python3 -m venv venv # or: virtualenv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
You should now be able to build the python bindings using the supplied script::
|
||||||
|
|
||||||
|
./install_python_bindings.py
|
||||||
|
|
||||||
|
The installation might take a while, depending on your machine.
|
||||||
|
The bindings will be installed in release mode but with debug symbols.
|
||||||
|
The release mode is currently necessary because some tests generate RSA keys
|
||||||
|
which is prohibitively slow in non-release mode.
|
||||||
|
|
||||||
|
After successful binding installation you can install a few more
|
||||||
|
Python packages before running the tests::
|
||||||
|
|
||||||
|
python -m pip install pytest pytest-timeout pytest-rerunfailures requests
|
||||||
|
pytest -v tests
|
||||||
|
|
||||||
|
|
||||||
|
running "live" tests with temporary accounts
|
||||||
|
---------------------------------------------
|
||||||
|
|
||||||
|
If you want to run "liveconfig" functional tests you can set
|
||||||
|
``DCC_NEW_TMP_EMAIL`` to:
|
||||||
|
|
||||||
|
- a particular https-url that you can ask for from the delta
|
||||||
|
chat devs. This is implemented on the server side via
|
||||||
|
the [mailadm](https://github.com/deltachat/mailadm) command line tool.
|
||||||
|
|
||||||
|
- or the path of a file that contains two lines, each describing
|
||||||
|
via "addr=... mail_pw=..." a test account login that will
|
||||||
|
be used for the live tests.
|
||||||
|
|
||||||
|
With ``DCC_NEW_TMP_EMAIL`` set pytest invocations will use real
|
||||||
|
e-mail accounts and run through all functional "liveconfig" tests.
|
||||||
|
|
||||||
|
|
||||||
Installing pre-built packages (Linux-only)
|
Installing pre-built packages (Linux-only)
|
||||||
========================================================
|
========================================================
|
||||||
|
|
||||||
If you have a Linux system you may try to install the ``deltachat`` binary "wheel" packages
|
If you have a Linux system you may try to install the ``deltachat`` binary "wheel" packages
|
||||||
without any "build-from-source" steps.
|
without any "build-from-source" steps.
|
||||||
Otherwise you need to `compile the Delta Chat bindings yourself <#sourceinstall>`_.
|
|
||||||
|
|
||||||
We recommend to first `install virtualenv <https://virtualenv.pypa.io/en/stable/installation.html>`_,
|
We suggest to `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
|
||||||
then create a fresh Python virtual environment and activate it in your shell::
|
then create a fresh Python virtual environment and activate it in your shell::
|
||||||
|
|
||||||
virtualenv venv # or: python -m venv
|
virtualenv venv # or: python -m venv
|
||||||
@@ -41,79 +103,6 @@ To verify it worked::
|
|||||||
`in contact with us <https://delta.chat/en/contribute>`_.
|
`in contact with us <https://delta.chat/en/contribute>`_.
|
||||||
|
|
||||||
|
|
||||||
Running tests
|
|
||||||
=============
|
|
||||||
|
|
||||||
After successful binding installation you can install a few more
|
|
||||||
Python packages before running the tests::
|
|
||||||
|
|
||||||
python -m pip install pytest pytest-xdist pytest-timeout pytest-rerunfailures requests
|
|
||||||
pytest -v tests
|
|
||||||
|
|
||||||
This will run all "offline" tests and skip all functional
|
|
||||||
end-to-end tests that require accounts on real e-mail servers.
|
|
||||||
|
|
||||||
.. _livetests:
|
|
||||||
|
|
||||||
running "live" tests with temporary accounts
|
|
||||||
---------------------------------------------
|
|
||||||
|
|
||||||
If you want to run live functional tests you can set ``DCC_NEW_TMP_EMAIL`` to a URL that creates e-mail accounts. Most developers use https://testrun.org URLS created and managed by [mailadm](https://mailadm.readthedocs.io/en/latest/).
|
|
||||||
|
|
||||||
Please feel free to contact us through a github issue or by e-mail and we'll send you a URL that you can then use for functional tests like this:
|
|
||||||
|
|
||||||
export DCC_NEW_TMP_EMAIL=<URL you got from us>
|
|
||||||
|
|
||||||
With this account-creation setting, pytest runs create ephemeral e-mail accounts on the http://testrun.org server. These accounts exists only for one hour and then are removed completely.
|
|
||||||
One hour is enough to invoke pytest and run all offline and online tests:
|
|
||||||
|
|
||||||
pytest
|
|
||||||
|
|
||||||
# or if you have installed pytest-xdist for parallel test execution
|
|
||||||
pytest -n6
|
|
||||||
|
|
||||||
Each test run creates new accounts.
|
|
||||||
|
|
||||||
|
|
||||||
.. _sourceinstall:
|
|
||||||
|
|
||||||
Installing bindings from source (Updated: July 2020)
|
|
||||||
=========================================================
|
|
||||||
|
|
||||||
Install Rust and Cargo first.
|
|
||||||
The easiest is probably to use `rustup <https://rustup.rs/>`_.
|
|
||||||
|
|
||||||
Bootstrap Rust and Cargo by using rustup::
|
|
||||||
|
|
||||||
curl https://sh.rustup.rs -sSf | sh
|
|
||||||
|
|
||||||
Then clone the deltachat-core-rust repo::
|
|
||||||
|
|
||||||
git clone https://github.com/deltachat/deltachat-core-rust
|
|
||||||
cd deltachat-core-rust
|
|
||||||
|
|
||||||
To install the Delta Chat Python bindings make sure you have Python3 installed.
|
|
||||||
E.g. on Debian-based systems `apt install python3 python3-pip
|
|
||||||
python3-venv` should give you a usable python installation.
|
|
||||||
|
|
||||||
Ensure you are in the deltachat-core-rust/python directory, create the
|
|
||||||
virtual environment and activate it in your shell::
|
|
||||||
|
|
||||||
cd python
|
|
||||||
python3 -m venv venv # or: virtualenv venv
|
|
||||||
source venv/bin/activate
|
|
||||||
|
|
||||||
You should now be able to build the python bindings using the supplied script::
|
|
||||||
|
|
||||||
python install_python_bindings.py
|
|
||||||
|
|
||||||
The core compilation and bindings building might take a while,
|
|
||||||
depending on the speed of your machine.
|
|
||||||
The bindings will be installed in release mode but with debug symbols.
|
|
||||||
The release mode is currently necessary because some tests generate RSA keys
|
|
||||||
which is prohibitively slow in non-release mode.
|
|
||||||
|
|
||||||
|
|
||||||
Code examples
|
Code examples
|
||||||
=============
|
=============
|
||||||
|
|
||||||
@@ -124,10 +113,10 @@ You may look at `examples <https://py.delta.chat/examples.html>`_.
|
|||||||
.. _`deltachat-core`: https://github.com/deltachat/deltachat-core-rust
|
.. _`deltachat-core`: https://github.com/deltachat/deltachat-core-rust
|
||||||
|
|
||||||
|
|
||||||
Building manylinux based wheels
|
Building manylinux1 based wheels
|
||||||
====================================
|
================================
|
||||||
|
|
||||||
Building portable manylinux wheels which come with libdeltachat.so
|
Building portable manylinux1 wheels which come with libdeltachat.so
|
||||||
can be done with docker-tooling.
|
can be done with docker-tooling.
|
||||||
|
|
||||||
using docker pull / premade images
|
using docker pull / premade images
|
||||||
@@ -143,7 +132,7 @@ This docker image can be used to run tests and build Python wheels for all inter
|
|||||||
|
|
||||||
$ docker run -e DCC_NEW_TMP_EMAIL \
|
$ docker run -e DCC_NEW_TMP_EMAIL \
|
||||||
--rm -it -v \$(pwd):/mnt -w /mnt \
|
--rm -it -v \$(pwd):/mnt -w /mnt \
|
||||||
deltachat/coredeps scripts/run_all.sh
|
deltachat/coredeps ci_scripts/run_all.sh
|
||||||
|
|
||||||
|
|
||||||
Optionally build your own docker image
|
Optionally build your own docker image
|
||||||
@@ -152,9 +141,9 @@ Optionally build your own docker image
|
|||||||
If you want to build your own custom docker image you can do this::
|
If you want to build your own custom docker image you can do this::
|
||||||
|
|
||||||
$ cd deltachat-core # cd to deltachat-core checkout directory
|
$ cd deltachat-core # cd to deltachat-core checkout directory
|
||||||
$ docker build -t deltachat/coredeps scripts/docker_coredeps
|
$ docker build -t deltachat/coredeps ci_scripts/docker_coredeps
|
||||||
|
|
||||||
This will use the ``scripts/docker_coredeps/Dockerfile`` to build
|
This will use the ``ci_scripts/docker_coredeps/Dockerfile`` to build
|
||||||
up docker image called ``deltachat/coredeps``. You can afterwards
|
up docker image called ``deltachat/coredeps``. You can afterwards
|
||||||
find it with::
|
find it with::
|
||||||
|
|
||||||
|
|||||||
3
python/doc/_templates/globaltoc.html
vendored
@@ -9,7 +9,8 @@
|
|||||||
</ul>
|
</ul>
|
||||||
<b>external links:</b>
|
<b>external links:</b>
|
||||||
<ul>
|
<ul>
|
||||||
<li><a href="https://github.com/deltachat/deltachat-core-rust">github repository</a></li>
|
<li><a href="https://github.com/deltachat/deltachat-core">github repository</a></li>
|
||||||
|
<!-- <li><a href="https://lists.codespeak.net/postorius/lists/muacrypt.lists.codespeak.net">Mailing list</></li> <-->
|
||||||
<li><a href="https://pypi.python.org/pypi/deltachat">pypi: deltachat</a></li>
|
<li><a href="https://pypi.python.org/pypi/deltachat">pypi: deltachat</a></li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
high level API reference
|
high level API reference
|
||||||
========================
|
========================
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
This API is work in progress and may change in versions prior to 1.0.
|
||||||
|
|
||||||
- :class:`deltachat.account.Account` (your main entry point, creates the
|
- :class:`deltachat.account.Account` (your main entry point, creates the
|
||||||
other classes)
|
other classes)
|
||||||
- :class:`deltachat.contact.Contact`
|
- :class:`deltachat.contact.Contact`
|
||||||
|
|||||||
7
python/doc/capi.rst
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
|
||||||
|
C deltachat interface
|
||||||
|
=====================
|
||||||
|
|
||||||
|
See :doc:`lapi` for accessing many of the below functions
|
||||||
|
through the ``deltachat.capi.lib`` namespace.
|
||||||
|
|
||||||
@@ -1,60 +1,37 @@
|
|||||||
|
|
||||||
|
|
||||||
examples
|
examples
|
||||||
========
|
========
|
||||||
|
|
||||||
|
|
||||||
|
Playing around on the commandline
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
Once you have :doc:`installed deltachat bindings <install>`
|
Once you have :doc:`installed deltachat bindings <install>`
|
||||||
you need email/password credentials for an IMAP/SMTP account.
|
you can start playing from the python interpreter commandline.
|
||||||
Delta Chat developers and the CI system use a special URL to create
|
For example you can type ``python`` and then::
|
||||||
temporary e-mail accounts on [testrun.org](https://testrun.org) for testing.
|
|
||||||
|
|
||||||
Receiving a Chat message from the command line
|
# instantiate and configure deltachat account
|
||||||
----------------------------------------------
|
import deltachat
|
||||||
|
ac = deltachat.Account("/tmp/db")
|
||||||
|
|
||||||
Here is a simple bot that:
|
# start configuration activity and smtp/imap threads
|
||||||
|
ac.start_threads()
|
||||||
|
ac.configure(addr="test2@hq5.merlinux.eu", mail_pw="********")
|
||||||
|
|
||||||
- receives a message and sends back ("echoes") a message
|
# create a contact and send a message
|
||||||
|
contact = ac.create_contact("someother@email.address")
|
||||||
|
chat = ac.create_chat_by_contact(contact)
|
||||||
|
chat.send_text("hi from the python interpreter command line")
|
||||||
|
|
||||||
- terminates the bot if the message `/quit` is sent
|
Checkout our :doc:`api` for the various high-level things you can do
|
||||||
|
to send/receive messages, create contacts and chats.
|
||||||
|
|
||||||
.. include:: ../examples/echo_and_quit.py
|
|
||||||
:literal:
|
|
||||||
|
|
||||||
With this file in your working directory you can run the bot
|
Looking at a real example
|
||||||
by specifying a database path, an e-mail address and password of
|
|
||||||
a SMTP-IMAP account::
|
|
||||||
|
|
||||||
$ cd examples
|
|
||||||
$ python echo_and_quit.py /tmp/db --email ADDRESS --password PASSWORD
|
|
||||||
|
|
||||||
While this process is running you can start sending chat messages
|
|
||||||
to `ADDRESS`.
|
|
||||||
|
|
||||||
Track member additions and removals in a group
|
|
||||||
----------------------------------------------
|
|
||||||
|
|
||||||
Here is a simple bot that:
|
|
||||||
|
|
||||||
- echoes messages sent to it
|
|
||||||
|
|
||||||
- tracks if configuration completed
|
|
||||||
|
|
||||||
- tracks member additions and removals for all chat groups
|
|
||||||
|
|
||||||
.. include:: ../examples/group_tracking.py
|
|
||||||
:literal:
|
|
||||||
|
|
||||||
With this file in your working directory you can run the bot
|
|
||||||
by specifying a database path, an e-mail address and password of
|
|
||||||
a SMTP-IMAP account::
|
|
||||||
|
|
||||||
python group_tracking.py --email ADDRESS --password PASSWORD /tmp/db
|
|
||||||
|
|
||||||
When this process is running you can start sending chat messages
|
|
||||||
to `ADDRESS`.
|
|
||||||
|
|
||||||
Writing bots for real
|
|
||||||
-------------------------
|
-------------------------
|
||||||
|
|
||||||
The `deltabot repository <https://github.com/deltachat/deltabot#deltachat-example-bot>`_
|
The `deltabot repository <https://github.com/deltachat/deltabot#deltachat-example-bot>`_
|
||||||
contains a little framework for writing deltachat bots in Python.
|
contains a real-life example of Python bindings usage.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ deltachat python bindings
|
|||||||
The ``deltachat`` Python package provides two layers of bindings for the
|
The ``deltachat`` Python package provides two layers of bindings for the
|
||||||
core Rust-library of the https://delta.chat messaging ecosystem:
|
core Rust-library of the https://delta.chat messaging ecosystem:
|
||||||
|
|
||||||
- :doc:`api` is a high level interface to deltachat-core.
|
- :doc:`api` is a high level interface to deltachat-core which aims
|
||||||
|
to be memory safe and thoroughly tested through continous tox/pytest runs.
|
||||||
- :doc:`plugins` is a brief introduction into implementing plugin hooks.
|
|
||||||
|
|
||||||
- :doc:`lapi` is a lowlevel CFFI-binding to the `Rust Core
|
- :doc:`lapi` is a lowlevel CFFI-binding to the `Rust Core
|
||||||
<https://github.com/deltachat/deltachat-core-rust>`_.
|
<https://github.com/deltachat/deltachat-core-rust>`_.
|
||||||
@@ -29,7 +28,6 @@ getting started
|
|||||||
changelog
|
changelog
|
||||||
api
|
api
|
||||||
lapi
|
lapi
|
||||||
plugins
|
|
||||||
|
|
||||||
..
|
..
|
||||||
Indices and tables
|
Indices and tables
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
|
|
||||||
Implementing Plugin Hooks
|
|
||||||
==========================
|
|
||||||
|
|
||||||
The Delta Chat Python bindings use `pluggy <https://pluggy.readthedocs.io>`_
|
|
||||||
for managing global and per-account plugin registration, and performing
|
|
||||||
hook calls. There are two kinds of plugins:
|
|
||||||
|
|
||||||
- Global plugins that are active for all accounts; they can implement
|
|
||||||
hooks at account-creation and account-shutdown time.
|
|
||||||
|
|
||||||
- Account plugins that are only active during the lifetime of a
|
|
||||||
single Account instance.
|
|
||||||
|
|
||||||
|
|
||||||
Registering a plugin
|
|
||||||
--------------------
|
|
||||||
|
|
||||||
.. autofunction:: deltachat.register_global_plugin
|
|
||||||
:noindex:
|
|
||||||
|
|
||||||
.. automethod:: deltachat.account.Account.add_account_plugin
|
|
||||||
:noindex:
|
|
||||||
|
|
||||||
|
|
||||||
Per-Account Hook specifications
|
|
||||||
-------------------------------
|
|
||||||
|
|
||||||
.. autoclass:: deltachat.hookspec.PerAccount
|
|
||||||
:members:
|
|
||||||
|
|
||||||
|
|
||||||
Global Hook specifications
|
|
||||||
--------------------------
|
|
||||||
|
|
||||||
.. autoclass:: deltachat.hookspec.Global
|
|
||||||
:members:
|
|
||||||
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
|
|
||||||
# content of echo_and_quit.py
|
|
||||||
|
|
||||||
from deltachat import account_hookimpl, run_cmdline
|
|
||||||
|
|
||||||
|
|
||||||
class EchoPlugin:
|
|
||||||
@account_hookimpl
|
|
||||||
def ac_incoming_message(self, message):
|
|
||||||
print("process_incoming message", message)
|
|
||||||
if message.text.strip() == "/quit":
|
|
||||||
message.account.shutdown()
|
|
||||||
else:
|
|
||||||
# unconditionally accept the chat
|
|
||||||
message.create_chat()
|
|
||||||
addr = message.get_sender_contact().addr
|
|
||||||
if message.is_system_message():
|
|
||||||
message.chat.send_text("echoing system message from {}:\n{}".format(addr, message))
|
|
||||||
else:
|
|
||||||
text = message.text
|
|
||||||
message.chat.send_text("echoing from {}:\n{}".format(addr, text))
|
|
||||||
|
|
||||||
@account_hookimpl
|
|
||||||
def ac_message_delivered(self, message):
|
|
||||||
print("ac_message_delivered", message)
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
run_cmdline(argv=argv, account_plugins=[EchoPlugin()])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
|
|
||||||
# content of group_tracking.py
|
|
||||||
|
|
||||||
from deltachat import account_hookimpl, run_cmdline
|
|
||||||
|
|
||||||
|
|
||||||
class GroupTrackingPlugin:
|
|
||||||
@account_hookimpl
|
|
||||||
def ac_incoming_message(self, message):
|
|
||||||
print("process_incoming message", message)
|
|
||||||
if message.text.strip() == "/quit":
|
|
||||||
message.account.shutdown()
|
|
||||||
else:
|
|
||||||
# unconditionally accept the chat
|
|
||||||
message.create_chat()
|
|
||||||
addr = message.get_sender_contact().addr
|
|
||||||
text = message.text
|
|
||||||
message.chat.send_text("echoing from {}:\n{}".format(addr, text))
|
|
||||||
|
|
||||||
@account_hookimpl
|
|
||||||
def ac_outgoing_message(self, message):
|
|
||||||
print("ac_outgoing_message:", message)
|
|
||||||
|
|
||||||
@account_hookimpl
|
|
||||||
def ac_configure_completed(self, success):
|
|
||||||
print("ac_configure_completed:", success)
|
|
||||||
|
|
||||||
@account_hookimpl
|
|
||||||
def ac_chat_modified(self, chat):
|
|
||||||
print("ac_chat_modified:", chat.id, chat.get_name())
|
|
||||||
for member in chat.get_contacts():
|
|
||||||
print("chat member: {}".format(member.addr))
|
|
||||||
|
|
||||||
@account_hookimpl
|
|
||||||
def ac_member_added(self, chat, contact, actor, message):
|
|
||||||
print("ac_member_added {} to chat {} from {}".format(
|
|
||||||
contact.addr, chat.id, actor or message.get_sender_contact().addr))
|
|
||||||
for member in chat.get_contacts():
|
|
||||||
print("chat member: {}".format(member.addr))
|
|
||||||
|
|
||||||
@account_hookimpl
|
|
||||||
def ac_member_removed(self, chat, contact, actor, message):
|
|
||||||
print("ac_member_removed {} from chat {} by {}".format(
|
|
||||||
contact.addr, chat.id, actor or message.get_sender_contact().addr))
|
|
||||||
|
|
||||||
|
|
||||||
def main(argv=None):
|
|
||||||
run_cmdline(argv=argv, account_plugins=[GroupTrackingPlugin()])
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
|
|
||||||
import pytest
|
|
||||||
import py
|
|
||||||
import echo_and_quit
|
|
||||||
import group_tracking
|
|
||||||
from deltachat.events import FFIEventLogger
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(scope='session')
|
|
||||||
def datadir():
|
|
||||||
"""The py.path.local object of the test-data/ directory."""
|
|
||||||
for path in reversed(py.path.local(__file__).parts()):
|
|
||||||
datadir = path.join('test-data')
|
|
||||||
if datadir.isdir():
|
|
||||||
return datadir
|
|
||||||
else:
|
|
||||||
pytest.skip('test-data directory not found')
|
|
||||||
|
|
||||||
|
|
||||||
def test_echo_quit_plugin(acfactory, lp):
|
|
||||||
lp.sec("creating one echo_and_quit bot")
|
|
||||||
botproc = acfactory.run_bot_process(echo_and_quit)
|
|
||||||
|
|
||||||
lp.sec("creating a temp account to contact the bot")
|
|
||||||
ac1 = acfactory.get_one_online_account()
|
|
||||||
|
|
||||||
lp.sec("sending a message to the bot")
|
|
||||||
bot_contact = ac1.create_contact(botproc.addr)
|
|
||||||
bot_chat = bot_contact.create_chat()
|
|
||||||
bot_chat.send_text("hello")
|
|
||||||
|
|
||||||
lp.sec("waiting for the reply message from the bot to arrive")
|
|
||||||
reply = ac1._evtracker.wait_next_incoming_message()
|
|
||||||
assert reply.chat == bot_chat
|
|
||||||
assert "hello" in reply.text
|
|
||||||
lp.sec("send quit sequence")
|
|
||||||
bot_chat.send_text("/quit")
|
|
||||||
botproc.wait()
|
|
||||||
|
|
||||||
|
|
||||||
def test_group_tracking_plugin(acfactory, lp):
|
|
||||||
lp.sec("creating one group-tracking bot and two temp accounts")
|
|
||||||
botproc = acfactory.run_bot_process(group_tracking, ffi=False)
|
|
||||||
|
|
||||||
ac1, ac2 = acfactory.get_two_online_accounts(quiet=True)
|
|
||||||
|
|
||||||
botproc.fnmatch_lines("""
|
|
||||||
*ac_configure_completed*
|
|
||||||
""")
|
|
||||||
ac1.add_account_plugin(FFIEventLogger(ac1))
|
|
||||||
ac2.add_account_plugin(FFIEventLogger(ac2))
|
|
||||||
|
|
||||||
lp.sec("creating bot test group with bot")
|
|
||||||
bot_contact = ac1.create_contact(botproc.addr)
|
|
||||||
ch = ac1.create_group_chat("bot test group")
|
|
||||||
ch.add_contact(bot_contact)
|
|
||||||
ch.send_text("hello")
|
|
||||||
|
|
||||||
botproc.fnmatch_lines("""
|
|
||||||
*ac_chat_modified*bot test group*
|
|
||||||
""")
|
|
||||||
|
|
||||||
lp.sec("adding third member {}".format(ac2.get_config("addr")))
|
|
||||||
contact3 = ac1.create_contact(ac2.get_config("addr"))
|
|
||||||
ch.add_contact(contact3)
|
|
||||||
|
|
||||||
reply = ac1._evtracker.wait_next_incoming_message()
|
|
||||||
assert "hello" in reply.text
|
|
||||||
|
|
||||||
lp.sec("now looking at what the bot received")
|
|
||||||
botproc.fnmatch_lines("""
|
|
||||||
*ac_member_added {}*from*{}*
|
|
||||||
""".format(contact3.addr, ac1.get_config("addr")))
|
|
||||||
|
|
||||||
lp.sec("contact successfully added, now removing")
|
|
||||||
ch.remove_contact(contact3)
|
|
||||||
botproc.fnmatch_lines("""
|
|
||||||
*ac_member_removed {}*from*{}*
|
|
||||||
""".format(contact3.addr, ac1.get_config("addr")))
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
from __future__ import print_function
|
|
||||||
from deltachat import capi
|
|
||||||
from deltachat.capi import ffi, lib
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
ctx = capi.lib.dc_context_new(ffi.NULL, ffi.NULL)
|
|
||||||
lib.dc_stop_io(ctx)
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
#!/usr/bin/env python3
|
#!/usr/bin/env python
|
||||||
|
|
||||||
"""
|
"""
|
||||||
setup a python binding development in-place install with cargo debug symbols.
|
setup a python binding development in-place install with cargo debug symbols.
|
||||||
@@ -17,14 +17,10 @@ if __name__ == "__main__":
|
|||||||
os.environ["DCC_RS_DEV"] = dn
|
os.environ["DCC_RS_DEV"] = dn
|
||||||
|
|
||||||
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
|
cmd = ["cargo", "build", "-p", "deltachat_ffi"]
|
||||||
|
|
||||||
if target == 'release':
|
if target == 'release':
|
||||||
os.environ["CARGO_PROFILE_RELEASE_LTO"] = "on"
|
|
||||||
cmd.append("--release")
|
cmd.append("--release")
|
||||||
|
|
||||||
print("running:", " ".join(cmd))
|
|
||||||
subprocess.check_call(cmd)
|
subprocess.check_call(cmd)
|
||||||
subprocess.check_call("rm -rf build/ src/deltachat/*.so src/deltachat/*.dylib src/deltachat/*.dll" , shell=True)
|
subprocess.check_call("rm -rf build/ src/deltachat/*.so" , shell=True)
|
||||||
|
|
||||||
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":
|
if len(sys.argv) <= 1 or sys.argv[1] != "onlybuild":
|
||||||
subprocess.check_call([
|
subprocess.check_call([
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
[mypy]
|
|
||||||
|
|
||||||
[mypy-deltachat.capi.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
|
|
||||||
[mypy-pluggy.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
|
|
||||||
[mypy-cffi.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
|
|
||||||
[mypy-imapclient.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
|
|
||||||
[mypy-pytest.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
|
|
||||||
[mypy-_pytest.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
|
|
||||||
[mypy-imap_tools.*]
|
|
||||||
ignore_missing_imports = True
|
|
||||||
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[build-system]
|
|
||||||
requires = ["setuptools>=45", "wheel", "setuptools_scm>=6.2", "cffi>=1.0.0", "pkgconfig"]
|
|
||||||
build-backend = "setuptools.build_meta"
|
|
||||||
|
|
||||||
[tool.setuptools_scm]
|
|
||||||
root = ".."
|
|
||||||
tag_regex = '^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
|
|
||||||
git_describe_command = "git describe --dirty --tags --long --match py-*.*"
|
|
||||||
@@ -8,22 +8,20 @@ def main():
|
|||||||
long_description = f.read()
|
long_description = f.read()
|
||||||
setuptools.setup(
|
setuptools.setup(
|
||||||
name='deltachat',
|
name='deltachat',
|
||||||
|
setup_requires=['setuptools_scm', 'cffi>=1.0.0'],
|
||||||
|
use_scm_version = {
|
||||||
|
"root": "..",
|
||||||
|
"relative_to": __file__,
|
||||||
|
'tag_regex': r'^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$',
|
||||||
|
'git_describe_command': "git describe --dirty --tags --long --match py-*.*",
|
||||||
|
},
|
||||||
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
|
description='Python bindings for the Delta Chat Core library using CFFI against the Rust-implemented libdeltachat',
|
||||||
long_description=long_description,
|
long_description=long_description,
|
||||||
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
||||||
install_requires=['cffi>=1.0.0', 'pluggy', 'imap-tools', 'requests'],
|
install_requires=['cffi>=1.0.0', 'pluggy'],
|
||||||
setup_requires=[
|
|
||||||
'setuptools_scm', # required for compatibility with `python3 setup.py sdist`
|
|
||||||
'pkgconfig',
|
|
||||||
],
|
|
||||||
packages=setuptools.find_packages('src'),
|
packages=setuptools.find_packages('src'),
|
||||||
package_dir={'': 'src'},
|
package_dir={'': 'src'},
|
||||||
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
||||||
entry_points = {
|
|
||||||
'pytest11': [
|
|
||||||
'deltachat.testplugin = deltachat.testplugin',
|
|
||||||
],
|
|
||||||
},
|
|
||||||
classifiers=[
|
classifiers=[
|
||||||
'Development Status :: 4 - Beta',
|
'Development Status :: 4 - Beta',
|
||||||
'Intended Audience :: Developers',
|
'Intended Audience :: Developers',
|
||||||
|
|||||||
@@ -1,13 +1,6 @@
|
|||||||
import sys
|
from deltachat import capi, const
|
||||||
|
from deltachat.capi import ffi
|
||||||
from . import capi, const, hookspec # noqa
|
from deltachat.account import Account # noqa
|
||||||
from .capi import ffi # noqa
|
|
||||||
from .account import Account # noqa
|
|
||||||
from .message import Message # noqa
|
|
||||||
from .contact import Contact # noqa
|
|
||||||
from .chat import Chat # noqa
|
|
||||||
from .hookspec import account_hookimpl, global_hookimpl # noqa
|
|
||||||
from . import events
|
|
||||||
|
|
||||||
from pkg_resources import get_distribution, DistributionNotFound
|
from pkg_resources import get_distribution, DistributionNotFound
|
||||||
try:
|
try:
|
||||||
@@ -17,72 +10,67 @@ except DistributionNotFound:
|
|||||||
__version__ = "0.0.0.dev0-unknown"
|
__version__ = "0.0.0.dev0-unknown"
|
||||||
|
|
||||||
|
|
||||||
|
_DC_CALLBACK_MAP = {}
|
||||||
|
|
||||||
|
|
||||||
|
@capi.ffi.def_extern()
|
||||||
|
def py_dc_callback(ctx, evt, data1, data2):
|
||||||
|
"""The global event handler.
|
||||||
|
|
||||||
|
CFFI only allows us to set one global event handler, so this one
|
||||||
|
looks up the correct event handler for the given context.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
callback = _DC_CALLBACK_MAP.get(ctx, lambda *a: 0)
|
||||||
|
except AttributeError:
|
||||||
|
# we are in a deep in GC-free/interpreter shutdown land
|
||||||
|
# nothing much better to do here than:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# the following code relates to the deltachat/_build.py's helper
|
||||||
|
# function which provides us signature info of an event call
|
||||||
|
evt_name = get_dc_event_name(evt)
|
||||||
|
event_sig_types = capi.lib.dc_get_event_signature_types(evt)
|
||||||
|
if data1 and event_sig_types & 1:
|
||||||
|
data1 = ffi.string(ffi.cast('char*', data1)).decode("utf8")
|
||||||
|
if data2 and event_sig_types & 2:
|
||||||
|
data2 = ffi.string(ffi.cast('char*', data2)).decode("utf8")
|
||||||
|
try:
|
||||||
|
if isinstance(data2, bytes):
|
||||||
|
data2 = data2.decode("utf8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# XXX ignoring the decode error is not quite correct but for now
|
||||||
|
# i don't want to hunt down encoding problems in the c lib
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
ret = callback(ctx, evt_name, data1, data2)
|
||||||
|
if ret is None:
|
||||||
|
ret = 0
|
||||||
|
assert isinstance(ret, int), repr(ret)
|
||||||
|
if event_sig_types & 4:
|
||||||
|
return ffi.cast('uintptr_t', ret)
|
||||||
|
elif event_sig_types & 8:
|
||||||
|
return ffi.cast('int', ret)
|
||||||
|
except: # noqa
|
||||||
|
raise
|
||||||
|
ret = 0
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
def set_context_callback(dc_context, func):
|
||||||
|
_DC_CALLBACK_MAP[dc_context] = func
|
||||||
|
|
||||||
|
|
||||||
|
def clear_context_callback(dc_context):
|
||||||
|
try:
|
||||||
|
_DC_CALLBACK_MAP.pop(dc_context, None)
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
||||||
if not _DC_EVENTNAME_MAP:
|
if not _DC_EVENTNAME_MAP:
|
||||||
for name in dir(const):
|
for name, val in vars(const).items():
|
||||||
if name.startswith("DC_EVENT_"):
|
if name.startswith("DC_EVENT_"):
|
||||||
_DC_EVENTNAME_MAP[getattr(const, name)] = name
|
_DC_EVENTNAME_MAP[val] = name
|
||||||
return _DC_EVENTNAME_MAP[integer]
|
return _DC_EVENTNAME_MAP[integer]
|
||||||
|
|
||||||
|
|
||||||
def register_global_plugin(plugin):
|
|
||||||
""" Register a global plugin which implements one or more
|
|
||||||
of the :class:`deltachat.hookspec.Global` hooks.
|
|
||||||
"""
|
|
||||||
gm = hookspec.Global._get_plugin_manager()
|
|
||||||
gm.register(plugin)
|
|
||||||
gm.check_pending()
|
|
||||||
|
|
||||||
|
|
||||||
def unregister_global_plugin(plugin):
|
|
||||||
gm = hookspec.Global._get_plugin_manager()
|
|
||||||
gm.unregister(plugin)
|
|
||||||
|
|
||||||
|
|
||||||
register_global_plugin(events)
|
|
||||||
|
|
||||||
|
|
||||||
def run_cmdline(argv=None, account_plugins=None):
|
|
||||||
""" Run a simple default command line app, registering the specified
|
|
||||||
account plugins. """
|
|
||||||
import argparse
|
|
||||||
if argv is None:
|
|
||||||
argv = sys.argv
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(prog=argv[0] if argv else None)
|
|
||||||
parser.add_argument("db", action="store", help="database file")
|
|
||||||
parser.add_argument("--show-ffi", action="store_true", help="show low level ffi events")
|
|
||||||
parser.add_argument("--email", action="store", help="email address")
|
|
||||||
parser.add_argument("--password", action="store", help="password")
|
|
||||||
|
|
||||||
args = parser.parse_args(argv[1:])
|
|
||||||
|
|
||||||
ac = Account(args.db)
|
|
||||||
|
|
||||||
if args.show_ffi:
|
|
||||||
ac.set_config("displayname", "bot")
|
|
||||||
log = events.FFIEventLogger(ac)
|
|
||||||
ac.add_account_plugin(log)
|
|
||||||
|
|
||||||
for plugin in account_plugins or []:
|
|
||||||
print("adding plugin", plugin)
|
|
||||||
ac.add_account_plugin(plugin)
|
|
||||||
|
|
||||||
if not ac.is_configured():
|
|
||||||
assert args.email and args.password, (
|
|
||||||
"you must specify --email and --password once to configure this database/account"
|
|
||||||
)
|
|
||||||
ac.set_config("addr", args.email)
|
|
||||||
ac.set_config("mail_pw", args.password)
|
|
||||||
ac.set_config("mvbox_move", "0")
|
|
||||||
ac.set_config("sentbox_watch", "0")
|
|
||||||
ac.set_config("bot", "1")
|
|
||||||
configtracker = ac.configure()
|
|
||||||
configtracker.wait_finish()
|
|
||||||
|
|
||||||
# start IO threads and configure if neccessary
|
|
||||||
ac.start_io()
|
|
||||||
|
|
||||||
print("{}: waiting for message".format(ac.get_config("addr")))
|
|
||||||
|
|
||||||
ac.wait_shutdown()
|
|
||||||
|
|||||||
@@ -1,58 +1,79 @@
|
|||||||
import distutils.ccompiler
|
import distutils.ccompiler
|
||||||
import distutils.log
|
import distutils.log
|
||||||
import distutils.sysconfig
|
import distutils.sysconfig
|
||||||
import os
|
|
||||||
import platform
|
|
||||||
import re
|
|
||||||
import shutil
|
|
||||||
import subprocess
|
|
||||||
import tempfile
|
import tempfile
|
||||||
import textwrap
|
import platform
|
||||||
|
import os
|
||||||
import cffi
|
import cffi
|
||||||
import pkgconfig # type: ignore
|
import shutil
|
||||||
|
from os.path import dirname as dn
|
||||||
|
from os.path import abspath
|
||||||
|
|
||||||
|
|
||||||
def local_build_flags(projdir, target):
|
def ffibuilder():
|
||||||
"""Construct build flags for building against a checkout.
|
projdir = os.environ.get('DCC_RS_DEV')
|
||||||
|
if not projdir:
|
||||||
:param projdir: The root directory of the deltachat-core-rust project.
|
p = dn(dn(dn(dn(abspath(__file__)))))
|
||||||
:param target: The rust build target, `debug` or `release`.
|
projdir = os.environ["DCC_RS_DEV"] = p
|
||||||
"""
|
target = os.environ.get('DCC_RS_TARGET', 'release')
|
||||||
flags = {}
|
if projdir:
|
||||||
if platform.system() == 'Darwin':
|
if platform.system() == 'Darwin':
|
||||||
flags['libraries'] = ['resolv', 'dl']
|
libs = ['resolv', 'dl']
|
||||||
flags['extra_link_args'] = [
|
extra_link_args = [
|
||||||
'-framework', 'CoreFoundation',
|
'-framework', 'CoreFoundation',
|
||||||
'-framework', 'CoreServices',
|
'-framework', 'CoreServices',
|
||||||
'-framework', 'Security',
|
'-framework', 'Security',
|
||||||
]
|
]
|
||||||
elif platform.system() == 'Linux':
|
elif platform.system() == 'Linux':
|
||||||
flags['libraries'] = ['rt', 'dl', 'm']
|
libs = ['rt', 'dl', 'm']
|
||||||
flags['extra_link_args'] = []
|
extra_link_args = []
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("Compilation not supported yet on Windows, can you help?")
|
||||||
|
target_dir = os.environ.get("CARGO_TARGET_DIR")
|
||||||
|
if target_dir is None:
|
||||||
|
target_dir = os.path.join(projdir, 'target')
|
||||||
|
objs = [os.path.join(target_dir, target, 'libdeltachat.a')]
|
||||||
|
assert os.path.exists(objs[0]), objs
|
||||||
|
incs = [os.path.join(projdir, 'deltachat-ffi')]
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("Compilation not supported yet on Windows, can you help?")
|
libs = ['deltachat']
|
||||||
target_dir = os.environ.get("CARGO_TARGET_DIR")
|
objs = []
|
||||||
if target_dir is None:
|
incs = []
|
||||||
target_dir = os.path.join(projdir, 'target')
|
extra_link_args = []
|
||||||
flags['extra_objects'] = [os.path.join(target_dir, target, 'libdeltachat.a')]
|
builder = cffi.FFI()
|
||||||
assert os.path.exists(flags['extra_objects'][0]), flags['extra_objects']
|
builder.set_source(
|
||||||
flags['include_dirs'] = [os.path.join(projdir, 'deltachat-ffi')]
|
'deltachat.capi',
|
||||||
return flags
|
"""
|
||||||
|
#include <deltachat.h>
|
||||||
|
const char * dupstring_helper(const char* string)
|
||||||
def system_build_flags():
|
{
|
||||||
"""Construct build flags for building against an installed libdeltachat."""
|
return strdup(string);
|
||||||
return pkgconfig.parse('deltachat')
|
}
|
||||||
|
int dc_get_event_signature_types(int e)
|
||||||
|
{
|
||||||
def extract_functions(flags):
|
int result = 0;
|
||||||
"""Extract the function definitions from deltachat.h.
|
if (DC_EVENT_DATA1_IS_STRING(e))
|
||||||
|
result |= 1;
|
||||||
This creates a .h file with a single `#include <deltachat.h>` line
|
if (DC_EVENT_DATA2_IS_STRING(e))
|
||||||
in it. It then runs the C preprocessor to create an output file
|
result |= 2;
|
||||||
which contains all function definitions found in `deltachat.h`.
|
if (DC_EVENT_RETURNS_STRING(e))
|
||||||
"""
|
result |= 4;
|
||||||
|
if (DC_EVENT_RETURNS_INT(e))
|
||||||
|
result |= 8;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
include_dirs=incs,
|
||||||
|
libraries=libs,
|
||||||
|
extra_objects=objs,
|
||||||
|
extra_link_args=extra_link_args,
|
||||||
|
)
|
||||||
|
builder.cdef("""
|
||||||
|
typedef int... time_t;
|
||||||
|
void free(void *ptr);
|
||||||
|
extern const char * dupstring_helper(const char* string);
|
||||||
|
extern int dc_get_event_signature_types(int);
|
||||||
|
""")
|
||||||
distutils.log.set_verbosity(distutils.log.INFO)
|
distutils.log.set_verbosity(distutils.log.INFO)
|
||||||
cc = distutils.ccompiler.new_compiler(force=True)
|
cc = distutils.ccompiler.new_compiler(force=True)
|
||||||
distutils.sysconfig.customize_compiler(cc)
|
distutils.sysconfig.customize_compiler(cc)
|
||||||
@@ -64,131 +85,20 @@ def extract_functions(flags):
|
|||||||
src_fp.write('#include <deltachat.h>')
|
src_fp.write('#include <deltachat.h>')
|
||||||
cc.preprocess(source=src_name,
|
cc.preprocess(source=src_name,
|
||||||
output_file=dst_name,
|
output_file=dst_name,
|
||||||
include_dirs=flags['include_dirs'],
|
include_dirs=incs,
|
||||||
macros=[('PY_CFFI', '1')])
|
macros=[('PY_CFFI', '1')])
|
||||||
with open(dst_name, "r") as dst_fp:
|
with open(dst_name, "r") as dst_fp:
|
||||||
return dst_fp.read()
|
builder.cdef(dst_fp.read())
|
||||||
finally:
|
finally:
|
||||||
shutil.rmtree(tmpdir)
|
shutil.rmtree(tmpdir)
|
||||||
|
|
||||||
|
|
||||||
def find_header(flags):
|
|
||||||
"""Use the compiler to find the deltachat.h header location.
|
|
||||||
|
|
||||||
This uses a small utility in deltachat.h to find the location of
|
|
||||||
the header file location.
|
|
||||||
"""
|
|
||||||
distutils.log.set_verbosity(distutils.log.INFO)
|
|
||||||
cc = distutils.ccompiler.new_compiler(force=True)
|
|
||||||
distutils.sysconfig.customize_compiler(cc)
|
|
||||||
tmpdir = tempfile.mkdtemp()
|
|
||||||
try:
|
|
||||||
src_name = os.path.join(tmpdir, "where.c")
|
|
||||||
obj_name = os.path.join(tmpdir, "where.o")
|
|
||||||
dst_name = os.path.join(tmpdir, "where")
|
|
||||||
with open(src_name, "w") as src_fp:
|
|
||||||
src_fp.write(textwrap.dedent("""
|
|
||||||
#include <stdio.h>
|
|
||||||
#include <deltachat.h>
|
|
||||||
|
|
||||||
int main(void) {
|
|
||||||
printf("%s", _dc_header_file_location());
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
"""))
|
|
||||||
cwd = os.getcwd()
|
|
||||||
try:
|
|
||||||
os.chdir(tmpdir)
|
|
||||||
cc.compile(sources=["where.c"],
|
|
||||||
include_dirs=flags['include_dirs'],
|
|
||||||
macros=[("PY_CFFI_INC", "1")])
|
|
||||||
finally:
|
|
||||||
os.chdir(cwd)
|
|
||||||
cc.link_executable(objects=[obj_name],
|
|
||||||
output_progname="where",
|
|
||||||
output_dir=tmpdir)
|
|
||||||
return subprocess.check_output(dst_name)
|
|
||||||
finally:
|
|
||||||
shutil.rmtree(tmpdir)
|
|
||||||
|
|
||||||
|
|
||||||
def extract_defines(flags):
|
|
||||||
"""Extract the required #DEFINEs from deltachat.h.
|
|
||||||
|
|
||||||
Since #DEFINEs are interpreted by the C preprocessor we can not
|
|
||||||
use the compiler to extract these and need to parse the header
|
|
||||||
file ourselves.
|
|
||||||
|
|
||||||
The defines are returned in a string that can be passed to CFFIs
|
|
||||||
cdef() method.
|
|
||||||
"""
|
|
||||||
header = find_header(flags)
|
|
||||||
defines_re = re.compile(r"""
|
|
||||||
\#define\s+ # The start of a define.
|
|
||||||
( # Begin capturing group which captures the define name.
|
|
||||||
(?: # A nested group which is not captured, this allows us
|
|
||||||
# to build the list of prefixes to extract without
|
|
||||||
# creation another capture group.
|
|
||||||
DC_EVENT
|
|
||||||
| DC_QR
|
|
||||||
| DC_MSG
|
|
||||||
| DC_LP
|
|
||||||
| DC_EMPTY
|
|
||||||
| DC_CERTCK
|
|
||||||
| DC_STATE
|
|
||||||
| DC_STR
|
|
||||||
| DC_CONTACT_ID
|
|
||||||
| DC_GCL
|
|
||||||
| DC_GCM
|
|
||||||
| DC_SOCKET
|
|
||||||
| DC_CHAT
|
|
||||||
| DC_PROVIDER
|
|
||||||
| DC_KEY_GEN
|
|
||||||
| DC_IMEX
|
|
||||||
| DC_CONNECTIVITY
|
|
||||||
) # End of prefix matching
|
|
||||||
_[\w_]+ # Match the suffix, e.g. _RSA2048 in DC_KEY_GEN_RSA2048
|
|
||||||
) # Close the capturing group, this contains
|
|
||||||
# the entire name e.g. DC_MSG_TEXT.
|
|
||||||
\s+\S+ # Ensure there is whitespace followed by a value.
|
|
||||||
""", re.VERBOSE)
|
|
||||||
defines = []
|
|
||||||
with open(header) as fp:
|
|
||||||
for line in fp:
|
|
||||||
match = defines_re.match(line)
|
|
||||||
if match:
|
|
||||||
defines.append(match.group(1))
|
|
||||||
return '\n'.join('#define {} ...'.format(d) for d in defines)
|
|
||||||
|
|
||||||
|
|
||||||
def ffibuilder():
|
|
||||||
projdir = os.environ.get('DCC_RS_DEV')
|
|
||||||
if projdir:
|
|
||||||
target = os.environ.get('DCC_RS_TARGET', 'release')
|
|
||||||
flags = local_build_flags(projdir, target)
|
|
||||||
else:
|
|
||||||
flags = system_build_flags()
|
|
||||||
builder = cffi.FFI()
|
|
||||||
builder.set_source(
|
|
||||||
'deltachat.capi',
|
|
||||||
"""
|
|
||||||
#include <deltachat.h>
|
|
||||||
int dc_event_has_string_data(int e)
|
|
||||||
{
|
|
||||||
return DC_EVENT_DATA2_IS_STRING(e);
|
|
||||||
}
|
|
||||||
""",
|
|
||||||
**flags,
|
|
||||||
)
|
|
||||||
builder.cdef("""
|
builder.cdef("""
|
||||||
typedef int... time_t;
|
extern "Python" uintptr_t py_dc_callback(
|
||||||
void free(void *ptr);
|
dc_context_t* context,
|
||||||
extern int dc_event_has_string_data(int);
|
int event,
|
||||||
|
uintptr_t data1,
|
||||||
|
uintptr_t data2);
|
||||||
""")
|
""")
|
||||||
function_defs = extract_functions(flags)
|
|
||||||
defines = extract_defines(flags)
|
|
||||||
builder.cdef(function_defs)
|
|
||||||
builder.cdef(defines)
|
|
||||||
return builder
|
return builder
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,25 +1,22 @@
|
|||||||
""" Account class implementation. """
|
""" Account class implementation. """
|
||||||
|
|
||||||
from __future__ import print_function
|
from __future__ import print_function
|
||||||
from contextlib import contextmanager
|
import atexit
|
||||||
from email.utils import parseaddr
|
import threading
|
||||||
from threading import Event
|
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
from array import array
|
from array import array
|
||||||
|
from queue import Queue
|
||||||
|
|
||||||
|
import deltachat
|
||||||
from . import const
|
from . import const
|
||||||
from .capi import ffi, lib
|
from .capi import ffi, lib
|
||||||
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array, DCLot
|
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array, DCLot
|
||||||
from .chat import Chat
|
from .chat import Chat
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from .contact import Contact
|
from .contact import Contact
|
||||||
from .tracker import ImexTracker, ConfigureTracker
|
from .eventlogger import EventLogger
|
||||||
from . import hookspec
|
from .hookspec import get_plugin_manager, hookimpl
|
||||||
from .events import EventThread
|
|
||||||
from typing import Union, Any, Dict, Optional, List, Generator
|
|
||||||
|
|
||||||
|
|
||||||
class MissingCredentials(ValueError):
|
|
||||||
""" Account is missing `addr` and `mail_pw` config values. """
|
|
||||||
|
|
||||||
|
|
||||||
class Account(object):
|
class Account(object):
|
||||||
@@ -27,59 +24,51 @@ class Account(object):
|
|||||||
by the underlying deltachat core library. All public Account methods are
|
by the underlying deltachat core library. All public Account methods are
|
||||||
meant to be memory-safe and return memory-safe objects.
|
meant to be memory-safe and return memory-safe objects.
|
||||||
"""
|
"""
|
||||||
MissingCredentials = MissingCredentials
|
def __init__(self, db_path, logid=None, os_name=None, debug=True):
|
||||||
|
|
||||||
def __init__(self, db_path, os_name=None, logging=True) -> None:
|
|
||||||
""" initialize account object.
|
""" initialize account object.
|
||||||
|
|
||||||
:param db_path: a path to the account database. The database
|
:param db_path: a path to the account database. The database
|
||||||
will be created if it doesn't exist.
|
will be created if it doesn't exist.
|
||||||
|
:param logid: an optional logging prefix that should be used with
|
||||||
|
the default internal logging.
|
||||||
:param os_name: this will be put to the X-Mailer header in outgoing messages
|
:param os_name: this will be put to the X-Mailer header in outgoing messages
|
||||||
|
:param debug: turn on debug logging for events.
|
||||||
"""
|
"""
|
||||||
# initialize per-account plugin system
|
self._dc_context = ffi.gc(
|
||||||
self._pm = hookspec.PerAccount._make_plugin_manager()
|
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, as_dc_charpointer(os_name)),
|
||||||
self._logging = logging
|
_destroy_dc_context,
|
||||||
|
)
|
||||||
|
self._evlogger = EventLogger(self, logid, debug)
|
||||||
|
self._threads = IOThreads(self._dc_context, self._evlogger._log_event)
|
||||||
|
|
||||||
self.add_account_plugin(self)
|
# register event call back and initialize plugin system
|
||||||
|
def _ll_event(ctx, evt_name, data1, data2):
|
||||||
|
assert ctx == self._dc_context
|
||||||
|
self.pluggy.hook.process_low_level_event(
|
||||||
|
account=self, event_name=evt_name, data1=data1, data2=data2
|
||||||
|
)
|
||||||
|
|
||||||
self.db_path = db_path
|
self.pluggy = get_plugin_manager()
|
||||||
|
self.pluggy.register(self._evlogger)
|
||||||
|
deltachat.set_context_callback(self._dc_context, _ll_event)
|
||||||
|
|
||||||
|
# open database
|
||||||
if hasattr(db_path, "encode"):
|
if hasattr(db_path, "encode"):
|
||||||
db_path = db_path.encode("utf8")
|
db_path = db_path.encode("utf8")
|
||||||
|
if not lib.dc_open(self._dc_context, db_path, ffi.NULL):
|
||||||
self._dc_context = ffi.gc(
|
raise ValueError("Could not dc_open: {}".format(db_path))
|
||||||
lib.dc_context_new(as_dc_charpointer(os_name), db_path, ffi.NULL),
|
|
||||||
lib.dc_context_unref,
|
|
||||||
)
|
|
||||||
if self._dc_context == ffi.NULL:
|
|
||||||
raise ValueError("Could not dc_context_new: {} {}".format(os_name, db_path))
|
|
||||||
|
|
||||||
self._shutdown_event = Event()
|
|
||||||
self._event_thread = EventThread(self)
|
|
||||||
self._configkeys = self.get_config("sys.config_keys").split()
|
self._configkeys = self.get_config("sys.config_keys").split()
|
||||||
hook = hookspec.Global._get_plugin_manager().hook
|
atexit.register(self.shutdown)
|
||||||
hook.dc_account_init(account=self)
|
|
||||||
|
|
||||||
def disable_logging(self) -> None:
|
|
||||||
""" disable logging. """
|
|
||||||
self._logging = False
|
|
||||||
|
|
||||||
def enable_logging(self) -> None:
|
|
||||||
""" re-enable logging. """
|
|
||||||
self._logging = True
|
|
||||||
|
|
||||||
# def __del__(self):
|
# def __del__(self):
|
||||||
# self.shutdown()
|
# self.shutdown()
|
||||||
|
|
||||||
def log(self, msg):
|
def _check_config_key(self, name):
|
||||||
if self._logging:
|
|
||||||
self._pm.hook.ac_log_line(message=msg)
|
|
||||||
|
|
||||||
def _check_config_key(self, name: str) -> None:
|
|
||||||
if name not in self._configkeys:
|
if name not in self._configkeys:
|
||||||
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
|
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
|
||||||
name, self._configkeys))
|
name, self._configkeys))
|
||||||
|
|
||||||
def get_info(self) -> Dict[str, str]:
|
def get_info(self):
|
||||||
""" return dictionary of built config parameters. """
|
""" return dictionary of built config parameters. """
|
||||||
lines = from_dc_charpointer(lib.dc_get_info(self._dc_context))
|
lines = from_dc_charpointer(lib.dc_get_info(self._dc_context))
|
||||||
d = {}
|
d = {}
|
||||||
@@ -90,35 +79,19 @@ class Account(object):
|
|||||||
d[key.lower()] = value
|
d[key.lower()] = value
|
||||||
return d
|
return d
|
||||||
|
|
||||||
def dump_account_info(self, logfile):
|
def set_stock_translation(self, id, string):
|
||||||
def log(*args, **kwargs):
|
|
||||||
kwargs["file"] = logfile
|
|
||||||
print(*args, **kwargs)
|
|
||||||
|
|
||||||
log("=============== " + self.get_config("displayname") + " ===============")
|
|
||||||
cursor = 0
|
|
||||||
for name, val in self.get_info().items():
|
|
||||||
entry = "{}={}".format(name.upper(), val)
|
|
||||||
if cursor + len(entry) > 80:
|
|
||||||
log("")
|
|
||||||
cursor = 0
|
|
||||||
log(entry, end=" ")
|
|
||||||
cursor += len(entry) + 1
|
|
||||||
log("")
|
|
||||||
|
|
||||||
def set_stock_translation(self, id: int, string: str) -> None:
|
|
||||||
""" set stock translation string.
|
""" set stock translation string.
|
||||||
|
|
||||||
:param id: id of stock string (const.DC_STR_*)
|
:param id: id of stock string (const.DC_STR_*)
|
||||||
:param value: string to set as new transalation
|
:param value: string to set as new transalation
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
bytestring = string.encode("utf8")
|
string = string.encode("utf8")
|
||||||
res = lib.dc_set_stock_translation(self._dc_context, id, bytestring)
|
res = lib.dc_set_stock_translation(self._dc_context, id, string)
|
||||||
if res == 0:
|
if res == 0:
|
||||||
raise ValueError("could not set translation string")
|
raise ValueError("could not set translation string")
|
||||||
|
|
||||||
def set_config(self, name: str, value: Optional[str]) -> None:
|
def set_config(self, name, value):
|
||||||
""" set configuration values.
|
""" set configuration values.
|
||||||
|
|
||||||
:param name: config key name (unicode)
|
:param name: config key name (unicode)
|
||||||
@@ -126,16 +99,16 @@ class Account(object):
|
|||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
self._check_config_key(name)
|
self._check_config_key(name)
|
||||||
namebytes = name.encode("utf8")
|
name = name.encode("utf8")
|
||||||
if namebytes == b"addr" and self.is_configured():
|
if name == b"addr" and self.is_configured():
|
||||||
raise ValueError("can not change 'addr' after account is configured.")
|
raise ValueError("can not change 'addr' after account is configured.")
|
||||||
if value is not None:
|
if value is not None:
|
||||||
valuebytes = value.encode("utf8")
|
value = value.encode("utf8")
|
||||||
else:
|
else:
|
||||||
valuebytes = ffi.NULL
|
value = ffi.NULL
|
||||||
lib.dc_set_config(self._dc_context, namebytes, valuebytes)
|
lib.dc_set_config(self._dc_context, name, value)
|
||||||
|
|
||||||
def get_config(self, name: str) -> str:
|
def get_config(self, name):
|
||||||
""" return unicode string value.
|
""" return unicode string value.
|
||||||
|
|
||||||
:param name: configuration key to lookup (eg "addr" or "mail_pw")
|
:param name: configuration key to lookup (eg "addr" or "mail_pw")
|
||||||
@@ -144,12 +117,12 @@ class Account(object):
|
|||||||
"""
|
"""
|
||||||
if name != "sys.config_keys":
|
if name != "sys.config_keys":
|
||||||
self._check_config_key(name)
|
self._check_config_key(name)
|
||||||
namebytes = name.encode("utf8")
|
name = name.encode("utf8")
|
||||||
res = lib.dc_get_config(self._dc_context, namebytes)
|
res = lib.dc_get_config(self._dc_context, name)
|
||||||
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
|
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
|
||||||
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, public, secret):
|
||||||
"""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.
|
||||||
@@ -161,31 +134,26 @@ class Account(object):
|
|||||||
if res == 0:
|
if res == 0:
|
||||||
raise Exception("Failed to set key")
|
raise Exception("Failed to set key")
|
||||||
|
|
||||||
def update_config(self, kwargs: Dict[str, Any]) -> None:
|
def configure(self, **kwargs):
|
||||||
""" update config values.
|
""" set config values and configure this account.
|
||||||
|
|
||||||
:param kwargs: name=value config settings for this account.
|
:param kwargs: name=value config settings for this account.
|
||||||
values need to be unicode.
|
values need to be unicode.
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
for key, value in kwargs.items():
|
for name, value in kwargs.items():
|
||||||
self.set_config(key, str(value))
|
self.set_config(name, value)
|
||||||
|
lib.dc_configure(self._dc_context)
|
||||||
|
|
||||||
def is_configured(self) -> bool:
|
def is_configured(self):
|
||||||
""" determine if the account is configured already; an initial connection
|
""" determine if the account is configured already; an initial connection
|
||||||
to SMTP/IMAP has been verified.
|
to SMTP/IMAP has been verified.
|
||||||
|
|
||||||
:returns: True if account is configured.
|
:returns: True if account is configured.
|
||||||
"""
|
"""
|
||||||
return True if lib.dc_is_configured(self._dc_context) else False
|
return lib.dc_is_configured(self._dc_context)
|
||||||
|
|
||||||
def is_open(self) -> bool:
|
def set_avatar(self, img_path):
|
||||||
"""Determine if account is open
|
|
||||||
|
|
||||||
:returns True if account is open."""
|
|
||||||
return True if lib.dc_context_is_open(self._dc_context) else False
|
|
||||||
|
|
||||||
def set_avatar(self, img_path: Optional[str]) -> None:
|
|
||||||
"""Set self avatar.
|
"""Set self avatar.
|
||||||
|
|
||||||
:raises ValueError: if profile image could not be set
|
:raises ValueError: if profile image could not be set
|
||||||
@@ -197,18 +165,31 @@ class Account(object):
|
|||||||
assert os.path.exists(img_path), img_path
|
assert os.path.exists(img_path), img_path
|
||||||
self.set_config("selfavatar", img_path)
|
self.set_config("selfavatar", img_path)
|
||||||
|
|
||||||
def check_is_configured(self) -> None:
|
def check_is_configured(self):
|
||||||
""" Raise ValueError if this account is not configured. """
|
""" Raise ValueError if this account is not configured. """
|
||||||
if not self.is_configured():
|
if not self.is_configured():
|
||||||
raise ValueError("need to configure first")
|
raise ValueError("need to configure first")
|
||||||
|
|
||||||
def get_latest_backupfile(self, backupdir) -> Optional[str]:
|
def empty_server_folders(self, inbox=False, mvbox=False):
|
||||||
|
""" empty server folders. """
|
||||||
|
flags = 0
|
||||||
|
if inbox:
|
||||||
|
flags |= const.DC_EMPTY_INBOX
|
||||||
|
if mvbox:
|
||||||
|
flags |= const.DC_EMPTY_MVBOX
|
||||||
|
if not flags:
|
||||||
|
raise ValueError("no flags set")
|
||||||
|
lib.dc_empty_server(self._dc_context, flags)
|
||||||
|
|
||||||
|
def get_latest_backupfile(self, backupdir):
|
||||||
""" return the latest backup file in a given directory.
|
""" return the latest backup file in a given directory.
|
||||||
"""
|
"""
|
||||||
res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir))
|
res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir))
|
||||||
return from_optional_dc_charpointer(res)
|
if res == ffi.NULL:
|
||||||
|
return None
|
||||||
|
return from_dc_charpointer(res)
|
||||||
|
|
||||||
def get_blobdir(self) -> str:
|
def get_blobdir(self):
|
||||||
""" return the directory for files.
|
""" return the directory for files.
|
||||||
|
|
||||||
All sent files are copied to this directory if necessary.
|
All sent files are copied to this directory if necessary.
|
||||||
@@ -216,100 +197,42 @@ class Account(object):
|
|||||||
"""
|
"""
|
||||||
return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context))
|
return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context))
|
||||||
|
|
||||||
def get_self_contact(self) -> Contact:
|
def get_self_contact(self):
|
||||||
""" return this account's identity as a :class:`deltachat.contact.Contact`.
|
""" return this account's identity as a :class:`deltachat.contact.Contact`.
|
||||||
|
|
||||||
:returns: :class:`deltachat.contact.Contact`
|
:returns: :class:`deltachat.contact.Contact`
|
||||||
"""
|
"""
|
||||||
return Contact(self, const.DC_CONTACT_ID_SELF)
|
self.check_is_configured()
|
||||||
|
return Contact(self._dc_context, const.DC_CONTACT_ID_SELF)
|
||||||
|
|
||||||
def create_contact(self, obj, name: Optional[str] = None) -> Contact:
|
def create_contact(self, email, name=None):
|
||||||
"""create a (new) Contact or return an existing one.
|
""" create a (new) Contact. If there already is a Contact
|
||||||
|
with that e-mail address, it is unblocked and its name is
|
||||||
|
updated.
|
||||||
|
|
||||||
Calling this method will always result in the same
|
:param email: email-address (text type)
|
||||||
underlying contact id. If there already is a Contact
|
:param name: display name for this contact (optional)
|
||||||
with that e-mail address, it is unblocked and its display
|
|
||||||
`name` is updated if specified.
|
|
||||||
|
|
||||||
:param obj: email-address, Account or Contact instance.
|
|
||||||
:param name: (optional) display name for this contact
|
|
||||||
:returns: :class:`deltachat.contact.Contact` instance.
|
:returns: :class:`deltachat.contact.Contact` instance.
|
||||||
"""
|
"""
|
||||||
(name, addr) = self.get_contact_addr_and_name(obj, name)
|
|
||||||
name = as_dc_charpointer(name)
|
name = as_dc_charpointer(name)
|
||||||
addr = as_dc_charpointer(addr)
|
email = as_dc_charpointer(email)
|
||||||
contact_id = lib.dc_create_contact(self._dc_context, name, addr)
|
contact_id = lib.dc_create_contact(self._dc_context, name, email)
|
||||||
return Contact(self, contact_id)
|
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
|
return Contact(self._dc_context, contact_id)
|
||||||
|
|
||||||
def get_contact(self, obj) -> Optional[Contact]:
|
def delete_contact(self, contact):
|
||||||
if isinstance(obj, Contact):
|
|
||||||
return obj
|
|
||||||
(_, addr) = self.get_contact_addr_and_name(obj)
|
|
||||||
return self.get_contact_by_addr(addr)
|
|
||||||
|
|
||||||
def get_contact_addr_and_name(self, obj, name: Optional[str] = None):
|
|
||||||
if isinstance(obj, Account):
|
|
||||||
if not obj.is_configured():
|
|
||||||
raise ValueError("can only add addresses from configured accounts")
|
|
||||||
addr, displayname = obj.get_config("addr"), obj.get_config("displayname")
|
|
||||||
elif isinstance(obj, Contact):
|
|
||||||
if obj.account != self:
|
|
||||||
raise ValueError("account mismatch {}".format(obj))
|
|
||||||
addr, displayname = obj.addr, obj.name
|
|
||||||
elif isinstance(obj, str):
|
|
||||||
displayname, addr = parseaddr(obj)
|
|
||||||
else:
|
|
||||||
raise TypeError("don't know how to create chat for %r" % (obj, ))
|
|
||||||
|
|
||||||
if name is None and displayname:
|
|
||||||
name = displayname
|
|
||||||
return (name, addr)
|
|
||||||
|
|
||||||
def delete_contact(self, contact: Contact) -> bool:
|
|
||||||
""" delete a Contact.
|
""" delete a Contact.
|
||||||
|
|
||||||
:param contact: contact object obtained
|
:param contact: contact object obtained
|
||||||
:returns: True if deletion succeeded (contact was deleted)
|
:returns: True if deletion succeeded (contact was deleted)
|
||||||
"""
|
"""
|
||||||
contact_id = contact.id
|
contact_id = contact.id
|
||||||
assert contact.account == self
|
assert contact._dc_context == self._dc_context
|
||||||
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
return bool(lib.dc_delete_contact(self._dc_context, contact_id))
|
||||||
|
|
||||||
def get_contact_by_addr(self, email: str) -> Optional[Contact]:
|
def get_contacts(self, query=None, with_self=False, only_verified=False):
|
||||||
""" get a contact for the email address or None if it's blocked or doesn't exist. """
|
""" get a (filtered) list of contacts.
|
||||||
_, addr = parseaddr(email)
|
|
||||||
addr = as_dc_charpointer(addr)
|
|
||||||
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
|
|
||||||
if contact_id:
|
|
||||||
return self.get_contact_by_id(contact_id)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_contact_by_id(self, contact_id: int) -> Contact:
|
|
||||||
""" return Contact instance or raise an exception.
|
|
||||||
:param contact_id: integer id of this contact.
|
|
||||||
:returns: :class:`deltachat.contact.Contact` instance.
|
|
||||||
"""
|
|
||||||
return Contact(self, contact_id)
|
|
||||||
|
|
||||||
def get_blocked_contacts(self) -> List[Contact]:
|
|
||||||
""" return a list of all blocked contacts.
|
|
||||||
|
|
||||||
:returns: list of :class:`deltachat.contact.Contact` objects.
|
|
||||||
"""
|
|
||||||
dc_array = ffi.gc(
|
|
||||||
lib.dc_get_blocked_contacts(self._dc_context),
|
|
||||||
lib.dc_array_unref
|
|
||||||
)
|
|
||||||
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
|
||||||
|
|
||||||
def get_contacts(
|
|
||||||
self,
|
|
||||||
query: Optional[str] = None,
|
|
||||||
with_self: bool = False,
|
|
||||||
only_verified: bool = False,
|
|
||||||
) -> List[Contact]:
|
|
||||||
"""get a (filtered) list of contacts.
|
|
||||||
|
|
||||||
:param query: if a string is specified, only return contacts
|
:param query: if a string is specified, only return contacts
|
||||||
whose name or e-mail matches query.
|
whose name or e-mail matches query.
|
||||||
@@ -327,43 +250,54 @@ class Account(object):
|
|||||||
lib.dc_get_contacts(self._dc_context, flags, query),
|
lib.dc_get_contacts(self._dc_context, flags, query),
|
||||||
lib.dc_array_unref
|
lib.dc_array_unref
|
||||||
)
|
)
|
||||||
return list(iter_array(dc_array, lambda x: Contact(self, x)))
|
return list(iter_array(dc_array, lambda x: Contact(self._dc_context, x)))
|
||||||
|
|
||||||
def get_fresh_messages(self) -> Generator[Message, None, None]:
|
def create_chat_by_contact(self, contact):
|
||||||
""" yield all fresh messages from all chats. """
|
""" create or get an existing 1:1 chat object for the specified contact or contact id.
|
||||||
dc_array = ffi.gc(
|
|
||||||
lib.dc_get_fresh_msgs(self._dc_context),
|
|
||||||
lib.dc_array_unref
|
|
||||||
)
|
|
||||||
yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
|
|
||||||
|
|
||||||
def create_chat(self, obj) -> Chat:
|
:param contact: chat_id (int) or contact object.
|
||||||
""" Create a 1:1 chat with Account, Contact or e-mail address. """
|
:returns: a :class:`deltachat.chat.Chat` object.
|
||||||
return self.create_contact(obj).create_chat()
|
"""
|
||||||
|
if hasattr(contact, "id"):
|
||||||
|
if contact._dc_context != self._dc_context:
|
||||||
|
raise ValueError("Contact belongs to a different Account")
|
||||||
|
contact_id = contact.id
|
||||||
|
else:
|
||||||
|
assert isinstance(contact, int)
|
||||||
|
contact_id = contact
|
||||||
|
chat_id = lib.dc_create_chat_by_contact_id(self._dc_context, contact_id)
|
||||||
|
return Chat(self, chat_id)
|
||||||
|
|
||||||
def create_group_chat(
|
def create_chat_by_message(self, message):
|
||||||
self,
|
""" create or get an existing chat object for the
|
||||||
name: str,
|
the specified message.
|
||||||
contacts: Optional[List[Contact]] = None,
|
|
||||||
verified: bool = False,
|
:param message: messsage id or message instance.
|
||||||
) -> Chat:
|
:returns: a :class:`deltachat.chat.Chat` object.
|
||||||
"""create a new group chat object.
|
"""
|
||||||
|
if hasattr(message, "id"):
|
||||||
|
if self._dc_context != message._dc_context:
|
||||||
|
raise ValueError("Message belongs to a different Account")
|
||||||
|
msg_id = message.id
|
||||||
|
else:
|
||||||
|
assert isinstance(message, int)
|
||||||
|
msg_id = message
|
||||||
|
chat_id = lib.dc_create_chat_by_msg_id(self._dc_context, msg_id)
|
||||||
|
return Chat(self, chat_id)
|
||||||
|
|
||||||
|
def create_group_chat(self, name, verified=False):
|
||||||
|
""" create a new group chat object.
|
||||||
|
|
||||||
Chats are unpromoted until the first message is sent.
|
Chats are unpromoted until the first message is sent.
|
||||||
|
|
||||||
:param contacts: list of contacts to add
|
|
||||||
:param verified: if true only verified contacts can be added.
|
:param verified: if true only verified contacts can be added.
|
||||||
:returns: a :class:`deltachat.chat.Chat` object.
|
:returns: a :class:`deltachat.chat.Chat` object.
|
||||||
"""
|
"""
|
||||||
bytes_name = name.encode("utf8")
|
bytes_name = name.encode("utf8")
|
||||||
chat_id = lib.dc_create_group_chat(self._dc_context, int(verified), bytes_name)
|
chat_id = lib.dc_create_group_chat(self._dc_context, int(verified), bytes_name)
|
||||||
chat = Chat(self, chat_id)
|
return Chat(self, chat_id)
|
||||||
if contacts is not None:
|
|
||||||
for contact in contacts:
|
|
||||||
chat.add_contact(contact)
|
|
||||||
return chat
|
|
||||||
|
|
||||||
def get_chats(self) -> List[Chat]:
|
def get_chats(self):
|
||||||
""" return list of chats.
|
""" return list of chats.
|
||||||
|
|
||||||
:returns: a list of :class:`deltachat.chat.Chat` objects.
|
:returns: a list of :class:`deltachat.chat.Chat` objects.
|
||||||
@@ -380,17 +314,17 @@ class Account(object):
|
|||||||
chatlist.append(Chat(self, chat_id))
|
chatlist.append(Chat(self, chat_id))
|
||||||
return chatlist
|
return chatlist
|
||||||
|
|
||||||
def get_device_chat(self) -> Chat:
|
def get_deaddrop_chat(self):
|
||||||
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
|
return Chat(self, const.DC_CHAT_ID_DEADDROP)
|
||||||
|
|
||||||
def get_message_by_id(self, msg_id: int) -> Message:
|
def get_message_by_id(self, msg_id):
|
||||||
""" return Message instance.
|
""" return Message instance.
|
||||||
:param msg_id: integer id of this message.
|
:param msg_id: integer id of this message.
|
||||||
:returns: :class:`deltachat.message.Message` instance.
|
:returns: :class:`deltachat.message.Message` instance.
|
||||||
"""
|
"""
|
||||||
return Message.from_db(self, msg_id)
|
return Message.from_db(self, msg_id)
|
||||||
|
|
||||||
def get_chat_by_id(self, chat_id: int) -> Chat:
|
def get_chat_by_id(self, chat_id):
|
||||||
""" return Chat instance.
|
""" return Chat instance.
|
||||||
:param chat_id: integer id of this chat.
|
:param chat_id: integer id of this chat.
|
||||||
:returns: :class:`deltachat.chat.Chat` instance.
|
:returns: :class:`deltachat.chat.Chat` instance.
|
||||||
@@ -402,21 +336,19 @@ class Account(object):
|
|||||||
lib.dc_chat_unref(res)
|
lib.dc_chat_unref(res)
|
||||||
return Chat(self, chat_id)
|
return Chat(self, chat_id)
|
||||||
|
|
||||||
def mark_seen_messages(self, messages: List[Union[int, Message]]) -> None:
|
def mark_seen_messages(self, messages):
|
||||||
""" mark the given set of messages as seen.
|
""" mark the given set of messages as seen.
|
||||||
|
|
||||||
:param messages: a list of message ids or Message instances.
|
:param messages: a list of message ids or Message instances.
|
||||||
"""
|
"""
|
||||||
arr = array("i")
|
arr = array("i")
|
||||||
for msg in messages:
|
for msg in messages:
|
||||||
if isinstance(msg, Message):
|
msg = getattr(msg, "id", msg)
|
||||||
arr.append(msg.id)
|
arr.append(msg)
|
||||||
else:
|
|
||||||
arr.append(msg)
|
|
||||||
msg_ids = ffi.cast("uint32_t*", ffi.from_buffer(arr))
|
msg_ids = ffi.cast("uint32_t*", ffi.from_buffer(arr))
|
||||||
lib.dc_markseen_msgs(self._dc_context, msg_ids, len(messages))
|
lib.dc_markseen_msgs(self._dc_context, msg_ids, len(messages))
|
||||||
|
|
||||||
def forward_messages(self, messages: List[Message], chat: Chat) -> None:
|
def forward_messages(self, messages, chat):
|
||||||
""" Forward list of messages to a chat.
|
""" Forward list of messages to a chat.
|
||||||
|
|
||||||
:param messages: list of :class:`deltachat.message.Message` object.
|
:param messages: list of :class:`deltachat.message.Message` object.
|
||||||
@@ -426,7 +358,7 @@ class Account(object):
|
|||||||
msg_ids = [msg.id for msg in messages]
|
msg_ids = [msg.id for msg in messages]
|
||||||
lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id)
|
lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id)
|
||||||
|
|
||||||
def delete_messages(self, messages: List[Message]) -> None:
|
def delete_messages(self, messages):
|
||||||
""" delete messages (local and remote).
|
""" delete messages (local and remote).
|
||||||
|
|
||||||
:param messages: list of :class:`deltachat.message.Message` object.
|
:param messages: list of :class:`deltachat.message.Message` object.
|
||||||
@@ -436,37 +368,32 @@ class Account(object):
|
|||||||
lib.dc_delete_msgs(self._dc_context, msg_ids, len(msg_ids))
|
lib.dc_delete_msgs(self._dc_context, msg_ids, len(msg_ids))
|
||||||
|
|
||||||
def export_self_keys(self, path):
|
def export_self_keys(self, path):
|
||||||
""" export public and private keys to the specified directory.
|
""" export public and private keys to the specified directory. """
|
||||||
|
return self._export(path, imex_cmd=1)
|
||||||
Note that the account does not have to be started.
|
|
||||||
"""
|
|
||||||
return self._export(path, imex_cmd=const.DC_IMEX_EXPORT_SELF_KEYS)
|
|
||||||
|
|
||||||
def export_all(self, path):
|
def export_all(self, path):
|
||||||
"""return new file containing a backup of all database state
|
"""return new file containing a backup of all database state
|
||||||
(chats, contacts, keys, media, ...). The file is created in the
|
(chats, contacts, keys, media, ...). The file is created in the
|
||||||
the `path` directory.
|
the `path` directory.
|
||||||
|
|
||||||
Note that the account has to be stopped; call stop_io() if necessary.
|
|
||||||
"""
|
"""
|
||||||
export_files = self._export(path, const.DC_IMEX_EXPORT_BACKUP)
|
export_files = self._export(path, 11)
|
||||||
if len(export_files) != 1:
|
if len(export_files) != 1:
|
||||||
raise RuntimeError("found more than one new file")
|
raise RuntimeError("found more than one new file")
|
||||||
return export_files[0]
|
return export_files[0]
|
||||||
|
|
||||||
def _export(self, path, imex_cmd):
|
def _export(self, path, imex_cmd):
|
||||||
with self.temp_plugin(ImexTracker()) as imex_tracker:
|
with ImexTracker(self) as imex_tracker:
|
||||||
self.imex(path, imex_cmd)
|
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||||
|
if not self._threads.is_started():
|
||||||
|
lib.dc_perform_imap_jobs(self._dc_context)
|
||||||
return imex_tracker.wait_finish()
|
return imex_tracker.wait_finish()
|
||||||
|
|
||||||
def import_self_keys(self, path):
|
def import_self_keys(self, path):
|
||||||
""" Import private keys found in the `path` directory.
|
""" Import private keys found in the `path` directory.
|
||||||
The last imported key is made the default keys unless its name
|
The last imported key is made the default keys unless its name
|
||||||
contains the string legacy. Public keys are not imported.
|
contains the string legacy. Public keys are not imported.
|
||||||
|
|
||||||
Note that the account does not have to be started.
|
|
||||||
"""
|
"""
|
||||||
self._import(path, imex_cmd=const.DC_IMEX_IMPORT_SELF_KEYS)
|
self._import(path, imex_cmd=2)
|
||||||
|
|
||||||
def import_all(self, path):
|
def import_all(self, path):
|
||||||
"""import delta chat state from the specified backup `path` (a file).
|
"""import delta chat state from the specified backup `path` (a file).
|
||||||
@@ -474,28 +401,29 @@ class Account(object):
|
|||||||
The account must be in unconfigured state for import to attempted.
|
The account must be in unconfigured state for import to attempted.
|
||||||
"""
|
"""
|
||||||
assert not self.is_configured(), "cannot import into configured account"
|
assert not self.is_configured(), "cannot import into configured account"
|
||||||
self._import(path, imex_cmd=const.DC_IMEX_IMPORT_BACKUP)
|
self._import(path, imex_cmd=12)
|
||||||
|
|
||||||
def _import(self, path, imex_cmd):
|
def _import(self, path, imex_cmd):
|
||||||
with self.temp_plugin(ImexTracker()) as imex_tracker:
|
with ImexTracker(self) as imex_tracker:
|
||||||
self.imex(path, imex_cmd)
|
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
||||||
|
if not self._threads.is_started():
|
||||||
|
lib.dc_perform_imap_jobs(self._dc_context)
|
||||||
imex_tracker.wait_finish()
|
imex_tracker.wait_finish()
|
||||||
|
|
||||||
def imex(self, path, imex_cmd):
|
def initiate_key_transfer(self):
|
||||||
lib.dc_imex(self._dc_context, imex_cmd, as_dc_charpointer(path), ffi.NULL)
|
|
||||||
|
|
||||||
def initiate_key_transfer(self) -> str:
|
|
||||||
"""return setup code after a Autocrypt setup message
|
"""return setup code after a Autocrypt setup message
|
||||||
has been successfully sent to our own e-mail address ("self-sent message").
|
has been successfully sent to our own e-mail address ("self-sent message").
|
||||||
If sending out was unsuccessful, a RuntimeError is raised.
|
If sending out was unsuccessful, a RuntimeError is raised.
|
||||||
"""
|
"""
|
||||||
self.check_is_configured()
|
self.check_is_configured()
|
||||||
|
if not self._threads.is_started():
|
||||||
|
raise RuntimeError("threads not running, can not send out")
|
||||||
res = lib.dc_initiate_key_transfer(self._dc_context)
|
res = lib.dc_initiate_key_transfer(self._dc_context)
|
||||||
if res == ffi.NULL:
|
if res == ffi.NULL:
|
||||||
raise RuntimeError("could not send out autocrypt setup message")
|
raise RuntimeError("could not send out autocrypt setup message")
|
||||||
return from_dc_charpointer(res)
|
return from_dc_charpointer(res)
|
||||||
|
|
||||||
def get_setup_contact_qr(self) -> str:
|
def get_setup_contact_qr(self):
|
||||||
""" get/create Setup-Contact QR Code as ascii-string.
|
""" get/create Setup-Contact QR Code as ascii-string.
|
||||||
|
|
||||||
this string needs to be transferred to another DC account
|
this string needs to be transferred to another DC account
|
||||||
@@ -545,9 +473,47 @@ class Account(object):
|
|||||||
raise ValueError("could not join group")
|
raise ValueError("could not join group")
|
||||||
return Chat(self, chat_id)
|
return Chat(self, chat_id)
|
||||||
|
|
||||||
def set_location(
|
def stop_ongoing(self):
|
||||||
self, latitude: float = 0.0, longitude: float = 0.0, accuracy: float = 0.0
|
lib.dc_stop_ongoing_process(self._dc_context)
|
||||||
) -> None:
|
|
||||||
|
#
|
||||||
|
# meta API for start/stop and event based processing
|
||||||
|
#
|
||||||
|
|
||||||
|
def wait_next_incoming_message(self):
|
||||||
|
""" wait for and return next incoming message. """
|
||||||
|
ev = self._evlogger.get_matching("DC_EVENT_INCOMING_MSG")
|
||||||
|
return self.get_message_by_id(ev[2])
|
||||||
|
|
||||||
|
def start_threads(self, mvbox=False, sentbox=False):
|
||||||
|
""" start IMAP/SMTP threads (and configure account if it hasn't happened).
|
||||||
|
|
||||||
|
:raises: ValueError if 'addr' or 'mail_pw' are not configured.
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
if not self.is_configured():
|
||||||
|
self.configure()
|
||||||
|
self._threads.start(mvbox=mvbox, sentbox=sentbox)
|
||||||
|
|
||||||
|
def stop_threads(self, wait=True):
|
||||||
|
""" stop IMAP/SMTP threads. """
|
||||||
|
if self._threads.is_started():
|
||||||
|
self.stop_ongoing()
|
||||||
|
self._threads.stop(wait=wait)
|
||||||
|
|
||||||
|
def shutdown(self, wait=True):
|
||||||
|
""" stop threads and close and remove underlying dc_context and callbacks. """
|
||||||
|
if hasattr(self, "_dc_context") and hasattr(self, "_threads"):
|
||||||
|
# print("SHUTDOWN", self)
|
||||||
|
self.stop_threads(wait=False)
|
||||||
|
lib.dc_close(self._dc_context)
|
||||||
|
self.stop_threads(wait=wait) # to wait for threads
|
||||||
|
deltachat.clear_context_callback(self._dc_context)
|
||||||
|
del self._dc_context
|
||||||
|
atexit.unregister(self.shutdown)
|
||||||
|
self.pluggy.unregister(self._evlogger)
|
||||||
|
|
||||||
|
def set_location(self, latitude=0.0, longitude=0.0, accuracy=0.0):
|
||||||
"""set a new location. It effects all chats where we currently
|
"""set a new location. It effects all chats where we currently
|
||||||
have enabled location streaming.
|
have enabled location streaming.
|
||||||
|
|
||||||
@@ -561,144 +527,131 @@ class Account(object):
|
|||||||
if dc_res == 0:
|
if dc_res == 0:
|
||||||
raise ValueError("no chat is streaming locations")
|
raise ValueError("no chat is streaming locations")
|
||||||
|
|
||||||
#
|
|
||||||
# meta API for start/stop and event based processing
|
|
||||||
#
|
|
||||||
|
|
||||||
def add_account_plugin(self, plugin, name=None):
|
class ImexTracker:
|
||||||
""" add an account plugin which implements one or more of
|
def __init__(self, account):
|
||||||
the :class:`deltachat.hookspec.PerAccount` hooks.
|
self._imex_events = Queue()
|
||||||
"""
|
self.account = account
|
||||||
self._pm.register(plugin, name=name)
|
|
||||||
self._pm.check_pending()
|
|
||||||
return plugin
|
|
||||||
|
|
||||||
def remove_account_plugin(self, plugin, name=None):
|
def __enter__(self):
|
||||||
""" remove an account plugin. """
|
self.account.pluggy.register(self)
|
||||||
self._pm.unregister(plugin, name=name)
|
return self
|
||||||
|
|
||||||
@contextmanager
|
def __exit__(self, *args):
|
||||||
def temp_plugin(self, plugin):
|
self.account.pluggy.unregister(self)
|
||||||
""" run a with-block with the given plugin temporarily registered. """
|
|
||||||
self._pm.register(plugin)
|
|
||||||
yield plugin
|
|
||||||
self._pm.unregister(plugin)
|
|
||||||
|
|
||||||
def stop_ongoing(self):
|
@hookimpl
|
||||||
""" Stop ongoing securejoin, configuration or other core jobs. """
|
def process_low_level_event(self, account, event_name, data1, data2):
|
||||||
lib.dc_stop_ongoing_process(self._dc_context)
|
# there could be multiple accounts instantiated
|
||||||
|
if self.account is not account:
|
||||||
def get_connectivity(self):
|
|
||||||
return lib.dc_get_connectivity(self._dc_context)
|
|
||||||
|
|
||||||
def get_connectivity_html(self) -> str:
|
|
||||||
return from_dc_charpointer(lib.dc_get_connectivity_html(self._dc_context))
|
|
||||||
|
|
||||||
def all_work_done(self):
|
|
||||||
return lib.dc_all_work_done(self._dc_context)
|
|
||||||
|
|
||||||
def start_io(self):
|
|
||||||
""" start this account's IO scheduling (Rust-core async scheduler)
|
|
||||||
|
|
||||||
If this account is not configured an Exception is raised.
|
|
||||||
You need to call account.configure() and account.wait_configure_finish()
|
|
||||||
before.
|
|
||||||
|
|
||||||
You may call `stop_scheduler`, `wait_shutdown` or `shutdown` after the
|
|
||||||
account is started.
|
|
||||||
|
|
||||||
If you are using this from a test, you may want to call
|
|
||||||
wait_all_initial_fetches() afterwards.
|
|
||||||
|
|
||||||
:raises MissingCredentials: if `addr` and `mail_pw` values are not set.
|
|
||||||
:raises ConfigureFailed: if the account could not be configured.
|
|
||||||
|
|
||||||
:returns: None
|
|
||||||
"""
|
|
||||||
if not self.is_configured():
|
|
||||||
raise ValueError("account not configured, cannot start io")
|
|
||||||
lib.dc_start_io(self._dc_context)
|
|
||||||
|
|
||||||
def maybe_network(self):
|
|
||||||
"""This function should be called when there is a hint
|
|
||||||
that the network is available again,
|
|
||||||
e.g. as a response to system event reporting network availability.
|
|
||||||
The library will try to send pending messages out immediately.
|
|
||||||
|
|
||||||
Moreover, to have a reliable state
|
|
||||||
when the app comes to foreground with network available,
|
|
||||||
it may be reasonable to call the function also at that moment.
|
|
||||||
|
|
||||||
It is okay to call the function unconditionally when there is
|
|
||||||
network available, however, calling the function
|
|
||||||
_without_ having network may interfere with the backoff algorithm
|
|
||||||
and will led to let the jobs fail faster, with fewer retries
|
|
||||||
and may avoid messages being sent out.
|
|
||||||
|
|
||||||
Finally, if the context was created by the dc_accounts_t account manager
|
|
||||||
(currently not implemented in the Python bindings),
|
|
||||||
use dc_accounts_maybe_network() instead of this function
|
|
||||||
"""
|
|
||||||
lib.dc_maybe_network(self._dc_context)
|
|
||||||
|
|
||||||
def configure(self, reconfigure: bool = False) -> ConfigureTracker:
|
|
||||||
""" Start configuration process and return a Configtracker instance
|
|
||||||
on which you can block with wait_finish() to get a True/False success
|
|
||||||
value for the configuration process.
|
|
||||||
"""
|
|
||||||
assert self.is_configured() == reconfigure
|
|
||||||
if not self.get_config("addr") or not self.get_config("mail_pw"):
|
|
||||||
raise MissingCredentials("addr or mail_pwd not set in config")
|
|
||||||
configtracker = ConfigureTracker(self)
|
|
||||||
self.add_account_plugin(configtracker)
|
|
||||||
lib.dc_configure(self._dc_context)
|
|
||||||
return configtracker
|
|
||||||
|
|
||||||
def wait_shutdown(self) -> None:
|
|
||||||
""" wait until shutdown of this account has completed. """
|
|
||||||
self._shutdown_event.wait()
|
|
||||||
|
|
||||||
def stop_io(self) -> None:
|
|
||||||
""" stop core IO scheduler if it is running. """
|
|
||||||
self.log("stop_ongoing")
|
|
||||||
self.stop_ongoing()
|
|
||||||
|
|
||||||
self.log("dc_stop_io (stop core IO scheduler)")
|
|
||||||
lib.dc_stop_io(self._dc_context)
|
|
||||||
|
|
||||||
def shutdown(self) -> None:
|
|
||||||
""" shutdown and destroy account (stop callback thread, close and remove
|
|
||||||
underlying dc_context)."""
|
|
||||||
if self._dc_context is None:
|
|
||||||
return
|
return
|
||||||
|
if event_name == "DC_EVENT_IMEX_PROGRESS":
|
||||||
|
self._imex_events.put(data1)
|
||||||
|
elif event_name == "DC_EVENT_IMEX_FILE_WRITTEN":
|
||||||
|
self._imex_events.put(data1)
|
||||||
|
|
||||||
self.stop_io()
|
def wait_finish(self, progress_timeout=60):
|
||||||
|
""" Return list of written files, raise ValueError if ExportFailed. """
|
||||||
|
files_written = []
|
||||||
|
while True:
|
||||||
|
ev = self._imex_events.get(timeout=progress_timeout)
|
||||||
|
if isinstance(ev, str):
|
||||||
|
files_written.append(ev)
|
||||||
|
elif ev == 0:
|
||||||
|
raise ValueError("export failed, exp-files: {}".format(files_written))
|
||||||
|
elif ev == 1000:
|
||||||
|
return files_written
|
||||||
|
|
||||||
self.log("remove dc_context references")
|
|
||||||
|
|
||||||
# if _dc_context is unref'ed the event thread should quickly
|
class IOThreads:
|
||||||
# receive the termination signal. However, some python code might
|
def __init__(self, dc_context, log_event=lambda *args: None):
|
||||||
# still hold a reference and so we use a secondary signal
|
self._dc_context = dc_context
|
||||||
# to make sure the even thread terminates if it receives any new
|
self._thread_quitflag = False
|
||||||
# event, indepedently from waiting for the core to send NULL to
|
self._name2thread = {}
|
||||||
# get_next_event().
|
self._log_event = log_event
|
||||||
self._event_thread.mark_shutdown()
|
|
||||||
self._dc_context = None
|
|
||||||
|
|
||||||
self.log("wait for event thread to finish")
|
def is_started(self):
|
||||||
try:
|
return len(self._name2thread) > 0
|
||||||
self._event_thread.wait(timeout=2)
|
|
||||||
except RuntimeError as e:
|
|
||||||
self.log("Waiting for event thread failed: {}".format(e))
|
|
||||||
|
|
||||||
if self._event_thread.is_alive():
|
def start(self, imap=True, smtp=True, mvbox=False, sentbox=False):
|
||||||
self.log("WARN: event thread did not terminate yet, ignoring.")
|
assert not self.is_started()
|
||||||
|
if imap:
|
||||||
|
self._start_one_thread("inbox", self.imap_thread_run)
|
||||||
|
if mvbox:
|
||||||
|
self._start_one_thread("mvbox", self.mvbox_thread_run)
|
||||||
|
if sentbox:
|
||||||
|
self._start_one_thread("sentbox", self.sentbox_thread_run)
|
||||||
|
if smtp:
|
||||||
|
self._start_one_thread("smtp", self.smtp_thread_run)
|
||||||
|
|
||||||
self._shutdown_event.set()
|
def _start_one_thread(self, name, func):
|
||||||
|
self._name2thread[name] = t = threading.Thread(target=func, name=name)
|
||||||
|
t.setDaemon(1)
|
||||||
|
t.start()
|
||||||
|
|
||||||
hook = hookspec.Global._get_plugin_manager().hook
|
def stop(self, wait=False):
|
||||||
hook.dc_account_after_shutdown(account=self)
|
self._thread_quitflag = True
|
||||||
self.log("shutdown finished")
|
|
||||||
|
# Workaround for a race condition. Make sure that thread is
|
||||||
|
# not in between checking for quitflag and entering idle.
|
||||||
|
time.sleep(0.5)
|
||||||
|
|
||||||
|
lib.dc_interrupt_imap_idle(self._dc_context)
|
||||||
|
lib.dc_interrupt_smtp_idle(self._dc_context)
|
||||||
|
lib.dc_interrupt_mvbox_idle(self._dc_context)
|
||||||
|
lib.dc_interrupt_sentbox_idle(self._dc_context)
|
||||||
|
if wait:
|
||||||
|
for name, thread in self._name2thread.items():
|
||||||
|
thread.join()
|
||||||
|
|
||||||
|
def imap_thread_run(self):
|
||||||
|
self._log_event("py-bindings-info", 0, "INBOX THREAD START")
|
||||||
|
while not self._thread_quitflag:
|
||||||
|
lib.dc_perform_imap_jobs(self._dc_context)
|
||||||
|
if not self._thread_quitflag:
|
||||||
|
lib.dc_perform_imap_fetch(self._dc_context)
|
||||||
|
if not self._thread_quitflag:
|
||||||
|
lib.dc_perform_imap_idle(self._dc_context)
|
||||||
|
self._log_event("py-bindings-info", 0, "INBOX THREAD FINISHED")
|
||||||
|
|
||||||
|
def mvbox_thread_run(self):
|
||||||
|
self._log_event("py-bindings-info", 0, "MVBOX THREAD START")
|
||||||
|
while not self._thread_quitflag:
|
||||||
|
lib.dc_perform_mvbox_jobs(self._dc_context)
|
||||||
|
if not self._thread_quitflag:
|
||||||
|
lib.dc_perform_mvbox_fetch(self._dc_context)
|
||||||
|
if not self._thread_quitflag:
|
||||||
|
lib.dc_perform_mvbox_idle(self._dc_context)
|
||||||
|
self._log_event("py-bindings-info", 0, "MVBOX THREAD FINISHED")
|
||||||
|
|
||||||
|
def sentbox_thread_run(self):
|
||||||
|
self._log_event("py-bindings-info", 0, "SENTBOX THREAD START")
|
||||||
|
while not self._thread_quitflag:
|
||||||
|
lib.dc_perform_sentbox_jobs(self._dc_context)
|
||||||
|
if not self._thread_quitflag:
|
||||||
|
lib.dc_perform_sentbox_fetch(self._dc_context)
|
||||||
|
if not self._thread_quitflag:
|
||||||
|
lib.dc_perform_sentbox_idle(self._dc_context)
|
||||||
|
self._log_event("py-bindings-info", 0, "SENTBOX THREAD FINISHED")
|
||||||
|
|
||||||
|
def smtp_thread_run(self):
|
||||||
|
self._log_event("py-bindings-info", 0, "SMTP THREAD START")
|
||||||
|
while not self._thread_quitflag:
|
||||||
|
lib.dc_perform_smtp_jobs(self._dc_context)
|
||||||
|
if not self._thread_quitflag:
|
||||||
|
lib.dc_perform_smtp_idle(self._dc_context)
|
||||||
|
self._log_event("py-bindings-info", 0, "SMTP THREAD FINISHED")
|
||||||
|
|
||||||
|
|
||||||
|
def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref):
|
||||||
|
# destructor for dc_context
|
||||||
|
dc_context_unref(dc_context)
|
||||||
|
try:
|
||||||
|
deltachat.clear_context_callback(dc_context)
|
||||||
|
except (TypeError, AttributeError):
|
||||||
|
# we are deep into Python Interpreter shutdown,
|
||||||
|
# so no need to clear the callback context mapping.
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class ScannedQRCode:
|
class ScannedQRCode:
|
||||||
|
|||||||
@@ -3,13 +3,12 @@
|
|||||||
import mimetypes
|
import mimetypes
|
||||||
import calendar
|
import calendar
|
||||||
import json
|
import json
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime
|
||||||
import os
|
import os
|
||||||
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array
|
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
|
||||||
from .capi import lib, ffi
|
from .capi import lib, ffi
|
||||||
from . import const
|
from . import const
|
||||||
from .message import Message
|
from .message import Message
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
|
|
||||||
class Chat(object):
|
class Chat(object):
|
||||||
@@ -18,30 +17,29 @@ class Chat(object):
|
|||||||
You obtain instances of it through :class:`deltachat.account.Account`.
|
You obtain instances of it through :class:`deltachat.account.Account`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, account, id) -> None:
|
def __init__(self, account, id):
|
||||||
from .account import Account
|
|
||||||
assert isinstance(account, Account), repr(account)
|
|
||||||
self.account = account
|
self.account = account
|
||||||
|
self._dc_context = account._dc_context
|
||||||
self.id = id
|
self.id = id
|
||||||
|
|
||||||
def __eq__(self, other) -> bool:
|
def __eq__(self, other):
|
||||||
return self.id == getattr(other, "id", None) and \
|
return self.id == getattr(other, "id", None) and \
|
||||||
self.account._dc_context == other.account._dc_context
|
self._dc_context == getattr(other, "_dc_context", None)
|
||||||
|
|
||||||
def __ne__(self, other) -> bool:
|
def __ne__(self, other):
|
||||||
return not (self == other)
|
return not (self == other)
|
||||||
|
|
||||||
def __repr__(self) -> str:
|
def __repr__(self):
|
||||||
return "<Chat id={} name={}>".format(self.id, self.get_name())
|
return "<Chat id={} name={} dc_context={}>".format(self.id, self.get_name(), self._dc_context)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _dc_chat(self):
|
def _dc_chat(self):
|
||||||
return ffi.gc(
|
return ffi.gc(
|
||||||
lib.dc_get_chat(self.account._dc_context, self.id),
|
lib.dc_get_chat(self._dc_context, self.id),
|
||||||
lib.dc_chat_unref
|
lib.dc_chat_unref
|
||||||
)
|
)
|
||||||
|
|
||||||
def delete(self) -> None:
|
def delete(self):
|
||||||
"""Delete this chat and all its messages.
|
"""Delete this chat and all its messages.
|
||||||
|
|
||||||
Note:
|
Note:
|
||||||
@@ -49,39 +47,24 @@ class Chat(object):
|
|||||||
- 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
|
||||||
"""
|
"""
|
||||||
lib.dc_delete_chat(self.account._dc_context, self.id)
|
lib.dc_delete_chat(self._dc_context, self.id)
|
||||||
|
|
||||||
def block(self) -> None:
|
|
||||||
"""Block this chat."""
|
|
||||||
lib.dc_block_chat(self.account._dc_context, self.id)
|
|
||||||
|
|
||||||
def accept(self) -> None:
|
|
||||||
"""Accept this contact request chat."""
|
|
||||||
lib.dc_accept_chat(self.account._dc_context, self.id)
|
|
||||||
|
|
||||||
# ------ chat status/metadata API ------------------------------
|
# ------ chat status/metadata API ------------------------------
|
||||||
|
|
||||||
def is_group(self) -> bool:
|
def is_deaddrop(self):
|
||||||
""" return true if this chat is a group chat.
|
""" return true if this chat is a deaddrop chat.
|
||||||
|
|
||||||
:returns: True if chat is a group-chat, false if it's a contact 1:1 chat.
|
:returns: True if chat is the deaddrop chat, False otherwise.
|
||||||
"""
|
"""
|
||||||
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
|
return self.id == const.DC_CHAT_ID_DEADDROP
|
||||||
|
|
||||||
def is_muted(self) -> bool:
|
def is_muted(self):
|
||||||
""" return true if this chat is muted.
|
""" return true if this chat is muted.
|
||||||
|
|
||||||
:returns: True if chat is muted, False otherwise.
|
:returns: True if chat is muted, False otherwise.
|
||||||
"""
|
"""
|
||||||
return lib.dc_chat_is_muted(self._dc_chat)
|
return lib.dc_chat_is_muted(self._dc_chat)
|
||||||
|
|
||||||
def is_contact_request(self):
|
|
||||||
""" return True if this chat is a contact request chat.
|
|
||||||
|
|
||||||
:returns: True if chat is a contact request chat, False otherwise.
|
|
||||||
"""
|
|
||||||
return lib.dc_chat_is_contact_request(self._dc_chat)
|
|
||||||
|
|
||||||
def is_promoted(self):
|
def is_promoted(self):
|
||||||
""" return True if this chat is promoted, i.e.
|
""" return True if this chat is promoted, i.e.
|
||||||
the member contacts are aware of their membership,
|
the member contacts are aware of their membership,
|
||||||
@@ -91,38 +74,30 @@ class Chat(object):
|
|||||||
"""
|
"""
|
||||||
return not lib.dc_chat_is_unpromoted(self._dc_chat)
|
return not lib.dc_chat_is_unpromoted(self._dc_chat)
|
||||||
|
|
||||||
def can_send(self) -> bool:
|
def is_verified(self):
|
||||||
"""Check if messages can be sent to a give chat.
|
""" return True if this chat is a verified group.
|
||||||
This is not true eg. for the contact requests or for the device-talk
|
|
||||||
|
|
||||||
:returns: True if the chat is writable, False otherwise
|
:returns: True if chat is verified, False otherwise.
|
||||||
"""
|
"""
|
||||||
return lib.dc_chat_can_send(self._dc_chat)
|
return lib.dc_chat_is_verified(self._dc_chat)
|
||||||
|
|
||||||
def is_protected(self) -> bool:
|
def get_name(self):
|
||||||
""" return True if this chat is a protected chat.
|
|
||||||
|
|
||||||
:returns: True if chat is protected, False otherwise.
|
|
||||||
"""
|
|
||||||
return lib.dc_chat_is_protected(self._dc_chat)
|
|
||||||
|
|
||||||
def get_name(self) -> Optional[str]:
|
|
||||||
""" return name of this chat.
|
""" return name of this chat.
|
||||||
|
|
||||||
:returns: unicode name
|
:returns: unicode name
|
||||||
"""
|
"""
|
||||||
return from_dc_charpointer(lib.dc_chat_get_name(self._dc_chat))
|
return from_dc_charpointer(lib.dc_chat_get_name(self._dc_chat))
|
||||||
|
|
||||||
def set_name(self, name: str) -> bool:
|
def set_name(self, name):
|
||||||
""" set name of this chat.
|
""" set name of this chat.
|
||||||
|
|
||||||
:param name: as a unicode string.
|
:param name: as a unicode string.
|
||||||
:returns: True on success, False otherwise
|
:returns: None
|
||||||
"""
|
"""
|
||||||
name = as_dc_charpointer(name)
|
name = as_dc_charpointer(name)
|
||||||
return bool(lib.dc_set_chat_name(self.account._dc_context, self.id, name))
|
return lib.dc_set_chat_name(self._dc_context, self.id, name)
|
||||||
|
|
||||||
def mute(self, duration: Optional[int] = None) -> None:
|
def mute(self, duration=None):
|
||||||
""" mutes the chat
|
""" mutes the chat
|
||||||
|
|
||||||
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
|
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
|
||||||
@@ -132,70 +107,47 @@ class Chat(object):
|
|||||||
mute_duration = -1
|
mute_duration = -1
|
||||||
else:
|
else:
|
||||||
mute_duration = duration
|
mute_duration = duration
|
||||||
ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, mute_duration)
|
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, mute_duration)
|
||||||
if not bool(ret):
|
if not bool(ret):
|
||||||
raise ValueError("Call to dc_set_chat_mute_duration failed")
|
raise ValueError("Call to dc_set_chat_mute_duration failed")
|
||||||
|
|
||||||
def unmute(self) -> None:
|
def unmute(self):
|
||||||
""" unmutes the chat
|
""" unmutes the chat
|
||||||
|
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, 0)
|
ret = lib.dc_set_chat_mute_duration(self._dc_context, self.id, 0)
|
||||||
if not bool(ret):
|
if not bool(ret):
|
||||||
raise ValueError("Failed to unmute chat")
|
raise ValueError("Failed to unmute chat")
|
||||||
|
|
||||||
def get_mute_duration(self) -> int:
|
def get_mute_duration(self):
|
||||||
""" Returns the number of seconds until the mute of this chat is lifted.
|
""" Returns the number of seconds until the mute of this chat is lifted.
|
||||||
|
|
||||||
:param duration:
|
:param duration:
|
||||||
:returns: Returns the number of seconds the chat is still muted for. (0 for not muted, -1 forever muted)
|
:returns: Returns the number of seconds the chat is still muted for. (0 for not muted, -1 forever muted)
|
||||||
"""
|
"""
|
||||||
return lib.dc_chat_get_remaining_mute_duration(self._dc_chat)
|
return bool(lib.dc_chat_get_remaining_mute_duration(self.id))
|
||||||
|
|
||||||
def get_ephemeral_timer(self) -> int:
|
def get_type(self):
|
||||||
""" get ephemeral timer.
|
""" return type of this chat.
|
||||||
|
|
||||||
:returns: ephemeral timer value in seconds
|
|
||||||
"""
|
|
||||||
return lib.dc_get_chat_ephemeral_timer(self.account._dc_context, self.id)
|
|
||||||
|
|
||||||
def set_ephemeral_timer(self, timer: int) -> bool:
|
|
||||||
""" set ephemeral timer.
|
|
||||||
|
|
||||||
:param: timer value in seconds
|
|
||||||
|
|
||||||
:returns: True on success, False otherwise
|
|
||||||
"""
|
|
||||||
return bool(lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer))
|
|
||||||
|
|
||||||
def get_type(self) -> int:
|
|
||||||
""" (deprecated) return type of this chat.
|
|
||||||
|
|
||||||
:returns: one of const.DC_CHAT_TYPE_*
|
:returns: one of const.DC_CHAT_TYPE_*
|
||||||
"""
|
"""
|
||||||
return lib.dc_chat_get_type(self._dc_chat)
|
return lib.dc_chat_get_type(self._dc_chat)
|
||||||
|
|
||||||
def get_encryption_info(self) -> Optional[str]:
|
def get_join_qr(self):
|
||||||
"""Return encryption info for this chat.
|
|
||||||
|
|
||||||
:returns: a string with encryption preferences of all chat members"""
|
|
||||||
res = lib.dc_get_chat_encrinfo(self.account._dc_context, self.id)
|
|
||||||
return from_dc_charpointer(res)
|
|
||||||
|
|
||||||
def get_join_qr(self) -> Optional[str]:
|
|
||||||
""" get/create Join-Group QR Code as ascii-string.
|
""" get/create Join-Group QR Code as ascii-string.
|
||||||
|
|
||||||
this string needs to be transferred to another DC account
|
this string needs to be transferred to another DC 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)
|
||||||
where account.join_with_qrcode(qr) needs to be called.
|
where account.join_with_qrcode(qr) needs to be called.
|
||||||
"""
|
"""
|
||||||
res = lib.dc_get_securejoin_qr(self.account._dc_context, self.id)
|
res = lib.dc_get_securejoin_qr(self._dc_context, self.id)
|
||||||
return from_dc_charpointer(res)
|
return from_dc_charpointer(res)
|
||||||
|
|
||||||
# ------ chat messaging API ------------------------------
|
# ------ chat messaging API ------------------------------
|
||||||
|
|
||||||
def send_msg(self, msg: Message) -> Message:
|
def send_msg(self, msg):
|
||||||
"""send a message by using a ready Message object.
|
"""send a message by using a ready Message object.
|
||||||
|
|
||||||
:param msg: a :class:`deltachat.message.Message` instance
|
:param msg: a :class:`deltachat.message.Message` instance
|
||||||
@@ -212,7 +164,7 @@ class Chat(object):
|
|||||||
assert msg.id != 0
|
assert msg.id != 0
|
||||||
# get a fresh copy of dc_msg, the core needs it
|
# get a fresh copy of dc_msg, the core needs it
|
||||||
msg = Message.from_db(self.account, msg.id)
|
msg = Message.from_db(self.account, msg.id)
|
||||||
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
|
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
|
||||||
if sent_id == 0:
|
if sent_id == 0:
|
||||||
raise ValueError("message could not be sent")
|
raise ValueError("message could not be sent")
|
||||||
# modify message in place to avoid bad state for the caller
|
# modify message in place to avoid bad state for the caller
|
||||||
@@ -227,7 +179,7 @@ class Chat(object):
|
|||||||
:returns: the resulting :class:`deltachat.message.Message` instance
|
:returns: the resulting :class:`deltachat.message.Message` instance
|
||||||
"""
|
"""
|
||||||
msg = as_dc_charpointer(text)
|
msg = as_dc_charpointer(text)
|
||||||
msg_id = lib.dc_send_text_msg(self.account._dc_context, self.id, msg)
|
msg_id = lib.dc_send_text_msg(self._dc_context, self.id, msg)
|
||||||
if msg_id == 0:
|
if msg_id == 0:
|
||||||
raise ValueError("message could not be send, does chat exist?")
|
raise ValueError("message could not be send, does chat exist?")
|
||||||
return Message.from_db(self.account, msg_id)
|
return Message.from_db(self.account, msg_id)
|
||||||
@@ -242,7 +194,7 @@ class Chat(object):
|
|||||||
"""
|
"""
|
||||||
msg = Message.new_empty(self.account, view_type="file")
|
msg = Message.new_empty(self.account, view_type="file")
|
||||||
msg.set_file(path, mime_type)
|
msg.set_file(path, mime_type)
|
||||||
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
|
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
|
||||||
if sent_id == 0:
|
if sent_id == 0:
|
||||||
raise ValueError("message could not be sent")
|
raise ValueError("message could not be sent")
|
||||||
return Message.from_db(self.account, sent_id)
|
return Message.from_db(self.account, sent_id)
|
||||||
@@ -257,25 +209,23 @@ class Chat(object):
|
|||||||
mime_type = mimetypes.guess_type(path)[0]
|
mime_type = mimetypes.guess_type(path)[0]
|
||||||
msg = Message.new_empty(self.account, view_type="image")
|
msg = Message.new_empty(self.account, view_type="image")
|
||||||
msg.set_file(path, mime_type)
|
msg.set_file(path, mime_type)
|
||||||
sent_id = lib.dc_send_msg(self.account._dc_context, self.id, msg._dc_msg)
|
sent_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
|
||||||
if sent_id == 0:
|
if sent_id == 0:
|
||||||
raise ValueError("message could not be sent")
|
raise ValueError("message could not be sent")
|
||||||
return Message.from_db(self.account, sent_id)
|
return Message.from_db(self.account, sent_id)
|
||||||
|
|
||||||
def prepare_message(self, msg):
|
def prepare_message(self, msg):
|
||||||
""" prepare a message for sending.
|
""" create a new prepared message.
|
||||||
|
|
||||||
:param msg: the message to be prepared.
|
:param msg: the message to be prepared.
|
||||||
:returns: a :class:`deltachat.message.Message` instance.
|
:returns: :class:`deltachat.message.Message` instance.
|
||||||
This is the same object that was passed in, which
|
|
||||||
has been modified with the new state of the core.
|
|
||||||
"""
|
"""
|
||||||
msg_id = lib.dc_prepare_msg(self.account._dc_context, self.id, msg._dc_msg)
|
msg_id = lib.dc_prepare_msg(self._dc_context, self.id, msg._dc_msg)
|
||||||
if msg_id == 0:
|
if msg_id == 0:
|
||||||
raise ValueError("message could not be prepared")
|
raise ValueError("message could not be prepared")
|
||||||
# modify message in place to avoid bad state for the caller
|
# invalidate passed in message which is not safe to use anymore
|
||||||
msg._dc_msg = Message.from_db(self.account, msg_id)._dc_msg
|
msg._dc_msg = msg.id = None
|
||||||
return msg
|
return Message.from_db(self.account, msg_id)
|
||||||
|
|
||||||
def prepare_message_file(self, path, mime_type=None, view_type="file"):
|
def prepare_message_file(self, path, mime_type=None, view_type="file"):
|
||||||
""" prepare a message for sending and return the resulting Message instance.
|
""" prepare a message for sending and return the resulting Message instance.
|
||||||
@@ -306,7 +256,7 @@ class Chat(object):
|
|||||||
msg = Message.from_db(self.account, message.id)
|
msg = Message.from_db(self.account, message.id)
|
||||||
|
|
||||||
# pass 0 as chat-id because core-docs say it's ok when out-preparing
|
# pass 0 as chat-id because core-docs say it's ok when out-preparing
|
||||||
sent_id = lib.dc_send_msg(self.account._dc_context, 0, msg._dc_msg)
|
sent_id = lib.dc_send_msg(self._dc_context, 0, msg._dc_msg)
|
||||||
if sent_id == 0:
|
if sent_id == 0:
|
||||||
raise ValueError("message could not be sent")
|
raise ValueError("message could not be sent")
|
||||||
assert sent_id == msg.id
|
assert sent_id == msg.id
|
||||||
@@ -320,9 +270,9 @@ class Chat(object):
|
|||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
if message is None:
|
if message is None:
|
||||||
lib.dc_set_draft(self.account._dc_context, self.id, ffi.NULL)
|
lib.dc_set_draft(self._dc_context, self.id, ffi.NULL)
|
||||||
else:
|
else:
|
||||||
lib.dc_set_draft(self.account._dc_context, self.id, message._dc_msg)
|
lib.dc_set_draft(self._dc_context, self.id, message._dc_msg)
|
||||||
|
|
||||||
def get_draft(self):
|
def get_draft(self):
|
||||||
""" get draft message for this chat.
|
""" get draft message for this chat.
|
||||||
@@ -330,7 +280,7 @@ class Chat(object):
|
|||||||
:param message: a :class:`Message` instance
|
:param message: a :class:`Message` instance
|
||||||
:returns: Message object or None (if no draft available)
|
:returns: Message object or None (if no draft available)
|
||||||
"""
|
"""
|
||||||
x = lib.dc_get_draft(self.account._dc_context, self.id)
|
x = lib.dc_get_draft(self._dc_context, self.id)
|
||||||
if x == ffi.NULL:
|
if x == ffi.NULL:
|
||||||
return None
|
return None
|
||||||
dc_msg = ffi.gc(x, lib.dc_msg_unref)
|
dc_msg = ffi.gc(x, lib.dc_msg_unref)
|
||||||
@@ -342,7 +292,7 @@ class Chat(object):
|
|||||||
:returns: list of :class:`deltachat.message.Message` objects for this chat.
|
:returns: list of :class:`deltachat.message.Message` objects for this chat.
|
||||||
"""
|
"""
|
||||||
dc_array = ffi.gc(
|
dc_array = ffi.gc(
|
||||||
lib.dc_get_chat_msgs(self.account._dc_context, self.id, 0, 0),
|
lib.dc_get_chat_msgs(self._dc_context, self.id, 0, 0),
|
||||||
lib.dc_array_unref
|
lib.dc_array_unref
|
||||||
)
|
)
|
||||||
return list(iter_array(dc_array, lambda x: Message.from_db(self.account, x)))
|
return list(iter_array(dc_array, lambda x: Message.from_db(self.account, x)))
|
||||||
@@ -352,69 +302,60 @@ class Chat(object):
|
|||||||
|
|
||||||
:returns: number of fresh messages
|
:returns: number of fresh messages
|
||||||
"""
|
"""
|
||||||
return lib.dc_get_fresh_msg_cnt(self.account._dc_context, self.id)
|
return lib.dc_get_fresh_msg_cnt(self._dc_context, self.id)
|
||||||
|
|
||||||
def mark_noticed(self):
|
def mark_noticed(self):
|
||||||
""" mark all messages in this chat as noticed.
|
""" mark all messages in this chat as noticed.
|
||||||
|
|
||||||
Noticed messages are no longer fresh.
|
Noticed messages are no longer fresh.
|
||||||
"""
|
"""
|
||||||
return lib.dc_marknoticed_chat(self.account._dc_context, self.id)
|
return lib.dc_marknoticed_chat(self._dc_context, self.id)
|
||||||
|
|
||||||
def get_summary(self):
|
def get_summary(self):
|
||||||
""" return dictionary with summary information. """
|
""" return dictionary with summary information. """
|
||||||
dc_res = lib.dc_chat_get_info_json(self.account._dc_context, self.id)
|
dc_res = lib.dc_chat_get_info_json(self._dc_context, self.id)
|
||||||
s = from_dc_charpointer(dc_res)
|
s = from_dc_charpointer(dc_res)
|
||||||
return json.loads(s)
|
return json.loads(s)
|
||||||
|
|
||||||
# ------ group management API ------------------------------
|
# ------ group management API ------------------------------
|
||||||
|
|
||||||
def add_contact(self, obj):
|
def add_contact(self, contact):
|
||||||
""" add a contact to this chat.
|
""" add a contact to this chat.
|
||||||
|
|
||||||
:params obj: Contact, Account or e-mail address.
|
:params: contact object.
|
||||||
:raises ValueError: if contact could not be added
|
:raises ValueError: if contact could not be added
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
contact = self.account.create_contact(obj)
|
ret = lib.dc_add_contact_to_chat(self._dc_context, self.id, contact.id)
|
||||||
ret = lib.dc_add_contact_to_chat(self.account._dc_context, self.id, contact.id)
|
|
||||||
if ret != 1:
|
if ret != 1:
|
||||||
raise ValueError("could not add contact {!r} to chat".format(contact))
|
raise ValueError("could not add contact {!r} to chat".format(contact))
|
||||||
return contact
|
|
||||||
|
|
||||||
def remove_contact(self, obj):
|
def remove_contact(self, contact):
|
||||||
""" remove a contact from this chat.
|
""" remove a contact from this chat.
|
||||||
|
|
||||||
:params obj: Contact, Account or e-mail address.
|
:params: contact object.
|
||||||
:raises ValueError: if contact could not be removed
|
:raises ValueError: if contact could not be removed
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
contact = self.account.get_contact(obj)
|
ret = lib.dc_remove_contact_from_chat(self._dc_context, self.id, contact.id)
|
||||||
ret = lib.dc_remove_contact_from_chat(self.account._dc_context, self.id, contact.id)
|
|
||||||
if ret != 1:
|
if ret != 1:
|
||||||
raise ValueError("could not remove contact {!r} from chat".format(contact))
|
raise ValueError("could not remove contact {!r} from chat".format(contact))
|
||||||
|
|
||||||
def get_contacts(self):
|
def get_contacts(self):
|
||||||
""" get all contacts for this chat.
|
""" get all contacts for this chat.
|
||||||
|
:params: contact object.
|
||||||
:returns: list of :class:`deltachat.contact.Contact` objects for this chat
|
:returns: list of :class:`deltachat.contact.Contact` objects for this chat
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from .contact import Contact
|
from .contact import Contact
|
||||||
dc_array = ffi.gc(
|
dc_array = ffi.gc(
|
||||||
lib.dc_get_chat_contacts(self.account._dc_context, self.id),
|
lib.dc_get_chat_contacts(self._dc_context, self.id),
|
||||||
lib.dc_array_unref
|
lib.dc_array_unref
|
||||||
)
|
)
|
||||||
return list(iter_array(
|
return list(iter_array(
|
||||||
dc_array, lambda id: Contact(self.account, id))
|
dc_array, lambda id: Contact(self._dc_context, id))
|
||||||
)
|
)
|
||||||
|
|
||||||
def num_contacts(self):
|
|
||||||
""" return number of contacts in this chat. """
|
|
||||||
dc_array = ffi.gc(
|
|
||||||
lib.dc_get_chat_contacts(self.account._dc_context, self.id),
|
|
||||||
lib.dc_array_unref
|
|
||||||
)
|
|
||||||
return lib.dc_array_get_cnt(dc_array)
|
|
||||||
|
|
||||||
def set_profile_image(self, img_path):
|
def set_profile_image(self, img_path):
|
||||||
"""Set group profile image.
|
"""Set group profile image.
|
||||||
|
|
||||||
@@ -427,7 +368,7 @@ class Chat(object):
|
|||||||
"""
|
"""
|
||||||
assert os.path.exists(img_path), img_path
|
assert os.path.exists(img_path), img_path
|
||||||
p = as_dc_charpointer(img_path)
|
p = as_dc_charpointer(img_path)
|
||||||
res = lib.dc_set_chat_profile_image(self.account._dc_context, self.id, p)
|
res = lib.dc_set_chat_profile_image(self._dc_context, self.id, p)
|
||||||
if res != 1:
|
if res != 1:
|
||||||
raise ValueError("Setting Profile Image {!r} failed".format(p))
|
raise ValueError("Setting Profile Image {!r} failed".format(p))
|
||||||
|
|
||||||
@@ -440,7 +381,7 @@ class Chat(object):
|
|||||||
:raises ValueError: if profile image could not be reset
|
:raises ValueError: if profile image could not be reset
|
||||||
:returns: None
|
:returns: None
|
||||||
"""
|
"""
|
||||||
res = lib.dc_set_chat_profile_image(self.account._dc_context, self.id, ffi.NULL)
|
res = lib.dc_set_chat_profile_image(self._dc_context, self.id, ffi.NULL)
|
||||||
if res != 1:
|
if res != 1:
|
||||||
raise ValueError("Removing Profile Image failed")
|
raise ValueError("Removing Profile Image failed")
|
||||||
|
|
||||||
@@ -464,13 +405,19 @@ class Chat(object):
|
|||||||
"""
|
"""
|
||||||
return lib.dc_chat_get_color(self._dc_chat)
|
return lib.dc_chat_get_color(self._dc_chat)
|
||||||
|
|
||||||
|
def get_subtitle(self):
|
||||||
|
"""return the subtitle of the chat
|
||||||
|
:returns: the subtitle
|
||||||
|
"""
|
||||||
|
return from_dc_charpointer(lib.dc_chat_get_subtitle(self._dc_chat))
|
||||||
|
|
||||||
# ------ location streaming API ------------------------------
|
# ------ location streaming API ------------------------------
|
||||||
|
|
||||||
def is_sending_locations(self):
|
def is_sending_locations(self):
|
||||||
"""return True if this chat has location-sending enabled currently.
|
"""return True if this chat has location-sending enabled currently.
|
||||||
:returns: True if location sending is enabled.
|
:returns: True if location sending is enabled.
|
||||||
"""
|
"""
|
||||||
return lib.dc_is_sending_locations_to_chat(self.account._dc_context, self.id)
|
return lib.dc_is_sending_locations_to_chat(self._dc_context, self.id)
|
||||||
|
|
||||||
def is_archived(self):
|
def is_archived(self):
|
||||||
"""return True if this chat is archived.
|
"""return True if this chat is archived.
|
||||||
@@ -483,7 +430,7 @@ class Chat(object):
|
|||||||
|
|
||||||
all subsequent messages will carry a location with them.
|
all subsequent messages will carry a location with them.
|
||||||
"""
|
"""
|
||||||
lib.dc_send_locations_to_chat(self.account._dc_context, self.id, seconds)
|
lib.dc_send_locations_to_chat(self._dc_context, self.id, seconds)
|
||||||
|
|
||||||
def get_locations(self, contact=None, timestamp_from=None, timestamp_to=None):
|
def get_locations(self, contact=None, timestamp_from=None, timestamp_to=None):
|
||||||
"""return list of locations for the given contact in the given timespan.
|
"""return list of locations for the given contact in the given timespan.
|
||||||
@@ -507,30 +454,24 @@ class Chat(object):
|
|||||||
else:
|
else:
|
||||||
contact_id = contact.id
|
contact_id = contact.id
|
||||||
|
|
||||||
dc_array = lib.dc_get_locations(self.account._dc_context, self.id, contact_id, time_from, time_to)
|
dc_array = lib.dc_get_locations(self._dc_context, self.id, contact_id, time_from, time_to)
|
||||||
return [
|
return [
|
||||||
Location(
|
Location(
|
||||||
latitude=lib.dc_array_get_latitude(dc_array, i),
|
latitude=lib.dc_array_get_latitude(dc_array, i),
|
||||||
longitude=lib.dc_array_get_longitude(dc_array, i),
|
longitude=lib.dc_array_get_longitude(dc_array, i),
|
||||||
accuracy=lib.dc_array_get_accuracy(dc_array, i),
|
accuracy=lib.dc_array_get_accuracy(dc_array, i),
|
||||||
timestamp=datetime.fromtimestamp(
|
timestamp=datetime.utcfromtimestamp(lib.dc_array_get_timestamp(dc_array, i)))
|
||||||
lib.dc_array_get_timestamp(dc_array, i),
|
|
||||||
timezone.utc
|
|
||||||
),
|
|
||||||
marker=from_optional_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
|
|
||||||
)
|
|
||||||
for i in range(lib.dc_array_get_cnt(dc_array))
|
for i in range(lib.dc_array_get_cnt(dc_array))
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
class Location:
|
class Location:
|
||||||
def __init__(self, latitude, longitude, accuracy, timestamp, marker):
|
def __init__(self, latitude, longitude, accuracy, timestamp):
|
||||||
assert isinstance(timestamp, datetime)
|
assert isinstance(timestamp, datetime)
|
||||||
self.latitude = latitude
|
self.latitude = latitude
|
||||||
self.longitude = longitude
|
self.longitude = longitude
|
||||||
self.accuracy = accuracy
|
self.accuracy = accuracy
|
||||||
self.timestamp = timestamp
|
self.timestamp = timestamp
|
||||||
self.marker = marker
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.__dict__ == other.__dict__
|
return self.__dict__ == other.__dict__
|
||||||
|
|||||||
@@ -1,13 +1,200 @@
|
|||||||
from typing import Any, List
|
import sys
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
from os.path import dirname, abspath
|
||||||
|
from os.path import join as joinpath
|
||||||
|
|
||||||
from .capi import lib
|
# the following const are generated from deltachat.h
|
||||||
|
# this works well when you in a git-checkout
|
||||||
|
# run "python deltachat/const.py" to regenerate events
|
||||||
|
# begin const generated
|
||||||
|
DC_GCL_ARCHIVED_ONLY = 0x01
|
||||||
|
DC_GCL_NO_SPECIALS = 0x02
|
||||||
|
DC_GCL_ADD_ALLDONE_HINT = 0x04
|
||||||
|
DC_GCL_VERIFIED_ONLY = 0x01
|
||||||
|
DC_GCL_ADD_SELF = 0x02
|
||||||
|
DC_QR_ASK_VERIFYCONTACT = 200
|
||||||
|
DC_QR_ASK_VERIFYGROUP = 202
|
||||||
|
DC_QR_FPR_OK = 210
|
||||||
|
DC_QR_FPR_MISMATCH = 220
|
||||||
|
DC_QR_FPR_WITHOUT_ADDR = 230
|
||||||
|
DC_QR_ACCOUNT = 250
|
||||||
|
DC_QR_ADDR = 320
|
||||||
|
DC_QR_TEXT = 330
|
||||||
|
DC_QR_URL = 332
|
||||||
|
DC_QR_ERROR = 400
|
||||||
|
DC_CHAT_ID_DEADDROP = 1
|
||||||
|
DC_CHAT_ID_TRASH = 3
|
||||||
|
DC_CHAT_ID_MSGS_IN_CREATION = 4
|
||||||
|
DC_CHAT_ID_STARRED = 5
|
||||||
|
DC_CHAT_ID_ARCHIVED_LINK = 6
|
||||||
|
DC_CHAT_ID_ALLDONE_HINT = 7
|
||||||
|
DC_CHAT_ID_LAST_SPECIAL = 9
|
||||||
|
DC_CHAT_TYPE_UNDEFINED = 0
|
||||||
|
DC_CHAT_TYPE_SINGLE = 100
|
||||||
|
DC_CHAT_TYPE_GROUP = 120
|
||||||
|
DC_CHAT_TYPE_VERIFIED_GROUP = 130
|
||||||
|
DC_MSG_ID_MARKER1 = 1
|
||||||
|
DC_MSG_ID_DAYMARKER = 9
|
||||||
|
DC_MSG_ID_LAST_SPECIAL = 9
|
||||||
|
DC_STATE_UNDEFINED = 0
|
||||||
|
DC_STATE_IN_FRESH = 10
|
||||||
|
DC_STATE_IN_NOTICED = 13
|
||||||
|
DC_STATE_IN_SEEN = 16
|
||||||
|
DC_STATE_OUT_PREPARING = 18
|
||||||
|
DC_STATE_OUT_DRAFT = 19
|
||||||
|
DC_STATE_OUT_PENDING = 20
|
||||||
|
DC_STATE_OUT_FAILED = 24
|
||||||
|
DC_STATE_OUT_DELIVERED = 26
|
||||||
|
DC_STATE_OUT_MDN_RCVD = 28
|
||||||
|
DC_CONTACT_ID_SELF = 1
|
||||||
|
DC_CONTACT_ID_INFO = 2
|
||||||
|
DC_CONTACT_ID_DEVICE = 5
|
||||||
|
DC_CONTACT_ID_LAST_SPECIAL = 9
|
||||||
|
DC_MSG_TEXT = 10
|
||||||
|
DC_MSG_IMAGE = 20
|
||||||
|
DC_MSG_GIF = 21
|
||||||
|
DC_MSG_STICKER = 23
|
||||||
|
DC_MSG_AUDIO = 40
|
||||||
|
DC_MSG_VOICE = 41
|
||||||
|
DC_MSG_VIDEO = 50
|
||||||
|
DC_MSG_FILE = 60
|
||||||
|
DC_LP_AUTH_OAUTH2 = 0x2
|
||||||
|
DC_LP_AUTH_NORMAL = 0x4
|
||||||
|
DC_LP_IMAP_SOCKET_STARTTLS = 0x100
|
||||||
|
DC_LP_IMAP_SOCKET_SSL = 0x200
|
||||||
|
DC_LP_IMAP_SOCKET_PLAIN = 0x400
|
||||||
|
DC_LP_SMTP_SOCKET_STARTTLS = 0x10000
|
||||||
|
DC_LP_SMTP_SOCKET_SSL = 0x20000
|
||||||
|
DC_LP_SMTP_SOCKET_PLAIN = 0x40000
|
||||||
|
DC_CERTCK_AUTO = 0
|
||||||
|
DC_CERTCK_STRICT = 1
|
||||||
|
DC_CERTCK_ACCEPT_INVALID_CERTIFICATES = 3
|
||||||
|
DC_EMPTY_MVBOX = 0x01
|
||||||
|
DC_EMPTY_INBOX = 0x02
|
||||||
|
DC_EVENT_INFO = 100
|
||||||
|
DC_EVENT_SMTP_CONNECTED = 101
|
||||||
|
DC_EVENT_IMAP_CONNECTED = 102
|
||||||
|
DC_EVENT_SMTP_MESSAGE_SENT = 103
|
||||||
|
DC_EVENT_IMAP_MESSAGE_DELETED = 104
|
||||||
|
DC_EVENT_IMAP_MESSAGE_MOVED = 105
|
||||||
|
DC_EVENT_IMAP_FOLDER_EMPTIED = 106
|
||||||
|
DC_EVENT_NEW_BLOB_FILE = 150
|
||||||
|
DC_EVENT_DELETED_BLOB_FILE = 151
|
||||||
|
DC_EVENT_WARNING = 300
|
||||||
|
DC_EVENT_ERROR = 400
|
||||||
|
DC_EVENT_ERROR_NETWORK = 401
|
||||||
|
DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410
|
||||||
|
DC_EVENT_MSGS_CHANGED = 2000
|
||||||
|
DC_EVENT_INCOMING_MSG = 2005
|
||||||
|
DC_EVENT_MSG_DELIVERED = 2010
|
||||||
|
DC_EVENT_MSG_FAILED = 2012
|
||||||
|
DC_EVENT_MSG_READ = 2015
|
||||||
|
DC_EVENT_CHAT_MODIFIED = 2020
|
||||||
|
DC_EVENT_CONTACTS_CHANGED = 2030
|
||||||
|
DC_EVENT_LOCATION_CHANGED = 2035
|
||||||
|
DC_EVENT_CONFIGURE_PROGRESS = 2041
|
||||||
|
DC_EVENT_IMEX_PROGRESS = 2051
|
||||||
|
DC_EVENT_IMEX_FILE_WRITTEN = 2052
|
||||||
|
DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060
|
||||||
|
DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061
|
||||||
|
DC_EVENT_SECUREJOIN_MEMBER_ADDED = 2062
|
||||||
|
DC_EVENT_FILE_COPIED = 2055
|
||||||
|
DC_EVENT_IS_OFFLINE = 2081
|
||||||
|
DC_EVENT_GET_STRING = 2091
|
||||||
|
DC_STR_SELFNOTINGRP = 21
|
||||||
|
DC_KEY_GEN_DEFAULT = 0
|
||||||
|
DC_KEY_GEN_RSA2048 = 1
|
||||||
|
DC_KEY_GEN_ED25519 = 2
|
||||||
|
DC_PROVIDER_STATUS_OK = 1
|
||||||
|
DC_PROVIDER_STATUS_PREPARATION = 2
|
||||||
|
DC_PROVIDER_STATUS_BROKEN = 3
|
||||||
|
DC_CHAT_VISIBILITY_NORMAL = 0
|
||||||
|
DC_CHAT_VISIBILITY_ARCHIVED = 1
|
||||||
|
DC_CHAT_VISIBILITY_PINNED = 2
|
||||||
|
DC_STR_NOMESSAGES = 1
|
||||||
|
DC_STR_SELF = 2
|
||||||
|
DC_STR_DRAFT = 3
|
||||||
|
DC_STR_MEMBER = 4
|
||||||
|
DC_STR_CONTACT = 6
|
||||||
|
DC_STR_VOICEMESSAGE = 7
|
||||||
|
DC_STR_DEADDROP = 8
|
||||||
|
DC_STR_IMAGE = 9
|
||||||
|
DC_STR_VIDEO = 10
|
||||||
|
DC_STR_AUDIO = 11
|
||||||
|
DC_STR_FILE = 12
|
||||||
|
DC_STR_STATUSLINE = 13
|
||||||
|
DC_STR_NEWGROUPDRAFT = 14
|
||||||
|
DC_STR_MSGGRPNAME = 15
|
||||||
|
DC_STR_MSGGRPIMGCHANGED = 16
|
||||||
|
DC_STR_MSGADDMEMBER = 17
|
||||||
|
DC_STR_MSGDELMEMBER = 18
|
||||||
|
DC_STR_MSGGROUPLEFT = 19
|
||||||
|
DC_STR_GIF = 23
|
||||||
|
DC_STR_ENCRYPTEDMSG = 24
|
||||||
|
DC_STR_E2E_AVAILABLE = 25
|
||||||
|
DC_STR_ENCR_TRANSP = 27
|
||||||
|
DC_STR_ENCR_NONE = 28
|
||||||
|
DC_STR_CANTDECRYPT_MSG_BODY = 29
|
||||||
|
DC_STR_FINGERPRINTS = 30
|
||||||
|
DC_STR_READRCPT = 31
|
||||||
|
DC_STR_READRCPT_MAILBODY = 32
|
||||||
|
DC_STR_MSGGRPIMGDELETED = 33
|
||||||
|
DC_STR_E2E_PREFERRED = 34
|
||||||
|
DC_STR_CONTACT_VERIFIED = 35
|
||||||
|
DC_STR_CONTACT_NOT_VERIFIED = 36
|
||||||
|
DC_STR_CONTACT_SETUP_CHANGED = 37
|
||||||
|
DC_STR_ARCHIVEDCHATS = 40
|
||||||
|
DC_STR_STARREDMSGS = 41
|
||||||
|
DC_STR_AC_SETUP_MSG_SUBJECT = 42
|
||||||
|
DC_STR_AC_SETUP_MSG_BODY = 43
|
||||||
|
DC_STR_SELFTALK_SUBTITLE = 50
|
||||||
|
DC_STR_CANNOT_LOGIN = 60
|
||||||
|
DC_STR_SERVER_RESPONSE = 61
|
||||||
|
DC_STR_MSGACTIONBYUSER = 62
|
||||||
|
DC_STR_MSGACTIONBYME = 63
|
||||||
|
DC_STR_MSGLOCATIONENABLED = 64
|
||||||
|
DC_STR_MSGLOCATIONDISABLED = 65
|
||||||
|
DC_STR_LOCATION = 66
|
||||||
|
DC_STR_STICKER = 67
|
||||||
|
DC_STR_DEVICE_MESSAGES = 68
|
||||||
|
DC_STR_COUNT = 68
|
||||||
|
# end const generated
|
||||||
|
|
||||||
|
|
||||||
def __getattr__(name: str) -> Any:
|
def read_event_defines(f):
|
||||||
if name.startswith("DC_"):
|
rex = re.compile(r'#define\s+((?:DC_EVENT|DC_QR|DC_MSG|DC_LP|DC_EMPTY|DC_CERTCK|DC_STATE|DC_STR|'
|
||||||
return getattr(lib, name)
|
r'DC_CONTACT_ID|DC_GCL|DC_CHAT|DC_PROVIDER|DC_KEY_GEN)_\S+)\s+([x\d]+).*')
|
||||||
return globals()[name]
|
for line in f:
|
||||||
|
m = rex.match(line)
|
||||||
|
if m:
|
||||||
|
yield m.groups()
|
||||||
|
|
||||||
|
|
||||||
def __dir__() -> List[str]:
|
if __name__ == "__main__":
|
||||||
return sorted(name for name in dir(lib) if name.startswith("DC_"))
|
here = abspath(__file__).rstrip("oc")
|
||||||
|
here_dir = dirname(here)
|
||||||
|
if len(sys.argv) >= 2:
|
||||||
|
deltah = sys.argv[1]
|
||||||
|
else:
|
||||||
|
deltah = joinpath(dirname(dirname(dirname(here_dir))), "deltachat-ffi", "deltachat.h")
|
||||||
|
assert os.path.exists(deltah)
|
||||||
|
|
||||||
|
lines = []
|
||||||
|
skip_to_end = False
|
||||||
|
for orig_line in open(here):
|
||||||
|
if skip_to_end:
|
||||||
|
if not orig_line.startswith("# end const"):
|
||||||
|
continue
|
||||||
|
skip_to_end = False
|
||||||
|
lines.append(orig_line)
|
||||||
|
if orig_line.startswith("# begin const"):
|
||||||
|
with open(deltah) as f:
|
||||||
|
for name, item in read_event_defines(f):
|
||||||
|
lines.append("{} = {}\n".format(name, item))
|
||||||
|
skip_to_end = True
|
||||||
|
|
||||||
|
tmpname = here + ".tmp"
|
||||||
|
with open(tmpname, "w") as f:
|
||||||
|
f.write("".join(lines))
|
||||||
|
os.rename(tmpname, here)
|
||||||
|
|||||||
@@ -1,12 +1,8 @@
|
|||||||
""" Contact object. """
|
""" Contact object. """
|
||||||
|
|
||||||
from datetime import date, datetime, timezone
|
from . import props
|
||||||
from typing import Optional
|
from .cutil import from_dc_charpointer
|
||||||
|
from .capi import lib, ffi
|
||||||
from . import const, props
|
|
||||||
from .capi import ffi, lib
|
|
||||||
from .chat import Chat
|
|
||||||
from .cutil import from_dc_charpointer, from_optional_dc_charpointer
|
|
||||||
|
|
||||||
|
|
||||||
class Contact(object):
|
class Contact(object):
|
||||||
@@ -14,94 +10,50 @@ class Contact(object):
|
|||||||
|
|
||||||
You obtain instances of it through :class:`deltachat.account.Account`.
|
You obtain instances of it through :class:`deltachat.account.Account`.
|
||||||
"""
|
"""
|
||||||
def __init__(self, account, id):
|
def __init__(self, dc_context, id):
|
||||||
from .account import Account
|
self._dc_context = dc_context
|
||||||
assert isinstance(account, Account), repr(account)
|
|
||||||
self.account = account
|
|
||||||
self.id = id
|
self.id = id
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
return self.account._dc_context == other.account._dc_context and self.id == other.id
|
return self._dc_context == other._dc_context and self.id == other.id
|
||||||
|
|
||||||
def __ne__(self, other):
|
def __ne__(self, other):
|
||||||
return not (self == other)
|
return not (self == other)
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self.account._dc_context)
|
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self._dc_context)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def _dc_contact(self):
|
def _dc_contact(self):
|
||||||
return ffi.gc(
|
return ffi.gc(
|
||||||
lib.dc_get_contact(self.account._dc_context, self.id),
|
lib.dc_get_contact(self._dc_context, self.id),
|
||||||
lib.dc_contact_unref
|
lib.dc_contact_unref
|
||||||
)
|
)
|
||||||
|
|
||||||
@props.with_doc
|
@props.with_doc
|
||||||
def addr(self) -> str:
|
def addr(self):
|
||||||
""" normalized e-mail address for this account. """
|
""" normalized e-mail address for this account. """
|
||||||
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
|
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
|
||||||
|
|
||||||
@props.with_doc
|
@props.with_doc
|
||||||
def name(self) -> str:
|
def display_name(self):
|
||||||
""" display name for this contact. """
|
""" display name for this contact. """
|
||||||
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
|
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
|
||||||
|
|
||||||
# deprecated alias
|
|
||||||
display_name = name
|
|
||||||
|
|
||||||
@props.with_doc
|
|
||||||
def last_seen(self) -> date:
|
|
||||||
"""Last seen timestamp."""
|
|
||||||
return datetime.fromtimestamp(
|
|
||||||
lib.dc_contact_get_last_seen(self._dc_contact), timezone.utc
|
|
||||||
)
|
|
||||||
|
|
||||||
def is_blocked(self):
|
def is_blocked(self):
|
||||||
""" Return True if the contact is blocked. """
|
""" Return True if the contact is blocked. """
|
||||||
return lib.dc_contact_is_blocked(self._dc_contact)
|
return lib.dc_contact_is_blocked(self._dc_contact)
|
||||||
|
|
||||||
def set_blocked(self, block=True):
|
|
||||||
""" [Deprecated, use block/unblock methods] Block or unblock a contact. """
|
|
||||||
return lib.dc_block_contact(self.account._dc_context, self.id, block)
|
|
||||||
|
|
||||||
def block(self):
|
|
||||||
""" Block this contact. Message will not be seen/retrieved from this contact. """
|
|
||||||
return lib.dc_block_contact(self.account._dc_context, self.id, True)
|
|
||||||
|
|
||||||
def unblock(self):
|
|
||||||
""" Unblock this contact. Messages from this contact will be retrieved (again)."""
|
|
||||||
return lib.dc_block_contact(self.account._dc_context, self.id, False)
|
|
||||||
|
|
||||||
def is_verified(self):
|
def is_verified(self):
|
||||||
""" 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)
|
||||||
|
|
||||||
def get_profile_image(self) -> Optional[str]:
|
def get_profile_image(self):
|
||||||
"""Get contact profile image.
|
"""Get contact profile image.
|
||||||
|
|
||||||
:returns: path to profile image, None if no profile image exists.
|
:returns: path to profile image, None if no profile image exists.
|
||||||
"""
|
"""
|
||||||
dc_res = lib.dc_contact_get_profile_image(self._dc_contact)
|
dc_res = lib.dc_contact_get_profile_image(self._dc_contact)
|
||||||
return from_optional_dc_charpointer(dc_res)
|
if dc_res == ffi.NULL:
|
||||||
|
return None
|
||||||
@property
|
return from_dc_charpointer(dc_res)
|
||||||
def status(self):
|
|
||||||
"""Get contact status.
|
|
||||||
|
|
||||||
:returns: contact status, empty string if it doesn't exist.
|
|
||||||
"""
|
|
||||||
return from_dc_charpointer(lib.dc_contact_get_status(self._dc_contact))
|
|
||||||
|
|
||||||
def create_chat(self):
|
|
||||||
""" create or get an existing 1:1 chat object for the specified contact or contact id.
|
|
||||||
|
|
||||||
:param contact: chat_id (int) or contact object.
|
|
||||||
:returns: a :class:`deltachat.chat.Chat` object.
|
|
||||||
"""
|
|
||||||
dc_context = self.account._dc_context
|
|
||||||
chat_id = lib.dc_create_chat_by_contact_id(dc_context, self.id)
|
|
||||||
assert chat_id > const.DC_CHAT_ID_LAST_SPECIAL, chat_id
|
|
||||||
return Chat(self.account, chat_id)
|
|
||||||
|
|
||||||
# deprecated name
|
|
||||||
get_chat = create_chat
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
from .capi import lib
|
from .capi import lib
|
||||||
from .capi import ffi
|
from .capi import ffi
|
||||||
from datetime import datetime, timezone
|
from datetime import datetime
|
||||||
from typing import Optional, TypeVar, Generator, Callable
|
|
||||||
|
|
||||||
T = TypeVar('T')
|
|
||||||
|
|
||||||
|
|
||||||
def as_dc_charpointer(obj):
|
def as_dc_charpointer(obj):
|
||||||
@@ -14,28 +11,20 @@ def as_dc_charpointer(obj):
|
|||||||
return obj
|
return obj
|
||||||
|
|
||||||
|
|
||||||
def iter_array(dc_array_t, constructor: Callable[[int], T]) -> Generator[T, None, None]:
|
def iter_array(dc_array_t, constructor):
|
||||||
for i in range(0, lib.dc_array_get_cnt(dc_array_t)):
|
for i in range(0, 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))
|
||||||
|
|
||||||
|
|
||||||
def from_dc_charpointer(obj) -> str:
|
def from_dc_charpointer(obj):
|
||||||
if obj != ffi.NULL:
|
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
||||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
|
||||||
raise ValueError
|
|
||||||
|
|
||||||
|
|
||||||
def from_optional_dc_charpointer(obj) -> Optional[str]:
|
|
||||||
if obj != ffi.NULL:
|
|
||||||
return ffi.string(ffi.gc(obj, lib.dc_str_unref)).decode("utf8")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
class DCLot:
|
class DCLot:
|
||||||
def __init__(self, dc_lot) -> None:
|
def __init__(self, dc_lot):
|
||||||
self._dc_lot = dc_lot
|
self._dc_lot = dc_lot
|
||||||
|
|
||||||
def id(self) -> int:
|
def id(self):
|
||||||
return lib.dc_lot_get_id(self._dc_lot)
|
return lib.dc_lot_get_id(self._dc_lot)
|
||||||
|
|
||||||
def state(self):
|
def state(self):
|
||||||
@@ -54,4 +43,4 @@ class DCLot:
|
|||||||
ts = lib.dc_lot_get_timestamp(self._dc_lot)
|
ts = lib.dc_lot_get_timestamp(self._dc_lot)
|
||||||
if ts == 0:
|
if ts == 0:
|
||||||
return None
|
return None
|
||||||
return datetime.fromtimestamp(ts, timezone.utc)
|
return datetime.utcfromtimestamp(ts)
|
||||||
|
|||||||
@@ -1,250 +0,0 @@
|
|||||||
"""
|
|
||||||
Internal Python-level IMAP handling used by the testplugin
|
|
||||||
and for cleaning up inbox/mvbox for each test function run.
|
|
||||||
"""
|
|
||||||
|
|
||||||
import io
|
|
||||||
import ssl
|
|
||||||
import pathlib
|
|
||||||
from imap_tools import MailBox, MailBoxTls, errors, AND, Header, MailMessageFlags, MailMessage
|
|
||||||
import imaplib
|
|
||||||
import deltachat
|
|
||||||
from deltachat import const, Account
|
|
||||||
from typing import List
|
|
||||||
|
|
||||||
|
|
||||||
FLAGS = b'FLAGS'
|
|
||||||
FETCH = b'FETCH'
|
|
||||||
ALL = "1:*"
|
|
||||||
|
|
||||||
|
|
||||||
@deltachat.global_hookimpl
|
|
||||||
def dc_account_extra_configure(account: Account):
|
|
||||||
""" Reset the account (we reuse accounts across tests)
|
|
||||||
and make 'account.direct_imap' available for direct IMAP ops.
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
|
|
||||||
if not hasattr(account, "direct_imap"):
|
|
||||||
imap = DirectImap(account)
|
|
||||||
|
|
||||||
for folder in imap.list_folders():
|
|
||||||
if folder.lower() == "inbox" or folder.lower() == "deltachat":
|
|
||||||
assert imap.select_folder(folder)
|
|
||||||
imap.delete(ALL, expunge=True)
|
|
||||||
else:
|
|
||||||
imap.conn.folder.delete(folder)
|
|
||||||
# We just deleted the folder, so we have to make DC forget about it, too
|
|
||||||
if account.get_config("configured_sentbox_folder") == folder:
|
|
||||||
account.set_config("configured_sentbox_folder", None)
|
|
||||||
|
|
||||||
setattr(account, "direct_imap", imap)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# Uncaught exceptions here would lead to a timeout without any note written to the log
|
|
||||||
# start with DC_EVENT_WARNING so that the line is printed in yellow and won't be overlooked when reading
|
|
||||||
account.log("DC_EVENT_WARNING =================== DIRECT_IMAP CAN'T RESET ACCOUNT: ===================")
|
|
||||||
account.log("DC_EVENT_WARNING =================== " + str(e) + " ===================")
|
|
||||||
|
|
||||||
|
|
||||||
@deltachat.global_hookimpl
|
|
||||||
def dc_account_after_shutdown(account):
|
|
||||||
""" shutdown the imap connection if there is one. """
|
|
||||||
imap = getattr(account, "direct_imap", None)
|
|
||||||
if imap is not None:
|
|
||||||
imap.shutdown()
|
|
||||||
del account.direct_imap
|
|
||||||
|
|
||||||
|
|
||||||
class DirectImap:
|
|
||||||
def __init__(self, account: Account) -> None:
|
|
||||||
self.account = account
|
|
||||||
self.logid = account.get_config("displayname") or id(account)
|
|
||||||
self._idling = False
|
|
||||||
self.connect()
|
|
||||||
|
|
||||||
def connect(self):
|
|
||||||
host = self.account.get_config("configured_mail_server")
|
|
||||||
port = int(self.account.get_config("configured_mail_port"))
|
|
||||||
security = int(self.account.get_config("configured_mail_security"))
|
|
||||||
|
|
||||||
user = self.account.get_config("addr")
|
|
||||||
pw = self.account.get_config("mail_pw")
|
|
||||||
|
|
||||||
if security == const.DC_SOCKET_PLAIN:
|
|
||||||
ssl_context = None
|
|
||||||
else:
|
|
||||||
ssl_context = ssl.create_default_context()
|
|
||||||
|
|
||||||
# don't check if certificate hostname doesn't match target hostname
|
|
||||||
ssl_context.check_hostname = False
|
|
||||||
|
|
||||||
# don't check if the certificate is trusted by a certificate authority
|
|
||||||
ssl_context.verify_mode = ssl.CERT_NONE
|
|
||||||
|
|
||||||
if security == const.DC_SOCKET_STARTTLS:
|
|
||||||
self.conn = MailBoxTls(host, port, ssl_context=ssl_context)
|
|
||||||
elif security == const.DC_SOCKET_PLAIN or security == const.DC_SOCKET_SSL:
|
|
||||||
self.conn = MailBox(host, port, ssl_context=ssl_context)
|
|
||||||
self.conn.login(user, pw)
|
|
||||||
|
|
||||||
self.select_folder("INBOX")
|
|
||||||
|
|
||||||
def shutdown(self):
|
|
||||||
try:
|
|
||||||
self.idle_done()
|
|
||||||
except (OSError, imaplib.IMAP4.abort):
|
|
||||||
pass
|
|
||||||
try:
|
|
||||||
self.conn.logout()
|
|
||||||
except (OSError, imaplib.IMAP4.abort):
|
|
||||||
print("Could not logout direct_imap conn")
|
|
||||||
|
|
||||||
def create_folder(self, foldername):
|
|
||||||
try:
|
|
||||||
self.conn.folder.create(foldername)
|
|
||||||
except errors.MailboxFolderCreateError as e:
|
|
||||||
print("Can't create", foldername, "probably it already exists:", str(e))
|
|
||||||
|
|
||||||
def select_folder(self, foldername: str) -> tuple:
|
|
||||||
assert not self._idling
|
|
||||||
return self.conn.folder.set(foldername)
|
|
||||||
|
|
||||||
def select_config_folder(self, config_name: str):
|
|
||||||
""" Return info about selected folder if it is
|
|
||||||
configured, otherwise None. """
|
|
||||||
if "_" not in config_name:
|
|
||||||
config_name = "configured_{}_folder".format(config_name)
|
|
||||||
foldername = self.account.get_config(config_name)
|
|
||||||
if foldername:
|
|
||||||
return self.select_folder(foldername)
|
|
||||||
|
|
||||||
def list_folders(self) -> List[str]:
|
|
||||||
""" return list of all existing folder names"""
|
|
||||||
assert not self._idling
|
|
||||||
return [folder.name for folder in self.conn.folder.list()]
|
|
||||||
|
|
||||||
def delete(self, uid_list: str, expunge=True):
|
|
||||||
""" delete a range of messages (imap-syntax).
|
|
||||||
If expunge is true, perform the expunge-operation
|
|
||||||
to make sure the messages are really gone and not
|
|
||||||
just flagged as deleted.
|
|
||||||
"""
|
|
||||||
self.conn.client.uid('STORE', uid_list, '+FLAGS', r'(\Deleted)')
|
|
||||||
if expunge:
|
|
||||||
self.conn.expunge()
|
|
||||||
|
|
||||||
def get_all_messages(self) -> List[MailMessage]:
|
|
||||||
assert not self._idling
|
|
||||||
return [mail for mail in self.conn.fetch()]
|
|
||||||
|
|
||||||
def get_unread_messages(self) -> List[str]:
|
|
||||||
assert not self._idling
|
|
||||||
return [msg.uid for msg in self.conn.fetch(AND(seen=False))]
|
|
||||||
|
|
||||||
def mark_all_read(self):
|
|
||||||
messages = self.get_unread_messages()
|
|
||||||
if messages:
|
|
||||||
res = self.conn.flag(messages, MailMessageFlags.SEEN, True)
|
|
||||||
print("marked seen:", messages, res)
|
|
||||||
|
|
||||||
def get_unread_cnt(self) -> int:
|
|
||||||
return len(self.get_unread_messages())
|
|
||||||
|
|
||||||
def dump_imap_structures(self, dir, logfile):
|
|
||||||
assert not self._idling
|
|
||||||
stream = io.StringIO()
|
|
||||||
|
|
||||||
def log(*args, **kwargs):
|
|
||||||
kwargs["file"] = stream
|
|
||||||
print(*args, **kwargs)
|
|
||||||
|
|
||||||
empty_folders = []
|
|
||||||
for imapfolder in self.list_folders():
|
|
||||||
self.select_folder(imapfolder)
|
|
||||||
messages = list(self.get_all_messages())
|
|
||||||
if not messages:
|
|
||||||
empty_folders.append(imapfolder)
|
|
||||||
continue
|
|
||||||
|
|
||||||
log("---------", imapfolder, len(messages), "messages ---------")
|
|
||||||
# get message content without auto-marking it as seen
|
|
||||||
# fetching 'RFC822' would mark it as seen.
|
|
||||||
for msg in self.conn.fetch(mark_seen=False):
|
|
||||||
body = getattr(msg.obj, "text", None)
|
|
||||||
if not body:
|
|
||||||
body = getattr(msg.obj, "html", None)
|
|
||||||
if not body:
|
|
||||||
log("Message", msg.uid, "has empty body")
|
|
||||||
continue
|
|
||||||
|
|
||||||
path = pathlib.Path(str(dir)).joinpath("IMAP", self.logid, imapfolder)
|
|
||||||
path.mkdir(parents=True, exist_ok=True)
|
|
||||||
fn = path.joinpath(str(msg.uid))
|
|
||||||
fn.write_bytes(body)
|
|
||||||
log("Message", msg.uid, fn)
|
|
||||||
log("Message", msg.uid, msg.flags, "Message-Id:", msg.obj.get("Message-Id"))
|
|
||||||
|
|
||||||
if empty_folders:
|
|
||||||
log("--------- EMPTY FOLDERS:", empty_folders)
|
|
||||||
|
|
||||||
print(stream.getvalue(), file=logfile)
|
|
||||||
|
|
||||||
def idle_start(self):
|
|
||||||
""" switch this connection to idle mode. non-blocking. """
|
|
||||||
assert not self._idling
|
|
||||||
res = self.conn.idle.start()
|
|
||||||
self._idling = True
|
|
||||||
return res
|
|
||||||
|
|
||||||
def idle_check(self, terminate=False, timeout=None) -> List[bytes]:
|
|
||||||
""" (blocking) wait for next idle message from server. """
|
|
||||||
assert self._idling
|
|
||||||
self.account.log("imap-direct: calling idle_check")
|
|
||||||
res = self.conn.idle.poll(timeout=timeout)
|
|
||||||
if terminate:
|
|
||||||
self.idle_done()
|
|
||||||
self.account.log("imap-direct: idle_check returned {!r}".format(res))
|
|
||||||
return res
|
|
||||||
|
|
||||||
def idle_wait_for_new_message(self, terminate=False, timeout=None) -> bytes:
|
|
||||||
while 1:
|
|
||||||
for item in self.idle_check(timeout=timeout):
|
|
||||||
if b'EXISTS' in item or b'RECENT' in item:
|
|
||||||
if terminate:
|
|
||||||
self.idle_done()
|
|
||||||
return item
|
|
||||||
|
|
||||||
def idle_wait_for_seen(self, terminate=False, timeout=None) -> int:
|
|
||||||
""" Return first message with SEEN flag from a running idle-stream.
|
|
||||||
"""
|
|
||||||
while 1:
|
|
||||||
for item in self.idle_check(timeout=timeout):
|
|
||||||
if FETCH in item:
|
|
||||||
self.account.log(str(item))
|
|
||||||
if FLAGS in item and rb'\Seen' in item:
|
|
||||||
if terminate:
|
|
||||||
self.idle_done()
|
|
||||||
return int(item.split(b' ')[1])
|
|
||||||
|
|
||||||
def idle_done(self):
|
|
||||||
""" send idle-done to server if we are currently in idle mode. """
|
|
||||||
if self._idling:
|
|
||||||
res = self.conn.idle.stop()
|
|
||||||
self._idling = False
|
|
||||||
return res
|
|
||||||
|
|
||||||
def append(self, folder: str, msg: str):
|
|
||||||
"""Upload a message to *folder*.
|
|
||||||
Trailing whitespace or a linebreak at the beginning will be removed automatically.
|
|
||||||
"""
|
|
||||||
if msg.startswith("\n"):
|
|
||||||
msg = msg[1:]
|
|
||||||
msg = '\n'.join([s.lstrip() for s in msg.splitlines()])
|
|
||||||
self.conn.append(bytes(msg, encoding='ascii'), folder)
|
|
||||||
|
|
||||||
def get_uid_by_message_id(self, message_id) -> str:
|
|
||||||
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header('MESSAGE-ID', message_id)))]
|
|
||||||
if len(msgs) == 0:
|
|
||||||
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
|
|
||||||
return msgs[0]
|
|
||||||
81
python/src/deltachat/eventlogger.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import threading
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
from queue import Queue, Empty
|
||||||
|
from .hookspec import hookimpl
|
||||||
|
|
||||||
|
|
||||||
|
class EventLogger:
|
||||||
|
_loglock = threading.RLock()
|
||||||
|
|
||||||
|
def __init__(self, account, logid=None, debug=True):
|
||||||
|
self.account = account
|
||||||
|
self._event_queue = Queue()
|
||||||
|
self._debug = debug
|
||||||
|
if logid is None:
|
||||||
|
logid = str(self.account._dc_context).strip(">").split()[-1]
|
||||||
|
self.logid = logid
|
||||||
|
self._timeout = None
|
||||||
|
self.init_time = time.time()
|
||||||
|
|
||||||
|
@hookimpl
|
||||||
|
def process_low_level_event(self, account, event_name, data1, data2):
|
||||||
|
if self.account == account:
|
||||||
|
self._log_event(event_name, data1, data2)
|
||||||
|
self._event_queue.put((event_name, data1, data2))
|
||||||
|
|
||||||
|
def set_timeout(self, timeout):
|
||||||
|
self._timeout = timeout
|
||||||
|
|
||||||
|
def consume_events(self, check_error=True):
|
||||||
|
while not self._event_queue.empty():
|
||||||
|
self.get(check_error=check_error)
|
||||||
|
|
||||||
|
def get(self, timeout=None, check_error=True):
|
||||||
|
timeout = timeout or self._timeout
|
||||||
|
ev = self._event_queue.get(timeout=timeout)
|
||||||
|
if check_error and ev[0] == "DC_EVENT_ERROR":
|
||||||
|
raise ValueError("{}({!r},{!r})".format(*ev))
|
||||||
|
return ev
|
||||||
|
|
||||||
|
def ensure_event_not_queued(self, event_name_regex):
|
||||||
|
__tracebackhide__ = True
|
||||||
|
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||||
|
while 1:
|
||||||
|
try:
|
||||||
|
ev = self._event_queue.get(False)
|
||||||
|
except Empty:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
assert not rex.match(ev[0]), "event found {}".format(ev)
|
||||||
|
|
||||||
|
def get_matching(self, event_name_regex, check_error=True, timeout=None):
|
||||||
|
self._log("-- waiting for event with regex: {} --".format(event_name_regex))
|
||||||
|
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||||
|
while 1:
|
||||||
|
ev = self.get(timeout=timeout, check_error=check_error)
|
||||||
|
if rex.match(ev[0]):
|
||||||
|
return ev
|
||||||
|
|
||||||
|
def get_info_matching(self, regex):
|
||||||
|
rex = re.compile("(?:{}).*".format(regex))
|
||||||
|
while 1:
|
||||||
|
ev = self.get_matching("DC_EVENT_INFO")
|
||||||
|
if rex.match(ev[2]):
|
||||||
|
return ev
|
||||||
|
|
||||||
|
def _log_event(self, evt_name, data1, data2):
|
||||||
|
# don't show events that are anyway empty impls now
|
||||||
|
if evt_name == "DC_EVENT_GET_STRING":
|
||||||
|
return
|
||||||
|
if self._debug:
|
||||||
|
evpart = "{}({!r},{!r})".format(evt_name, data1, data2)
|
||||||
|
self._log(evpart)
|
||||||
|
|
||||||
|
def _log(self, msg):
|
||||||
|
t = threading.currentThread()
|
||||||
|
tname = getattr(t, "name", t)
|
||||||
|
if tname == "MainThread":
|
||||||
|
tname = "MAIN"
|
||||||
|
with self._loglock:
|
||||||
|
print("{:2.2f} [{}-{}] {}".format(time.time() - self.init_time, tname, self.logid, msg))
|
||||||
@@ -1,280 +0,0 @@
|
|||||||
import threading
|
|
||||||
import time
|
|
||||||
import re
|
|
||||||
import os
|
|
||||||
from queue import Queue, Empty
|
|
||||||
|
|
||||||
import deltachat
|
|
||||||
from .hookspec import account_hookimpl
|
|
||||||
from contextlib import contextmanager
|
|
||||||
from .capi import ffi, lib
|
|
||||||
from .message import map_system_message
|
|
||||||
from .cutil import from_optional_dc_charpointer
|
|
||||||
|
|
||||||
|
|
||||||
class FFIEvent:
|
|
||||||
def __init__(self, name: str, data1, data2):
|
|
||||||
self.name = name
|
|
||||||
self.data1 = data1
|
|
||||||
self.data2 = data2
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return "{name} data1={data1} data2={data2}".format(**self.__dict__)
|
|
||||||
|
|
||||||
|
|
||||||
class FFIEventLogger:
|
|
||||||
""" If you register an instance of this logger with an Account
|
|
||||||
you'll get all ffi-events printed.
|
|
||||||
"""
|
|
||||||
# to prevent garbled logging
|
|
||||||
_loglock = threading.RLock()
|
|
||||||
|
|
||||||
def __init__(self, account) -> None:
|
|
||||||
self.account = account
|
|
||||||
self.logid = self.account.get_config("displayname")
|
|
||||||
self.init_time = time.time()
|
|
||||||
|
|
||||||
@account_hookimpl
|
|
||||||
def ac_process_ffi_event(self, ffi_event: FFIEvent) -> None:
|
|
||||||
self.account.log(str(ffi_event))
|
|
||||||
|
|
||||||
@account_hookimpl
|
|
||||||
def ac_log_line(self, message):
|
|
||||||
t = threading.current_thread()
|
|
||||||
tname = getattr(t, "name", t)
|
|
||||||
if tname == "MainThread":
|
|
||||||
tname = "MAIN"
|
|
||||||
elapsed = time.time() - self.init_time
|
|
||||||
locname = tname
|
|
||||||
if self.logid:
|
|
||||||
locname += "-" + self.logid
|
|
||||||
s = "{:2.2f} [{}] {}".format(elapsed, locname, message)
|
|
||||||
|
|
||||||
if os.name == "posix":
|
|
||||||
WARN = '\033[93m'
|
|
||||||
ERROR = '\033[91m'
|
|
||||||
ENDC = '\033[0m'
|
|
||||||
if message.startswith("DC_EVENT_WARNING"):
|
|
||||||
s = WARN + s + ENDC
|
|
||||||
if message.startswith("DC_EVENT_ERROR"):
|
|
||||||
s = ERROR + s + ENDC
|
|
||||||
with self._loglock:
|
|
||||||
print(s, flush=True)
|
|
||||||
|
|
||||||
|
|
||||||
class FFIEventTracker:
|
|
||||||
def __init__(self, account, timeout=None):
|
|
||||||
self.account = account
|
|
||||||
self._timeout = timeout
|
|
||||||
self._event_queue = Queue()
|
|
||||||
|
|
||||||
@account_hookimpl
|
|
||||||
def ac_process_ffi_event(self, ffi_event: FFIEvent):
|
|
||||||
self._event_queue.put(ffi_event)
|
|
||||||
|
|
||||||
def set_timeout(self, timeout):
|
|
||||||
self._timeout = timeout
|
|
||||||
|
|
||||||
def consume_events(self, check_error=True):
|
|
||||||
while not self._event_queue.empty():
|
|
||||||
self.get(check_error=check_error)
|
|
||||||
|
|
||||||
def get(self, timeout=None, check_error=True):
|
|
||||||
timeout = timeout if timeout is not None else self._timeout
|
|
||||||
ev = self._event_queue.get(timeout=timeout)
|
|
||||||
if check_error and ev.name == "DC_EVENT_ERROR":
|
|
||||||
raise ValueError("unexpected event: {}".format(ev))
|
|
||||||
return ev
|
|
||||||
|
|
||||||
def iter_events(self, timeout=None, check_error=True):
|
|
||||||
while 1:
|
|
||||||
yield self.get(timeout=timeout, check_error=check_error)
|
|
||||||
|
|
||||||
def get_matching(self, event_name_regex, check_error=True, timeout=None):
|
|
||||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
|
||||||
for ev in self.iter_events(timeout=timeout, check_error=check_error):
|
|
||||||
if rex.match(ev.name):
|
|
||||||
return ev
|
|
||||||
|
|
||||||
def get_info_contains(self, regex: str) -> FFIEvent:
|
|
||||||
rex = re.compile(regex)
|
|
||||||
while 1:
|
|
||||||
ev = self.get_matching("DC_EVENT_INFO")
|
|
||||||
if rex.search(ev.data2):
|
|
||||||
return ev
|
|
||||||
|
|
||||||
def get_info_regex_groups(self, regex, check_error=True):
|
|
||||||
rex = re.compile(regex)
|
|
||||||
while 1:
|
|
||||||
ev = self.get_matching("DC_EVENT_INFO", check_error=check_error)
|
|
||||||
m = rex.match(ev.data2)
|
|
||||||
if m is not None:
|
|
||||||
return m.groups()
|
|
||||||
|
|
||||||
def wait_for_connectivity(self, connectivity):
|
|
||||||
"""Wait for the specified connectivity.
|
|
||||||
This only works reliably if the connectivity doesn't change
|
|
||||||
again too quickly, otherwise we might miss it."""
|
|
||||||
while 1:
|
|
||||||
if self.account.get_connectivity() == connectivity:
|
|
||||||
return
|
|
||||||
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
|
||||||
|
|
||||||
def wait_for_connectivity_change(self, previous, expected_next):
|
|
||||||
"""Wait until the connectivity changes to `expected_next`.
|
|
||||||
Fails the test if it changes to something else."""
|
|
||||||
while 1:
|
|
||||||
current = self.account.get_connectivity()
|
|
||||||
if current == expected_next:
|
|
||||||
return
|
|
||||||
elif current != previous:
|
|
||||||
raise Exception("Expected connectivity " + str(expected_next) + " but got " + str(current))
|
|
||||||
|
|
||||||
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
|
||||||
|
|
||||||
def wait_for_all_work_done(self):
|
|
||||||
while 1:
|
|
||||||
if self.account.all_work_done():
|
|
||||||
return
|
|
||||||
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
|
|
||||||
|
|
||||||
def ensure_event_not_queued(self, event_name_regex):
|
|
||||||
__tracebackhide__ = True
|
|
||||||
rex = re.compile("(?:{}).*".format(event_name_regex))
|
|
||||||
while 1:
|
|
||||||
try:
|
|
||||||
ev = self._event_queue.get(False)
|
|
||||||
except Empty:
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
assert not rex.match(ev.name), "event found {}".format(ev)
|
|
||||||
|
|
||||||
def wait_securejoin_inviter_progress(self, target):
|
|
||||||
while 1:
|
|
||||||
event = self.get_matching("DC_EVENT_SECUREJOIN_INVITER_PROGRESS")
|
|
||||||
if event.data2 >= target:
|
|
||||||
print("** SECUREJOINT-INVITER PROGRESS {}".format(target), self.account)
|
|
||||||
break
|
|
||||||
|
|
||||||
def wait_all_initial_fetches(self):
|
|
||||||
"""Has to be called after start_io() to wait for fetch_existing_msgs to run
|
|
||||||
so that new messages are not mistaken for old ones:
|
|
||||||
- ac1 and ac2 are created
|
|
||||||
- ac1 sends a message to ac2
|
|
||||||
- ac2 is still running FetchExsistingMsgs job and thinks it's an existing, old message
|
|
||||||
- therefore no DC_EVENT_INCOMING_MSG is sent"""
|
|
||||||
self.get_info_contains("Done fetching existing messages")
|
|
||||||
|
|
||||||
def wait_next_incoming_message(self):
|
|
||||||
""" wait for and return next incoming message. """
|
|
||||||
ev = self.get_matching("DC_EVENT_INCOMING_MSG")
|
|
||||||
return self.account.get_message_by_id(ev.data2)
|
|
||||||
|
|
||||||
def wait_next_messages_changed(self):
|
|
||||||
""" wait for and return next message-changed message or None
|
|
||||||
if the event contains no msgid"""
|
|
||||||
ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
|
|
||||||
if ev.data2 > 0:
|
|
||||||
return self.account.get_message_by_id(ev.data2)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def wait_msg_delivered(self, msg):
|
|
||||||
ev = self.get_matching("DC_EVENT_MSG_DELIVERED")
|
|
||||||
assert ev.data1 == msg.chat.id
|
|
||||||
assert ev.data2 == msg.id
|
|
||||||
assert msg.is_out_delivered()
|
|
||||||
|
|
||||||
|
|
||||||
class EventThread(threading.Thread):
|
|
||||||
""" Event Thread for an account.
|
|
||||||
|
|
||||||
With each Account init this callback thread is started.
|
|
||||||
"""
|
|
||||||
def __init__(self, account) -> None:
|
|
||||||
self.account = account
|
|
||||||
super(EventThread, self).__init__(name="events")
|
|
||||||
self.daemon = True
|
|
||||||
self._marked_for_shutdown = False
|
|
||||||
self.start()
|
|
||||||
|
|
||||||
@contextmanager
|
|
||||||
def log_execution(self, message):
|
|
||||||
self.account.log(message + " START")
|
|
||||||
yield
|
|
||||||
self.account.log(message + " FINISHED")
|
|
||||||
|
|
||||||
def mark_shutdown(self) -> None:
|
|
||||||
self._marked_for_shutdown = True
|
|
||||||
|
|
||||||
def wait(self, timeout=None) -> None:
|
|
||||||
if self == threading.current_thread():
|
|
||||||
# we are in the callback thread and thus cannot
|
|
||||||
# wait for the thread-loop to finish.
|
|
||||||
return
|
|
||||||
self.join(timeout=timeout)
|
|
||||||
|
|
||||||
def run(self) -> None:
|
|
||||||
""" get and run events until shutdown. """
|
|
||||||
with self.log_execution("EVENT THREAD"):
|
|
||||||
self._inner_run()
|
|
||||||
|
|
||||||
def _inner_run(self):
|
|
||||||
event_emitter = ffi.gc(
|
|
||||||
lib.dc_get_event_emitter(self.account._dc_context),
|
|
||||||
lib.dc_event_emitter_unref,
|
|
||||||
)
|
|
||||||
while not self._marked_for_shutdown:
|
|
||||||
event = lib.dc_get_next_event(event_emitter)
|
|
||||||
if event == ffi.NULL:
|
|
||||||
break
|
|
||||||
if self._marked_for_shutdown:
|
|
||||||
break
|
|
||||||
evt = lib.dc_event_get_id(event)
|
|
||||||
data1 = lib.dc_event_get_data1_int(event)
|
|
||||||
# the following code relates to the deltachat/_build.py's helper
|
|
||||||
# function which provides us signature info of an event call
|
|
||||||
evt_name = deltachat.get_dc_event_name(evt)
|
|
||||||
if lib.dc_event_has_string_data(evt):
|
|
||||||
data2 = from_optional_dc_charpointer(lib.dc_event_get_data2_str(event))
|
|
||||||
else:
|
|
||||||
data2 = lib.dc_event_get_data2_int(event)
|
|
||||||
|
|
||||||
lib.dc_event_unref(event)
|
|
||||||
ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2)
|
|
||||||
try:
|
|
||||||
self.account._pm.hook.ac_process_ffi_event(account=self, ffi_event=ffi_event)
|
|
||||||
for name, kwargs in self._map_ffi_event(ffi_event):
|
|
||||||
self.account.log("calling hook name={} kwargs={}".format(name, kwargs))
|
|
||||||
hook = getattr(self.account._pm.hook, name)
|
|
||||||
hook(**kwargs)
|
|
||||||
except Exception:
|
|
||||||
if self.account._dc_context is not None:
|
|
||||||
raise
|
|
||||||
|
|
||||||
def _map_ffi_event(self, ffi_event: FFIEvent):
|
|
||||||
name = ffi_event.name
|
|
||||||
account = self.account
|
|
||||||
if name == "DC_EVENT_CONFIGURE_PROGRESS":
|
|
||||||
data1 = ffi_event.data1
|
|
||||||
if data1 == 0 or data1 == 1000:
|
|
||||||
success = data1 == 1000
|
|
||||||
yield "ac_configure_completed", dict(success=success)
|
|
||||||
elif name == "DC_EVENT_INCOMING_MSG":
|
|
||||||
msg = account.get_message_by_id(ffi_event.data2)
|
|
||||||
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
|
|
||||||
elif name == "DC_EVENT_MSGS_CHANGED":
|
|
||||||
if ffi_event.data2 != 0:
|
|
||||||
msg = account.get_message_by_id(ffi_event.data2)
|
|
||||||
if msg.is_outgoing():
|
|
||||||
res = map_system_message(msg)
|
|
||||||
if res and res[0].startswith("ac_member"):
|
|
||||||
yield res
|
|
||||||
yield "ac_outgoing_message", dict(message=msg)
|
|
||||||
elif msg.is_in_fresh():
|
|
||||||
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg))
|
|
||||||
elif name == "DC_EVENT_MSG_DELIVERED":
|
|
||||||
msg = account.get_message_by_id(ffi_event.data2)
|
|
||||||
yield "ac_message_delivered", dict(message=msg)
|
|
||||||
elif name == "DC_EVENT_CHAT_MODIFIED":
|
|
||||||
chat = account.get_chat_by_id(ffi_event.data1)
|
|
||||||
yield "ac_chat_modified", dict(chat=chat)
|
|
||||||
@@ -1,115 +1,25 @@
|
|||||||
""" Hooks for Python bindings to Delta Chat Core Rust CFFI"""
|
""" Hooks for python bindings """
|
||||||
|
|
||||||
import pluggy
|
import pluggy
|
||||||
|
|
||||||
|
name = "deltachat"
|
||||||
|
|
||||||
account_spec_name = "deltachat-account"
|
hookspec = pluggy.HookspecMarker(name)
|
||||||
account_hookspec = pluggy.HookspecMarker(account_spec_name)
|
hookimpl = pluggy.HookimplMarker(name)
|
||||||
account_hookimpl = pluggy.HookimplMarker(account_spec_name)
|
_plugin_manager = None
|
||||||
|
|
||||||
global_spec_name = "deltachat-global"
|
|
||||||
global_hookspec = pluggy.HookspecMarker(global_spec_name)
|
|
||||||
global_hookimpl = pluggy.HookimplMarker(global_spec_name)
|
|
||||||
|
|
||||||
|
|
||||||
class PerAccount:
|
def get_plugin_manager():
|
||||||
""" per-Account-instance hook specifications.
|
global _plugin_manager
|
||||||
|
if _plugin_manager is None:
|
||||||
All hooks are executed in a dedicated Event thread.
|
_plugin_manager = pluggy.PluginManager(name)
|
||||||
Hooks are generally not allowed to block/last long as this
|
_plugin_manager.add_hookspecs(DeltaChatHookSpecs)
|
||||||
blocks overall event processing on the python side.
|
return _plugin_manager
|
||||||
"""
|
|
||||||
@classmethod
|
|
||||||
def _make_plugin_manager(cls):
|
|
||||||
pm = pluggy.PluginManager(account_spec_name)
|
|
||||||
pm.add_hookspecs(cls)
|
|
||||||
return pm
|
|
||||||
|
|
||||||
@account_hookspec
|
|
||||||
def ac_process_ffi_event(self, ffi_event):
|
|
||||||
""" process a CFFI low level events for a given account.
|
|
||||||
|
|
||||||
ffi_event has "name", "data1", "data2" values as specified
|
|
||||||
with `DC_EVENT_* <https://c.delta.chat/group__DC__EVENT.html>`_.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@account_hookspec
|
|
||||||
def ac_log_line(self, message):
|
|
||||||
""" log a message related to the account. """
|
|
||||||
|
|
||||||
@account_hookspec
|
|
||||||
def ac_configure_completed(self, success):
|
|
||||||
""" Called after a configure process completed. """
|
|
||||||
|
|
||||||
@account_hookspec
|
|
||||||
def ac_incoming_message(self, message):
|
|
||||||
""" Called on any incoming message (both existing chats and contact requests). """
|
|
||||||
|
|
||||||
@account_hookspec
|
|
||||||
def ac_outgoing_message(self, message):
|
|
||||||
""" Called on each outgoing message (both system and "normal")."""
|
|
||||||
|
|
||||||
@account_hookspec
|
|
||||||
def ac_message_delivered(self, message):
|
|
||||||
""" Called when an outgoing message has been delivered to SMTP.
|
|
||||||
|
|
||||||
:param message: Message that was just delivered.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@account_hookspec
|
|
||||||
def ac_chat_modified(self, chat):
|
|
||||||
""" Chat was created or modified regarding membership, avatar, title.
|
|
||||||
|
|
||||||
:param chat: Chat which was modified.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@account_hookspec
|
|
||||||
def ac_member_added(self, chat, contact, actor, message):
|
|
||||||
""" Called for each contact added to an accepted chat.
|
|
||||||
|
|
||||||
:param chat: Chat where contact was added.
|
|
||||||
:param contact: Contact that was added.
|
|
||||||
:param actor: Who added the contact (None if it was our self-addr)
|
|
||||||
:param message: The original system message that reports the addition.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@account_hookspec
|
|
||||||
def ac_member_removed(self, chat, contact, actor, message):
|
|
||||||
""" Called for each contact removed from a chat.
|
|
||||||
|
|
||||||
:param chat: Chat where contact was removed.
|
|
||||||
:param contact: Contact that was removed.
|
|
||||||
:param actor: Who removed the contact (None if it was our self-addr)
|
|
||||||
:param message: The original system message that reports the removal.
|
|
||||||
"""
|
|
||||||
|
|
||||||
|
|
||||||
class Global:
|
class DeltaChatHookSpecs:
|
||||||
""" global hook specifications using a per-process singleton
|
""" Plugin Hook specifications for Python bindings to Delta Chat CFFI. """
|
||||||
plugin manager instance.
|
|
||||||
|
|
||||||
"""
|
@hookspec
|
||||||
_plugin_manager = None
|
def process_low_level_event(self, account, event_name, data1, data2):
|
||||||
|
""" process a CFFI low level events for a given account. """
|
||||||
@classmethod
|
|
||||||
def _get_plugin_manager(cls):
|
|
||||||
if cls._plugin_manager is None:
|
|
||||||
cls._plugin_manager = pm = pluggy.PluginManager(global_spec_name)
|
|
||||||
pm.add_hookspecs(cls)
|
|
||||||
return cls._plugin_manager
|
|
||||||
|
|
||||||
@global_hookspec
|
|
||||||
def dc_account_init(self, account):
|
|
||||||
""" called when `Account::__init__()` function starts executing. """
|
|
||||||
|
|
||||||
@global_hookspec
|
|
||||||
def dc_account_extra_configure(self, account):
|
|
||||||
""" Called when account configuration successfully finished.
|
|
||||||
|
|
||||||
This hook can be used to perform extra work before
|
|
||||||
ac_configure_completed is called.
|
|
||||||
"""
|
|
||||||
|
|
||||||
@global_hookspec
|
|
||||||
def dc_account_after_shutdown(self, account):
|
|
||||||
""" Called after the account has been shutdown. """
|
|
||||||
|
|||||||