mirror of
https://github.com/chatmail/core.git
synced 2026-05-07 00:46:31 +03:00
feat: bring back and adapt python bindings with rust core
* import python, try to adapt for rust * add missing wrapper functions * - try to write up how to build python bindings - strike some unused functions from deltachat.h * adjustments to make tox work * try to run circle-ci with python build * don't do docs * running cargo test as well * don't run cargo test anymore, that's done in other ci jobs * also build docs * don't run doxygen anymore * subst C with Rust * a try to get better wheels Closes #41
This commit is contained in:
committed by
Friedel Ziegelmayer
parent
a2fc127923
commit
6ce8374513
@@ -123,11 +123,53 @@ jobs:
|
|||||||
target: "aarch64-linux-android"
|
target: "aarch64-linux-android"
|
||||||
|
|
||||||
|
|
||||||
|
build_test_docs_wheel:
|
||||||
|
machine: True
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
# - run: docker pull deltachat/doxygen
|
||||||
|
- run: docker pull deltachat/coredeps
|
||||||
|
- run:
|
||||||
|
name: build docs, run tests and build wheels
|
||||||
|
command: ci_scripts/ci_run.sh
|
||||||
|
environment:
|
||||||
|
TESTS: 1
|
||||||
|
DOCS: 1
|
||||||
|
|
||||||
|
- run:
|
||||||
|
name: copying docs and wheels to workspace
|
||||||
|
command: |
|
||||||
|
mkdir -p workspace/python
|
||||||
|
# cp -av docs workspace/c-docs
|
||||||
|
cp -av python/.docker-tox/wheelhouse workspace/
|
||||||
|
cp -av python/doc/_build/ workspace/py-docs
|
||||||
|
|
||||||
|
- persist_to_workspace:
|
||||||
|
root: workspace
|
||||||
|
paths:
|
||||||
|
# - c-docs
|
||||||
|
- py-docs
|
||||||
|
- wheelhouse
|
||||||
|
|
||||||
|
upload_docs_wheels:
|
||||||
|
machine: True
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- attach_workspace:
|
||||||
|
at: workspace
|
||||||
|
- run: ls -laR workspace
|
||||||
|
- run: ci_scripts/ci_upload.sh workspace/py-docs workspace/wheelhouse
|
||||||
|
|
||||||
|
|
||||||
workflows:
|
workflows:
|
||||||
version: 2.1
|
version: 2.1
|
||||||
|
|
||||||
test:
|
test:
|
||||||
jobs:
|
jobs:
|
||||||
|
- build_test_docs_wheel
|
||||||
|
- upload_docs_wheels:
|
||||||
|
requires:
|
||||||
|
- build_test_docs_wheel
|
||||||
- cargo_fetch
|
- cargo_fetch
|
||||||
- rustfmt:
|
- rustfmt:
|
||||||
requires:
|
requires:
|
||||||
|
|||||||
52
ci_scripts/README.md
Normal file
52
ci_scripts/README.md
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
|
||||||
|
# Continuous Integration Scripts for Delta Chat
|
||||||
|
|
||||||
|
Continuous Integration is run through CircleCI
|
||||||
|
but is largely independent of it.
|
||||||
|
|
||||||
|
|
||||||
|
## Generating docker containers for performing build step work
|
||||||
|
|
||||||
|
All tests, docs and wheel building is run in docker containers:
|
||||||
|
|
||||||
|
- **coredeps/Dockerfile** specifies an image that contains all
|
||||||
|
of Delta Chat's core dependencies as linkable libraries.
|
||||||
|
It also serves to run python tests and build wheels
|
||||||
|
(binary packages for Python).
|
||||||
|
|
||||||
|
- **doxygen/Dockerfile** specifies an image that contains
|
||||||
|
the doxygen tool which is used to generate C-docs.
|
||||||
|
|
||||||
|
To run tests locally you can pull existing images from "docker.io",
|
||||||
|
the hub for sharing Docker images::
|
||||||
|
|
||||||
|
docker pull deltachat/coredeps
|
||||||
|
docker pull deltachat/doxygen
|
||||||
|
|
||||||
|
or you can build the docker images yourself locally
|
||||||
|
to avoid the relatively large download::
|
||||||
|
|
||||||
|
cd ci_scripts # where all CI things are
|
||||||
|
docker build -t deltachat/coredeps docker-coredeps
|
||||||
|
docker build -t deltachat/doxygen docker-doxygen
|
||||||
|
|
||||||
|
## ci_run.sh (main entrypoint called by circle-ci)
|
||||||
|
|
||||||
|
Once you have the docker images available
|
||||||
|
you can run python testing, documentation generation
|
||||||
|
and building binary wheels::
|
||||||
|
|
||||||
|
sh DOCS=1 TESTS=1 ci_scripts/ci_run.sh
|
||||||
|
|
||||||
|
## ci_upload.sh (uploading artifacts on success)
|
||||||
|
|
||||||
|
- python docs to `https://py.delta.chat/_unofficial_unreleased_docs/<BRANCH>`
|
||||||
|
|
||||||
|
- doxygen docs to `https://c.delta.chat/_unofficial_unreleased_docs/<BRANCH>`
|
||||||
|
|
||||||
|
- python wheels to `https://m.devpi.net/dc/<BRANCH>`
|
||||||
|
so that you install fully self-contained wheels like this:
|
||||||
|
`pip install -U -i https://m.devpi.net/dc/<BRANCH> deltachat`
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
22
ci_scripts/ci_run.sh
Executable file
22
ci_scripts/ci_run.sh
Executable file
@@ -0,0 +1,22 @@
|
|||||||
|
# perform CI jobs on PRs and after merges to master.
|
||||||
|
# triggered from .circleci/config.yml
|
||||||
|
|
||||||
|
set -e -x
|
||||||
|
|
||||||
|
export BRANCH=${CIRCLE_BRANCH:-test7}
|
||||||
|
|
||||||
|
# run doxygen on c-source (needed by later doc-generation steps).
|
||||||
|
# XXX modifies the host filesystem docs/xml and docs/html directories
|
||||||
|
# XXX which you can then only remove with sudo as they belong to root
|
||||||
|
|
||||||
|
# XXX we don't do doxygen doc generation with Rust anymore, needs to be
|
||||||
|
# substituted with rust-docs
|
||||||
|
#if [ -n "$DOCS" ] ; then
|
||||||
|
# docker run --rm -it -v $PWD:/mnt -w /mnt/docs deltachat/doxygen doxygen
|
||||||
|
#fi
|
||||||
|
|
||||||
|
# run everything else inside docker (TESTS, DOCS, WHEELS)
|
||||||
|
docker run -e BRANCH -e TESTS -e DOCS \
|
||||||
|
--rm -it -v $(pwd):/mnt -w /mnt \
|
||||||
|
deltachat/coredeps ci_scripts/run_all.sh
|
||||||
|
|
||||||
47
ci_scripts/ci_upload.sh
Executable file
47
ci_scripts/ci_upload.sh
Executable file
@@ -0,0 +1,47 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
if [ -z "$DEVPI_LOGIN" ] ; then
|
||||||
|
echo "required: password for 'dc' user on https://m.devpi/net/dc index"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
set -xe
|
||||||
|
|
||||||
|
#DOXYDOCDIR=${1:?directory where doxygen docs to be found}
|
||||||
|
PYDOCDIR=${1:?directory with python docs}
|
||||||
|
WHEELHOUSEDIR=${2:?directory with pre-built wheels}
|
||||||
|
|
||||||
|
export BRANCH=${CIRCLE_BRANCH:?specify branch for uploading purposes}
|
||||||
|
|
||||||
|
|
||||||
|
# python docs to py.delta.chat
|
||||||
|
rsync -avz \
|
||||||
|
-e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
||||||
|
"$PYDOCDIR/html/" \
|
||||||
|
delta@py.delta.chat:build/${BRANCH}
|
||||||
|
|
||||||
|
# C docs to c.delta.chat
|
||||||
|
#rsync -avz \
|
||||||
|
# -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null" \
|
||||||
|
# "$DOXYDOCDIR/html/" \
|
||||||
|
# delta@py.delta.chat:build-c/${BRANCH}
|
||||||
|
|
||||||
|
echo -----------------------
|
||||||
|
echo upload wheels
|
||||||
|
echo -----------------------
|
||||||
|
|
||||||
|
# Bundle external shared libraries into the wheels
|
||||||
|
pushd $WHEELHOUSEDIR
|
||||||
|
|
||||||
|
pip install devpi-client
|
||||||
|
devpi use https://m.devpi.net
|
||||||
|
devpi login dc --password $DEVPI_LOGIN
|
||||||
|
|
||||||
|
devpi use dc/$BRANCH || {
|
||||||
|
devpi index -c $BRANCH
|
||||||
|
devpi use dc/$BRANCH
|
||||||
|
}
|
||||||
|
devpi index $BRANCH bases=/root/pypi
|
||||||
|
devpi upload deltachat*.whl
|
||||||
|
|
||||||
|
popd
|
||||||
25
ci_scripts/docker-coredeps/Dockerfile
Normal file
25
ci_scripts/docker-coredeps/Dockerfile
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
FROM quay.io/pypa/manylinux1_x86_64
|
||||||
|
|
||||||
|
# Configure ld.so/ldconfig and pkg-config
|
||||||
|
RUN echo /usr/local/lib64 > /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 PIP_DISABLE_PIP_VERSION_CHECK 1
|
||||||
|
|
||||||
|
# Install python tools (auditwheels,tox, ...)
|
||||||
|
ADD deps/build_python.sh /builder/build_python.sh
|
||||||
|
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_python.sh && cd .. && rm -r tmp1
|
||||||
|
|
||||||
|
# Install Rust nightly
|
||||||
|
ADD deps/build_rust.sh /builder/build_rust.sh
|
||||||
|
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_rust.sh && cd .. && rm -r tmp1
|
||||||
|
|
||||||
|
# Install a recent Perl, needed to install OpenSSL
|
||||||
|
ADD deps/build_perl.sh /builder/build_perl.sh
|
||||||
|
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_perl.sh && cd .. && rm -r tmp1
|
||||||
|
|
||||||
|
# Install OpenSSL
|
||||||
|
ADD deps/build_openssl.sh /builder/build_openssl.sh
|
||||||
|
RUN mkdir tmp1 && cd tmp1 && bash /builder/build_openssl.sh && cd .. && rm -r tmp1
|
||||||
|
|
||||||
21
ci_scripts/docker-coredeps/deps/build_openssl.sh
Executable file
21
ci_scripts/docker-coredeps/deps/build_openssl.sh
Executable file
@@ -0,0 +1,21 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e -x
|
||||||
|
|
||||||
|
OPENSSL_VERSION=1.1.1a
|
||||||
|
OPENSSL_SHA256=fc20130f8b7cbd2fb918b2f14e2f429e109c31ddd0fb38fc5d71d9ffed3f9f41
|
||||||
|
|
||||||
|
curl -O https://www.openssl.org/source/openssl-${OPENSSL_VERSION}.tar.gz
|
||||||
|
echo "${OPENSSL_SHA256} openssl-${OPENSSL_VERSION}.tar.gz" | sha256sum -c -
|
||||||
|
tar xzf openssl-${OPENSSL_VERSION}.tar.gz
|
||||||
|
cd openssl-${OPENSSL_VERSION}
|
||||||
|
./config shared no-ssl2 no-ssl3 -fPIC --prefix=/usr/local
|
||||||
|
|
||||||
|
sed -i "s/^SHLIB_MAJOR=.*/SHLIB_MAJOR=200/" Makefile && \
|
||||||
|
sed -i "s/^SHLIB_MINOR=.*/SHLIB_MINOR=0.0/" Makefile && \
|
||||||
|
sed -i "s/^SHLIB_VERSION_NUMBER=.*/SHLIB_VERSION_NUMBER=200.0.0/" Makefile
|
||||||
|
|
||||||
|
make depend
|
||||||
|
make
|
||||||
|
make install_sw install_ssldirs
|
||||||
|
ldconfig -v | grep ssl
|
||||||
12
ci_scripts/docker-coredeps/deps/build_perl.sh
Executable file
12
ci_scripts/docker-coredeps/deps/build_perl.sh
Executable file
@@ -0,0 +1,12 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
PERL_VERSION=5.28.0
|
||||||
|
PERL_SHA256=7e929f64d4cb0e9d1159d4a59fc89394e27fa1f7004d0836ca0d514685406ea8
|
||||||
|
curl -O https://www.cpan.org/src/5.0/perl-${PERL_VERSION}.tar.gz
|
||||||
|
echo "${PERL_SHA256} perl-${PERL_VERSION}.tar.gz" | sha256sum -c -
|
||||||
|
tar xzf perl-${PERL_VERSION}.tar.gz
|
||||||
|
cd perl-${PERL_VERSION}
|
||||||
|
|
||||||
|
./Configure -de
|
||||||
|
make
|
||||||
|
make install
|
||||||
14
ci_scripts/docker-coredeps/deps/build_python.sh
Executable file
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
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-04-19 -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-04-19-x86_64-unknown-linux-gnu/share/
|
||||||
49
ci_scripts/docker-coredeps/deps/run_all.sh
Executable file
49
ci_scripts/docker-coredeps/deps/run_all.sh
Executable file
@@ -0,0 +1,49 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
#
|
||||||
|
# Build the Delta Chat C/Rust library
|
||||||
|
#
|
||||||
|
set -e -x
|
||||||
|
|
||||||
|
# perform clean build of core and install
|
||||||
|
export TOXWORKDIR=.docker-tox
|
||||||
|
|
||||||
|
# build core library
|
||||||
|
|
||||||
|
cargo build --release -p deltachat_ffi
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
#
|
||||||
|
# run python tests
|
||||||
|
#
|
||||||
|
|
||||||
|
if [ -n "$TESTS" ]; then
|
||||||
|
|
||||||
|
echo ----------------
|
||||||
|
echo run python tests
|
||||||
|
echo ----------------
|
||||||
|
|
||||||
|
pushd python
|
||||||
|
# first run all tests ...
|
||||||
|
rm -rf tests/__pycache__
|
||||||
|
rm -rf src/deltachat/__pycache__
|
||||||
|
export PYTHONDONTWRITEBYTECODE=1
|
||||||
|
tox --workdir "$TOXWORKDIR" -e py27,py35,py36,py37
|
||||||
|
popd
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
if [ -n "$DOCS" ]; then
|
||||||
|
echo -----------------------
|
||||||
|
echo generating python docs
|
||||||
|
echo -----------------------
|
||||||
|
(cd python && tox --workdir "$TOXWORKDIR" -e doc)
|
||||||
|
fi
|
||||||
5
ci_scripts/docker-doxygen/Dockerfile
Normal file
5
ci_scripts/docker-doxygen/Dockerfile
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
FROM debian:stable
|
||||||
|
|
||||||
|
# this is tagged as deltachat/doxygen
|
||||||
|
|
||||||
|
RUN apt-get update && apt-get install -y doxygen
|
||||||
52
ci_scripts/run_all.sh
Executable file
52
ci_scripts/run_all.sh
Executable file
@@ -0,0 +1,52 @@
|
|||||||
|
#!/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
|
||||||
|
|
||||||
|
# make sure subsequent compiler invocations find header and libraries
|
||||||
|
export CFLAGS=-I`pwd`/deltachat-ffi
|
||||||
|
export LD_LIBRARY_PATH=`pwd`/target/release
|
||||||
|
|
||||||
|
# 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
|
||||||
|
tox --workdir "$TOXWORKDIR" -e py27,py35,py36,py37,auditwheels
|
||||||
|
popd
|
||||||
|
fi
|
||||||
|
|
||||||
|
|
||||||
|
if [ -n "$DOCS" ]; then
|
||||||
|
echo -----------------------
|
||||||
|
echo generating python docs
|
||||||
|
echo -----------------------
|
||||||
|
(cd python && tox --workdir "$TOXWORKDIR" -e doc)
|
||||||
|
fi
|
||||||
@@ -229,8 +229,6 @@ char* dc_get_config (dc_context_t*, const char* key);
|
|||||||
char* dc_get_info (dc_context_t*);
|
char* dc_get_info (dc_context_t*);
|
||||||
char* dc_get_oauth2_url (dc_context_t*, const char* addr, const char* redirect);
|
char* dc_get_oauth2_url (dc_context_t*, const char* addr, const char* redirect);
|
||||||
char* dc_get_version_str (void);
|
char* dc_get_version_str (void);
|
||||||
void dc_openssl_init_not_required (void);
|
|
||||||
void dc_no_compound_msgs (void); // deprecated
|
|
||||||
|
|
||||||
|
|
||||||
// connect
|
// connect
|
||||||
@@ -437,8 +435,7 @@ const uintptr_t* dc_array_get_raw (const dc_array_t*);
|
|||||||
* Rendering the deaddrop in the described way
|
* Rendering the deaddrop in the described way
|
||||||
* would not add extra work in the UI then.
|
* would not add extra work in the UI then.
|
||||||
*/
|
*/
|
||||||
dc_chatlist_t* dc_chatlist_new (dc_context_t*);
|
// dc_chatlist_t* dc_chatlist_new (dc_context_t*);
|
||||||
void dc_chatlist_empty (dc_chatlist_t*);
|
|
||||||
void dc_chatlist_unref (dc_chatlist_t*);
|
void dc_chatlist_unref (dc_chatlist_t*);
|
||||||
size_t dc_chatlist_get_cnt (const dc_chatlist_t*);
|
size_t dc_chatlist_get_cnt (const dc_chatlist_t*);
|
||||||
uint32_t dc_chatlist_get_chat_id (const dc_chatlist_t*, size_t index);
|
uint32_t dc_chatlist_get_chat_id (const dc_chatlist_t*, size_t index);
|
||||||
@@ -470,8 +467,6 @@ dc_context_t* dc_chatlist_get_context (dc_chatlist_t*);
|
|||||||
#define DC_CHAT_TYPE_VERIFIED_GROUP 130
|
#define DC_CHAT_TYPE_VERIFIED_GROUP 130
|
||||||
|
|
||||||
|
|
||||||
dc_chat_t* dc_chat_new (dc_context_t*);
|
|
||||||
void dc_chat_empty (dc_chat_t*);
|
|
||||||
void dc_chat_unref (dc_chat_t*);
|
void dc_chat_unref (dc_chat_t*);
|
||||||
|
|
||||||
uint32_t dc_chat_get_id (const dc_chat_t*);
|
uint32_t dc_chat_get_id (const dc_chat_t*);
|
||||||
@@ -578,8 +573,6 @@ void dc_msg_latefiling_mediasize (dc_msg_t*, int width, int height,
|
|||||||
#define DC_CONTACT_ID_LAST_SPECIAL 9
|
#define DC_CONTACT_ID_LAST_SPECIAL 9
|
||||||
|
|
||||||
|
|
||||||
dc_contact_t* dc_contact_new (dc_context_t*);
|
|
||||||
void dc_contact_empty (dc_contact_t*);
|
|
||||||
void dc_contact_unref (dc_contact_t*);
|
void dc_contact_unref (dc_contact_t*);
|
||||||
uint32_t dc_contact_get_id (const dc_contact_t*);
|
uint32_t dc_contact_get_id (const dc_contact_t*);
|
||||||
char* dc_contact_get_addr (const dc_contact_t*);
|
char* dc_contact_get_addr (const dc_contact_t*);
|
||||||
|
|||||||
4
python/.gitignore
vendored
Normal file
4
python/.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
doc/_build/
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
src/deltachat/capi.abi3.so
|
||||||
73
python/CHANGELOG
Normal file
73
python/CHANGELOG
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
0.9.1-dev
|
||||||
|
---------
|
||||||
|
|
||||||
|
- use docker image for building wheels
|
||||||
|
- fix code documentation links
|
||||||
|
|
||||||
|
0.9.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
- build against latest deltachat core (0.41 ++) with RPGP compiled in
|
||||||
|
- streamline package building
|
||||||
|
- allow installing from devpi.net package index
|
||||||
|
|
||||||
|
|
||||||
|
0.8.2
|
||||||
|
-----
|
||||||
|
|
||||||
|
- adapt to deleted and new event names
|
||||||
|
|
||||||
|
0.8.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
- changed license to MPL 2.0, see https://github.com/deltachat/deltachat-core/issues/403
|
||||||
|
for the confirmation from contributors.
|
||||||
|
|
||||||
|
|
||||||
|
0.8.0
|
||||||
|
-----
|
||||||
|
|
||||||
|
- now depends on core tagged as v0.23.0
|
||||||
|
|
||||||
|
- drop send_image and other helper methods in favor of
|
||||||
|
just a Chat.send_file() that takes all kinds of media types
|
||||||
|
|
||||||
|
- introduce Message.get_mime_headers() which returns
|
||||||
|
the headers for an incoming message if the ``save_mime_headers``
|
||||||
|
config option is set.
|
||||||
|
|
||||||
|
- add Message.time_received attribute, now returning an unaware
|
||||||
|
datetime
|
||||||
|
|
||||||
|
- set_config() and get_config() now throw an error if a config option
|
||||||
|
does not exist
|
||||||
|
|
||||||
|
- some API renames
|
||||||
|
|
||||||
|
0.7.1
|
||||||
|
-----
|
||||||
|
|
||||||
|
- add Account.get_infostring() to show low-level info about account state
|
||||||
|
|
||||||
|
|
||||||
|
0.7
|
||||||
|
---
|
||||||
|
|
||||||
|
- add Chat.delete(), Chat.send_image() and Chat.send_file()
|
||||||
|
|
||||||
|
- renamed Chat.send_text_message to Chat.send_text()
|
||||||
|
(all other send methods also have no "_msg" at the end
|
||||||
|
as it's pretty clear Chat.send_X is going to send a message
|
||||||
|
to the chat)
|
||||||
|
|
||||||
|
- new Account.create_message() to create new messages
|
||||||
|
that are not in the database (yet)
|
||||||
|
|
||||||
|
- refined logging of events which now shows relative timestamps
|
||||||
|
|
||||||
|
|
||||||
|
0.6
|
||||||
|
---
|
||||||
|
|
||||||
|
- initial release with full low level C-API, and a first
|
||||||
|
high level API
|
||||||
373
python/LICENSE
Normal file
373
python/LICENSE
Normal file
@@ -0,0 +1,373 @@
|
|||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
||||||
3
python/MANIFEST.in
Normal file
3
python/MANIFEST.in
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
include tox.ini
|
||||||
|
recursive-include tests *.py
|
||||||
|
recursive-include tests/data *
|
||||||
161
python/README.rst
Normal file
161
python/README.rst
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
=========================
|
||||||
|
deltachat python bindings
|
||||||
|
=========================
|
||||||
|
|
||||||
|
This package provides bindings to the deltachat-core_ Rust -library
|
||||||
|
which provides imap/smtp/crypto handling as well as chat/group/messages
|
||||||
|
handling to Android, Desktop and IO user interfaces.
|
||||||
|
|
||||||
|
Installing pre-built packages (linux-only)
|
||||||
|
==========================================
|
||||||
|
|
||||||
|
If you have a linux system you may install the ``deltachat`` binary "wheel" package
|
||||||
|
without any "build-from-source" steps.
|
||||||
|
|
||||||
|
1. `Install virtualenv <https://virtualenv.pypa.io/en/stable/installation/>`_,
|
||||||
|
then create a fresh python environment and activate it in your shell::
|
||||||
|
|
||||||
|
virtualenv -p python3 venv
|
||||||
|
source venv/bin/activate
|
||||||
|
|
||||||
|
Afterwards, invoking ``python`` or ``pip install`` will only
|
||||||
|
modify files in your ``venv`` directory and leave your system installation
|
||||||
|
alone.
|
||||||
|
|
||||||
|
2. Install the wheel for linux::
|
||||||
|
|
||||||
|
pip install deltachat
|
||||||
|
|
||||||
|
Verify it worked by typing::
|
||||||
|
|
||||||
|
python -c "import deltachat"
|
||||||
|
|
||||||
|
|
||||||
|
Installing a wheel from a PR/branch
|
||||||
|
---------------------------------------
|
||||||
|
|
||||||
|
For Linux, we automatically build wheels for all github PR branches
|
||||||
|
and push them to a python package index. To install the latest github ``master`` branch::
|
||||||
|
|
||||||
|
pip install -i https://m.devpi.net/dc/master deltachat
|
||||||
|
|
||||||
|
|
||||||
|
Installing bindings from source
|
||||||
|
===============================
|
||||||
|
|
||||||
|
If you can't use "binary" method above then you need to compile
|
||||||
|
to core deltachat library::
|
||||||
|
|
||||||
|
git clone https://github.com/deltachat/deltachat-core-rust
|
||||||
|
cd deltachat-core-rust
|
||||||
|
cargo build -p deltachat_ffi --release
|
||||||
|
|
||||||
|
This will result in a ``libdeltachat.so`` and ``deltachat.h`` file
|
||||||
|
in the ``deltachat_ffi`` directory. These files are needed for
|
||||||
|
creating the python bindings for deltachat::
|
||||||
|
|
||||||
|
cd python
|
||||||
|
CFLAGS=I`pwd`/../deltachat-ffi pip install -e .
|
||||||
|
|
||||||
|
You then need to set the load-dynamic-library path in your shell::
|
||||||
|
|
||||||
|
export LD_LIBRARY_PATH=`pwd`/../target/release
|
||||||
|
|
||||||
|
so that importing the bindings finds the correct library::
|
||||||
|
|
||||||
|
python -c 'import deltachat ; print(deltachat.__version__)'
|
||||||
|
|
||||||
|
This should print your deltachat bindings version.
|
||||||
|
|
||||||
|
.. note::
|
||||||
|
|
||||||
|
If you can help to automate the building of wheels for Mac or Windows,
|
||||||
|
that'd be much appreciated! please then get
|
||||||
|
`in contact with us <https://delta.chat/en/contribute>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Code examples
|
||||||
|
=============
|
||||||
|
|
||||||
|
You may look at `examples <https://py.delta.chat/examples.html>`_.
|
||||||
|
|
||||||
|
|
||||||
|
Running tests
|
||||||
|
=============
|
||||||
|
|
||||||
|
Get a checkout of the `deltachat-core github repository`_ and type::
|
||||||
|
|
||||||
|
cd python
|
||||||
|
export CFLAGS=I`pwd`/../deltachat-ffi
|
||||||
|
export LD_LIBRARY_PATH=`pwd`/../target/release
|
||||||
|
pip install tox
|
||||||
|
tox
|
||||||
|
|
||||||
|
If you want to run functional tests with real
|
||||||
|
e-mail test accounts, generate a "liveconfig" file where each
|
||||||
|
lines contains test account settings, for example::
|
||||||
|
|
||||||
|
# 'liveconfig' file specifying imap/smtp accounts
|
||||||
|
addr=some-email@example.org mail_pw=password
|
||||||
|
addr=other-email@example.org mail_pw=otherpassword
|
||||||
|
|
||||||
|
The "keyword=value" style allows to specify any
|
||||||
|
`deltachat account config setting <https://c.delta.chat/classdc__context__t.html#aff3b894f6cfca46cab5248fdffdf083d>`_ so you can also specify smtp or imap servers, ports, ssl modes etc.
|
||||||
|
Typically DC's automatic configuration allows to not specify these settings.
|
||||||
|
|
||||||
|
You can now run tests with this ``liveconfig`` file::
|
||||||
|
|
||||||
|
tox -- --liveconfig liveconfig
|
||||||
|
|
||||||
|
|
||||||
|
.. _`deltachat-core github repository`: https://github.com/deltachat/deltachat-core-rust
|
||||||
|
.. _`deltachat-core`: https://github.com/deltachat/deltachat-core-rust
|
||||||
|
|
||||||
|
|
||||||
|
Building manylinux1 wheels
|
||||||
|
==========================
|
||||||
|
|
||||||
|
Building portable manylinux1 wheels which come with libdeltachat.so
|
||||||
|
and all it's dependencies is easy using the provided docker tooling.
|
||||||
|
|
||||||
|
using docker pull / premade images
|
||||||
|
------------------------------------
|
||||||
|
|
||||||
|
We publish a build environment under the ``deltachat/wheel`` tag so
|
||||||
|
that you can pull it from the ``hub.docker.com`` site's "deltachat"
|
||||||
|
organization::
|
||||||
|
|
||||||
|
$ docker pull deltachat/wheel
|
||||||
|
|
||||||
|
The ``deltachat/wheel`` image can be used to build both libdeltachat.so
|
||||||
|
and the Python wheels::
|
||||||
|
|
||||||
|
$ docker run --rm -it -v $(pwd):/io/ deltachat/wheel /io/python/wheelbuilder/build-wheels.sh
|
||||||
|
|
||||||
|
This command runs a script within the image, after mounting ``$(pwd)`` as ``/io`` within
|
||||||
|
the docker image. The script is specified as a path within the docker image's filesystem.
|
||||||
|
The resulting wheel files will be in ``python/wheelhouse``.
|
||||||
|
|
||||||
|
|
||||||
|
Optionally build your own docker image
|
||||||
|
--------------------------------------
|
||||||
|
|
||||||
|
If you want to build your own custom docker image you can do this::
|
||||||
|
|
||||||
|
$ cd deltachat-core # cd to deltachat-core checkout directory
|
||||||
|
$ docker build -t deltachat/wheel python/wheelbuilder/
|
||||||
|
|
||||||
|
This will use the ``python/wheelbuilder/Dockerfile`` to build
|
||||||
|
up docker image called ``deltachat/wheel``. You can afterwards
|
||||||
|
find it with::
|
||||||
|
|
||||||
|
$ docker images
|
||||||
|
|
||||||
|
|
||||||
|
Troubleshooting
|
||||||
|
---------------
|
||||||
|
|
||||||
|
On more recent systems running the docker image may crash. You can
|
||||||
|
fix this by adding ``vsyscall=emulate`` to the Linux kernel boot
|
||||||
|
arguments commandline. E.g. on Debian you'd add this to
|
||||||
|
``GRUB_CMDLINE_LINUX_DEFAULT`` in ``/etc/default/grub``.
|
||||||
197
python/doc/Makefile
Normal file
197
python/doc/Makefile
Normal file
@@ -0,0 +1,197 @@
|
|||||||
|
# Makefile for Sphinx documentation
|
||||||
|
#
|
||||||
|
|
||||||
|
VERSION = $(shell python -c "import conf ; print(conf.version)")
|
||||||
|
DOCZIP = devpi-$(VERSION).doc.zip
|
||||||
|
# You can set these variables from the command line.
|
||||||
|
SPHINXOPTS =
|
||||||
|
SPHINXBUILD = sphinx-build
|
||||||
|
PAPER =
|
||||||
|
BUILDDIR = _build
|
||||||
|
RSYNCOPTS = -e "ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null"
|
||||||
|
|
||||||
|
export HOME=/tmp/home
|
||||||
|
export TESTHOME=$(HOME)
|
||||||
|
|
||||||
|
# Internal variables.
|
||||||
|
PAPEROPT_a4 = -D latex_paper_size=a4
|
||||||
|
PAPEROPT_letter = -D latex_paper_size=letter
|
||||||
|
ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
# the i18n builder cannot share the environment and doctrees with the others
|
||||||
|
I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
|
||||||
|
|
||||||
|
# This variable is not auto generated as the order is important.
|
||||||
|
USER_MAN_CHAPTERS = commands\
|
||||||
|
user\
|
||||||
|
indices\
|
||||||
|
packages\
|
||||||
|
# userman/index.rst\
|
||||||
|
# userman/devpi_misc.rst\
|
||||||
|
# userman/devpi_concepts.rst\
|
||||||
|
|
||||||
|
|
||||||
|
#export DEVPI_CLIENTDIR=$(CURDIR)/.tmp_devpi_user_man/client
|
||||||
|
#export DEVPI_SERVERDIR=$(CURDIR)/.tmp_devpi_user_man/server
|
||||||
|
|
||||||
|
chapter = commands
|
||||||
|
|
||||||
|
.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp \
|
||||||
|
epub latex latexpdf text man changes linkcheck doctest gettext install \
|
||||||
|
quickstart-releaseprocess quickstart-pypimirror quickstart-server regen \
|
||||||
|
prepare-quickstart\
|
||||||
|
regen.server-fresh regen.server-restart regen.server-clean\
|
||||||
|
regen.uman-all regen.uman
|
||||||
|
|
||||||
|
help:
|
||||||
|
@echo "Please use \`make <target>' where <target> is one of"
|
||||||
|
@echo " html to make standalone HTML files"
|
||||||
|
@echo " dirhtml to make HTML files named index.html in directories"
|
||||||
|
@echo " singlehtml to make a single large HTML file"
|
||||||
|
@echo " pickle to make pickle files"
|
||||||
|
@echo " json to make JSON files"
|
||||||
|
@echo " htmlhelp to make HTML files and a HTML help project"
|
||||||
|
@echo " qthelp to make HTML files and a qthelp project"
|
||||||
|
@echo " devhelp to make HTML files and a Devhelp project"
|
||||||
|
@echo " epub to make an epub"
|
||||||
|
@echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
|
||||||
|
@echo " latexpdf to make LaTeX files and run them through pdflatex"
|
||||||
|
@echo " text to make text files"
|
||||||
|
@echo " man to make manual pages"
|
||||||
|
@echo " texinfo to make Texinfo files"
|
||||||
|
@echo " info to make Texinfo files and run them through makeinfo"
|
||||||
|
@echo " gettext to make PO message catalogs"
|
||||||
|
@echo " changes to make an overview of all changed/added/deprecated items"
|
||||||
|
@echo " linkcheck to check all external links for integrity"
|
||||||
|
@echo " doctest to run all doctests embedded in the documentation (if enabled)"
|
||||||
|
@echo
|
||||||
|
@echo "User Manual Regen Targets"
|
||||||
|
@echo " regen.uman regenerates page. of the user manual chapeter e.g. regen.uman chapter=..."
|
||||||
|
@echo " regen.uman-all regenerates the user manual"
|
||||||
|
@echo " regen.uman-clean stop temp server and clean up directory"
|
||||||
|
@echo " Chapter List: $(USER_MAN_CHAPTERS)"
|
||||||
|
|
||||||
|
clean:
|
||||||
|
-rm -rf $(BUILDDIR)/*
|
||||||
|
|
||||||
|
version:
|
||||||
|
@echo "version $(VERSION)"
|
||||||
|
|
||||||
|
doczip: html
|
||||||
|
python doczip.py $(DOCZIP) _build/html
|
||||||
|
|
||||||
|
install: html
|
||||||
|
rsync -avz $(RSYNCOPTS) _build/html/ delta@py.delta.chat:build/master
|
||||||
|
|
||||||
|
|
||||||
|
html:
|
||||||
|
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
|
||||||
|
|
||||||
|
dirhtml:
|
||||||
|
$(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
|
||||||
|
|
||||||
|
singlehtml:
|
||||||
|
$(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
|
||||||
|
|
||||||
|
pickle:
|
||||||
|
$(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the pickle files."
|
||||||
|
|
||||||
|
json:
|
||||||
|
$(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can process the JSON files."
|
||||||
|
|
||||||
|
htmlhelp:
|
||||||
|
$(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run HTML Help Workshop with the" \
|
||||||
|
".hhp project file in $(BUILDDIR)/htmlhelp."
|
||||||
|
|
||||||
|
qthelp:
|
||||||
|
$(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||||
|
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||||
|
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/devpi.qhcp"
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/devpi.qhc"
|
||||||
|
|
||||||
|
devhelp:
|
||||||
|
$(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
|
||||||
|
@echo
|
||||||
|
@echo "Build finished."
|
||||||
|
@echo "To view the help file:"
|
||||||
|
@echo "# mkdir -p $$HOME/.local/share/devhelp/devpi"
|
||||||
|
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/devpi"
|
||||||
|
@echo "# devhelp"
|
||||||
|
|
||||||
|
epub:
|
||||||
|
$(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The epub file is in $(BUILDDIR)/epub."
|
||||||
|
|
||||||
|
latex:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo
|
||||||
|
@echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
|
||||||
|
@echo "Run \`make' in that directory to run these through (pdf)latex" \
|
||||||
|
"(use \`make latexpdf' here to do that automatically)."
|
||||||
|
|
||||||
|
latexpdf:
|
||||||
|
$(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
|
||||||
|
@echo "Running LaTeX files through pdflatex..."
|
||||||
|
$(MAKE) -C $(BUILDDIR)/latex all-pdf
|
||||||
|
@echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
|
||||||
|
|
||||||
|
text:
|
||||||
|
$(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The text files are in $(BUILDDIR)/text."
|
||||||
|
|
||||||
|
man:
|
||||||
|
$(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The manual pages are in $(BUILDDIR)/man."
|
||||||
|
|
||||||
|
texinfo:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
|
||||||
|
@echo "Run \`make' in that directory to run these through makeinfo" \
|
||||||
|
"(use \`make info' here to do that automatically)."
|
||||||
|
|
||||||
|
info:
|
||||||
|
$(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
|
||||||
|
@echo "Running Texinfo files through makeinfo..."
|
||||||
|
make -C $(BUILDDIR)/texinfo info
|
||||||
|
@echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
|
||||||
|
|
||||||
|
gettext:
|
||||||
|
$(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
|
||||||
|
@echo
|
||||||
|
@echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
|
||||||
|
|
||||||
|
changes:
|
||||||
|
$(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
|
||||||
|
@echo
|
||||||
|
@echo "The overview file is in $(BUILDDIR)/changes."
|
||||||
|
|
||||||
|
linkcheck:
|
||||||
|
$(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
|
||||||
|
@echo
|
||||||
|
@echo "Link check complete; look for any errors in the above output " \
|
||||||
|
"or in $(BUILDDIR)/linkcheck/output.txt."
|
||||||
|
|
||||||
|
doctest:
|
||||||
|
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
|
||||||
|
@echo "Testing of doctests in the sources finished, look at the " \
|
||||||
|
"results in $(BUILDDIR)/doctest/output.txt."
|
||||||
|
|
||||||
|
|
||||||
17
python/doc/_static/custom.css
vendored
Normal file
17
python/doc/_static/custom.css
vendored
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
/* customizations to Alabaster theme . */
|
||||||
|
|
||||||
|
div.document {
|
||||||
|
width: 1480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.body {
|
||||||
|
max-width: 1280px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.globaltoc {
|
||||||
|
font-size: 1.4em;
|
||||||
|
}
|
||||||
|
|
||||||
|
img.logo {
|
||||||
|
height: 120px;
|
||||||
|
}
|
||||||
167
python/doc/_static/delta-chat.svg
vendored
Executable file
167
python/doc/_static/delta-chat.svg
vendored
Executable file
@@ -0,0 +1,167 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||||
|
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
||||||
|
|
||||||
|
<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"
|
||||||
|
width="48px"
|
||||||
|
height="48px"
|
||||||
|
id="svg2985"
|
||||||
|
version="1.1"
|
||||||
|
inkscape:version="0.91 r13725"
|
||||||
|
sodipodi:docname="delta-v7-pathed.svg"
|
||||||
|
inkscape:export-filename="/home/bpetersen/projects/messenger-android/MessengerProj/src/main/res/drawable-xhdpi/ic_launcher.png"
|
||||||
|
inkscape:export-xdpi="183.83"
|
||||||
|
inkscape:export-ydpi="183.83">
|
||||||
|
<defs
|
||||||
|
id="defs2987">
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient4409">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#f9f9f9;stop-opacity:1"
|
||||||
|
offset="0"
|
||||||
|
id="stop4411" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#cccccc;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop4413" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient4399">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#f9f9f9;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop4401" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#f9f9f9;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop4403" />
|
||||||
|
</linearGradient>
|
||||||
|
<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>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient4367">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#dc000f;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop4369" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#00ff00;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop4371" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
id="linearGradient4359">
|
||||||
|
<stop
|
||||||
|
style="stop-color:#dc000f;stop-opacity:1;"
|
||||||
|
offset="0"
|
||||||
|
id="stop4361" />
|
||||||
|
<stop
|
||||||
|
style="stop-color:#000000;stop-opacity:0;"
|
||||||
|
offset="1"
|
||||||
|
id="stop4363" />
|
||||||
|
</linearGradient>
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient4375"
|
||||||
|
id="linearGradient4381"
|
||||||
|
x1="31.957268"
|
||||||
|
y1="29.751493"
|
||||||
|
x2="-45.041405"
|
||||||
|
y2="-18.591616"
|
||||||
|
gradientUnits="userSpaceOnUse"
|
||||||
|
gradientTransform="matrix(0.93766393,0,0,0.93766393,1.542566,1.7199693)" />
|
||||||
|
<linearGradient
|
||||||
|
inkscape:collect="always"
|
||||||
|
xlink:href="#linearGradient4409"
|
||||||
|
id="linearGradient4415"
|
||||||
|
x1="16.345125"
|
||||||
|
y1="3.8388948"
|
||||||
|
x2="36.001561"
|
||||||
|
y2="24.359164"
|
||||||
|
gradientUnits="userSpaceOnUse" />
|
||||||
|
</defs>
|
||||||
|
<sodipodi:namedview
|
||||||
|
id="base"
|
||||||
|
pagecolor="#ffffff"
|
||||||
|
bordercolor="#666666"
|
||||||
|
borderopacity="1.0"
|
||||||
|
inkscape:pageopacity="0.0"
|
||||||
|
inkscape:pageshadow="2"
|
||||||
|
inkscape:zoom="9.8994949"
|
||||||
|
inkscape:cx="1.9547978"
|
||||||
|
inkscape:cy="28.000232"
|
||||||
|
inkscape:current-layer="layer1"
|
||||||
|
showgrid="true"
|
||||||
|
inkscape:grid-bbox="true"
|
||||||
|
inkscape:document-units="px"
|
||||||
|
inkscape:snap-global="false"
|
||||||
|
inkscape:snap-bbox="true"
|
||||||
|
inkscape:bbox-nodes="true"
|
||||||
|
inkscape:bbox-paths="true"
|
||||||
|
inkscape:snap-bbox-edge-midpoints="true"
|
||||||
|
inkscape:window-width="1543"
|
||||||
|
inkscape:window-height="876"
|
||||||
|
inkscape:window-x="57"
|
||||||
|
inkscape:window-y="24"
|
||||||
|
inkscape:window-maximized="1" />
|
||||||
|
<metadata
|
||||||
|
id="metadata2990">
|
||||||
|
<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>
|
||||||
|
<g
|
||||||
|
id="layer1"
|
||||||
|
inkscape:label="Layer 1"
|
||||||
|
inkscape:groupmode="layer">
|
||||||
|
<path
|
||||||
|
style="fill:#ffffff;fill-opacity:1;stroke:#000000;stroke-width:0.57405078;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:0.43921569"
|
||||||
|
d="m 24.015419,1.2870249 c -12.549421,0 -22.7283936,10.1789711 -22.7283936,22.7283931 0,12.549422 10.1789726,22.728395 22.7283936,22.728395 14.337742,-0.342877 9.614352,-4.702705 23.697556,0.969161 -7.545453,-13.001555 -1.082973,-13.32964 -0.969161,-23.697556 0,-12.549422 -10.178973,-22.7283931 -22.728395,-22.7283931 z"
|
||||||
|
id="path3769"
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
sodipodi:nodetypes="sscccs" />
|
||||||
|
<path
|
||||||
|
inkscape:connector-curvature="0"
|
||||||
|
id="path3799"
|
||||||
|
d="M 23.982249,5.3106163 C 13.645822,5.4364005 5.2618355,13.92999 5.2618355,24.275753 c 0,10.345764 8.3839865,18.635301 18.7204135,18.509516 9.827724,-0.03951 7.516769,-5.489695 18.380082,-0.443187 -5.950849,-9.296115 0.201753,-10.533667 0.340336,-18.521947 0,-10.345766 -8.383989,-18.6353031 -18.720418,-18.5095187 z"
|
||||||
|
style="fill:url(#linearGradient4381);fill-opacity:1;stroke:none"
|
||||||
|
sodipodi:nodetypes="sscccs" />
|
||||||
|
<g
|
||||||
|
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
|
||||||
|
id="text4383" />
|
||||||
|
<g
|
||||||
|
style="font-style:normal;font-weight:normal;font-size:40px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#000000;fill-opacity:1;stroke:none"
|
||||||
|
id="text4421" />
|
||||||
|
<g
|
||||||
|
transform="scale(1.1122373,0.89908874)"
|
||||||
|
style="font-style:normal;font-weight:normal;font-size:42.10587311px;line-height:125%;font-family:Sans;letter-spacing:0px;word-spacing:0px;fill:#ffffff;fill-opacity:1;stroke:none"
|
||||||
|
id="text3797">
|
||||||
|
<path
|
||||||
|
d="m 21.688854,23.636251 q -1.027975,-1.151333 -2.857771,-2.754974 -2.014832,-1.768118 -2.713855,-2.775534 -0.699024,-1.027975 -0.699024,-2.240986 0,-1.809237 1.68588,-2.837212 1.68588,-1.048535 4.399735,-1.048535 2.713855,0 4.728687,0.925178 2.035391,0.925177 2.035391,2.549379 0,0.781261 -0.493428,1.295249 -0.493428,0.513987 -1.151333,0.513987 -0.945737,0 -2.220426,-1.418606 -1.295249,-1.439165 -2.199868,-2.014832 -0.884059,-0.596225 -2.07651,-0.596225 -1.521404,0 -2.50826,0.678463 -0.966297,0.678464 -0.966297,1.726999 0,0.986857 0.801821,1.850356 0.801821,0.863499 4.132461,3.145605 3.556795,2.446581 5.01652,3.824068 1.480285,1.377487 2.405462,3.3512 0.925178,1.973713 0.925178,4.17358 0,3.865188 -2.734414,6.825757 -2.713855,2.94001 -6.352888,2.94001 -3.310081,0 -5.592187,-2.364344 -2.282105,-2.364343 -2.282105,-6.311769 0,-3.803509 2.50826,-6.352888 2.528819,-2.549379 6.208971,-3.083926 z m 0.904619,0.945737 q -5.900579,0.966297 -5.900579,8.100447 0,3.680152 1.459725,5.715543 1.480285,2.035391 3.433438,2.035391 2.035391,0 3.3512,-1.953153 1.315808,-1.973713 1.315808,-5.324913 0,-4.852044 -3.659592,-8.573315 z"
|
||||||
|
style="font-family:'Times New Roman';-inkscape-font-specification:'Times New Roman';fill:#ffffff;fill-opacity:1"
|
||||||
|
id="path4161" />
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 7.0 KiB |
BIN
python/doc/_static/favicon.ico
vendored
Normal file
BIN
python/doc/_static/favicon.ico
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
20
python/doc/_templates/globaltoc.html
vendored
Normal file
20
python/doc/_templates/globaltoc.html
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
|
||||||
|
<div class="globaltoc">
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li><a href="{{ pathto('index') }}">index</a></li>
|
||||||
|
<li><a href="{{ pathto('install') }}">install</a></li>
|
||||||
|
<li><a href="{{ pathto('api') }}">high level API</a></li>
|
||||||
|
<li><a href="{{ pathto('lapi') }}">low level API</a></li>
|
||||||
|
<li><a href="{{ pathto('capi') }}">C deltachat.h</a></li>
|
||||||
|
</ul>
|
||||||
|
<b>external links:</b>
|
||||||
|
<ul>
|
||||||
|
<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>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<b>#deltachat [freenode]</b>
|
||||||
|
|
||||||
|
</div>
|
||||||
1
python/doc/_templates/sidebarintro.html
vendored
Normal file
1
python/doc/_templates/sidebarintro.html
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<h3>deltachat {{release}}</h3>
|
||||||
54
python/doc/api.rst
Normal file
54
python/doc/api.rst
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
|
||||||
|
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
|
||||||
|
other classes)
|
||||||
|
- :class:`deltachat.chatting.Contact`
|
||||||
|
- :class:`deltachat.chatting.Chat`
|
||||||
|
- :class:`deltachat.message.Message`
|
||||||
|
- :class:`deltachat.message.MessageType`
|
||||||
|
- :class:`deltachat.message.MessageState`
|
||||||
|
|
||||||
|
Account
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. autoclass:: deltachat.account.Account
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
|
Contact
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. autoclass:: deltachat.chatting.Contact
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Chat
|
||||||
|
----
|
||||||
|
|
||||||
|
.. autoclass:: deltachat.chatting.Chat
|
||||||
|
:members:
|
||||||
|
|
||||||
|
Message
|
||||||
|
-------
|
||||||
|
|
||||||
|
.. autoclass:: deltachat.message.Message
|
||||||
|
:members:
|
||||||
|
|
||||||
|
MessageType
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. autoclass:: deltachat.message.MessageType
|
||||||
|
:members:
|
||||||
|
|
||||||
|
MessageState
|
||||||
|
------------
|
||||||
|
|
||||||
|
.. autoclass:: deltachat.message.MessageState
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
||||||
7
python/doc/capi.rst
Normal file
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.
|
||||||
|
|
||||||
4
python/doc/changelog.rst
Normal file
4
python/doc/changelog.rst
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
Changelog for deltachat-core's Python bindings
|
||||||
|
==============================================
|
||||||
|
|
||||||
|
.. include:: ../CHANGELOG
|
||||||
299
python/doc/conf.py
Normal file
299
python/doc/conf.py
Normal file
@@ -0,0 +1,299 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
#
|
||||||
|
# devpi documentation build configuration file, created by
|
||||||
|
# sphinx-quickstart on Mon Jun 3 16:11:22 2013.
|
||||||
|
#
|
||||||
|
# This file is execfile()d with the current directory set to its containing dir.
|
||||||
|
#
|
||||||
|
# Note that not all possible configuration values are present in this
|
||||||
|
# autogenerated file.
|
||||||
|
#
|
||||||
|
# All configuration values have a default; values that are commented out
|
||||||
|
# serve to show the default.
|
||||||
|
|
||||||
|
import sys, os
|
||||||
|
|
||||||
|
# The version info for the project you're documenting, acts as replacement for
|
||||||
|
# |version| and |release|, also used in various other places throughout the
|
||||||
|
# built documents.
|
||||||
|
from deltachat import __version__ as release
|
||||||
|
version = ".".join(release.split(".")[:2])
|
||||||
|
|
||||||
|
# If extensions (or modules to document with autodoc) are in another directory,
|
||||||
|
# add these directories to sys.path here. If the directory is relative to the
|
||||||
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
|
#sys.path.insert(0, os.path.abspath('.'))
|
||||||
|
|
||||||
|
# -- General configuration -----------------------------------------------------
|
||||||
|
|
||||||
|
# If your documentation needs a minimal Sphinx version, state it here.
|
||||||
|
#needs_sphinx = '1.0'
|
||||||
|
|
||||||
|
# Add any Sphinx extension module names here, as strings. They can be extensions
|
||||||
|
# coming with Sphinx (named 'sphinx.ext.*') or your custom ones.
|
||||||
|
extensions = [
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinx.ext.autosummary',
|
||||||
|
#'sphinx.ext.intersphinx',
|
||||||
|
'sphinx.ext.todo',
|
||||||
|
'sphinx.ext.viewcode',
|
||||||
|
'breathe',
|
||||||
|
#'sphinx.ext.githubpages',
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
templates_path = ['_templates']
|
||||||
|
|
||||||
|
# The suffix of source filenames.
|
||||||
|
source_suffix = '.rst'
|
||||||
|
|
||||||
|
# The encoding of source files.
|
||||||
|
#source_encoding = 'utf-8-sig'
|
||||||
|
|
||||||
|
# The master toctree document.
|
||||||
|
master_doc = 'index'
|
||||||
|
|
||||||
|
# General information about the project.
|
||||||
|
project = u'deltachat'
|
||||||
|
copyright = u'2018, holger krekel and contributors'
|
||||||
|
|
||||||
|
|
||||||
|
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||||
|
# for a list of supported languages.
|
||||||
|
#language = None
|
||||||
|
|
||||||
|
# There are two options for replacing |today|: either, you set today to some
|
||||||
|
# non-false value, then it is used:
|
||||||
|
#today = ''
|
||||||
|
# Else, today_fmt is used as the format for a strftime call.
|
||||||
|
#today_fmt = '%B %d, %Y'
|
||||||
|
|
||||||
|
# List of patterns, relative to source directory, that match files and
|
||||||
|
# directories to ignore when looking for source files.
|
||||||
|
exclude_patterns = ['sketch', '_build', "attic"]
|
||||||
|
|
||||||
|
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||||
|
#default_role = None
|
||||||
|
|
||||||
|
# If true, '()' will be appended to :func: etc. cross-reference text.
|
||||||
|
#add_function_parentheses = True
|
||||||
|
|
||||||
|
# If true, the current module name will be prepended to all description
|
||||||
|
# unit titles (such as .. function::).
|
||||||
|
#add_module_names = True
|
||||||
|
|
||||||
|
# If true, sectionauthor and moduleauthor directives will be shown in the
|
||||||
|
# output. They are ignored by default.
|
||||||
|
#show_authors = False
|
||||||
|
|
||||||
|
# The name of the Pygments (syntax highlighting) style to use.
|
||||||
|
pygments_style = 'sphinx'
|
||||||
|
|
||||||
|
# A list of ignored prefixes for module index sorting.
|
||||||
|
#modindex_common_prefix = []
|
||||||
|
|
||||||
|
|
||||||
|
# -- breathe options ------
|
||||||
|
|
||||||
|
breathe_projects = {
|
||||||
|
"deltachat": "../../docs/xml/"
|
||||||
|
}
|
||||||
|
|
||||||
|
breathe_default_project = "deltachat"
|
||||||
|
|
||||||
|
# -- Options for HTML output ---------------------------------------------------
|
||||||
|
|
||||||
|
sys.path.append(os.path.abspath('_themes'))
|
||||||
|
html_theme_path = ['_themes']
|
||||||
|
|
||||||
|
# The theme to use for HTML and HTML Help pages. See the documentation for
|
||||||
|
# a list of builtin themes.
|
||||||
|
# html_theme = 'flask'
|
||||||
|
html_theme = 'alabaster'
|
||||||
|
|
||||||
|
# Theme options are theme-specific and customize the look and feel of a theme
|
||||||
|
# further. For a list of options available for each theme, see the
|
||||||
|
# documentation.
|
||||||
|
html_theme_options = {
|
||||||
|
'logo': '_static/delta-chat.svg',
|
||||||
|
'font_size': "1.1em",
|
||||||
|
'caption_font_size': "0.9em",
|
||||||
|
'code_font_size': "1.1em",
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
# Add any paths that contain custom themes here, relative to this directory.
|
||||||
|
#html_theme_path = ["_themes"]
|
||||||
|
|
||||||
|
# The name for this set of Sphinx documents. If None, it defaults to
|
||||||
|
# "<project> v<release> documentation".
|
||||||
|
#html_title = None
|
||||||
|
|
||||||
|
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||||
|
#html_short_title = None
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top
|
||||||
|
# of the sidebar.
|
||||||
|
html_logo = "_static/delta-chat.svg"
|
||||||
|
|
||||||
|
# The name of an image file (within the static path) to use as favicon of the
|
||||||
|
# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32
|
||||||
|
# pixels large.
|
||||||
|
html_favicon = '_static/favicon.ico'
|
||||||
|
|
||||||
|
# Add any paths that contain custom static files (such as style sheets) here,
|
||||||
|
# relative to this directory. They are copied after the builtin static files,
|
||||||
|
# so a file named "default.css" will overwrite the builtin "default.css".
|
||||||
|
html_static_path = ['_static']
|
||||||
|
|
||||||
|
# If not '', a 'Last updated on:' timestamp is inserted at every page bottom,
|
||||||
|
# using the given strftime format.
|
||||||
|
#html_last_updated_fmt = '%b %d, %Y'
|
||||||
|
|
||||||
|
# If true, SmartyPants will be used to convert quotes and dashes to
|
||||||
|
# typographically correct entities.
|
||||||
|
#html_use_smartypants = True
|
||||||
|
|
||||||
|
# Custom sidebar templates, maps document names to template names.
|
||||||
|
#
|
||||||
|
html_sidebars = {
|
||||||
|
'index': [
|
||||||
|
'sidebarintro.html',
|
||||||
|
'globaltoc.html',
|
||||||
|
'searchbox.html'
|
||||||
|
],
|
||||||
|
'**': [
|
||||||
|
'sidebarintro.html',
|
||||||
|
'globaltoc.html',
|
||||||
|
'relations.html',
|
||||||
|
'searchbox.html'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# Additional templates that should be rendered to pages, maps page names to
|
||||||
|
# template names.
|
||||||
|
#html_additional_pages = {}
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#html_domain_indices = True
|
||||||
|
|
||||||
|
# If false, no index is generated.
|
||||||
|
#html_use_index = True
|
||||||
|
|
||||||
|
# If true, the index is split into individual pages for each letter.
|
||||||
|
#html_split_index = False
|
||||||
|
|
||||||
|
# If true, links to the reST sources are added to the pages.
|
||||||
|
html_show_sourcelink = False
|
||||||
|
|
||||||
|
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||||
|
html_show_sphinx = False
|
||||||
|
|
||||||
|
# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True.
|
||||||
|
#html_show_copyright = True
|
||||||
|
|
||||||
|
# If true, an OpenSearch description file will be output, and all pages will
|
||||||
|
# contain a <link> tag referring to it. The value of this option must be the
|
||||||
|
# base URL from which the finished HTML is served.
|
||||||
|
html_use_opensearch = 'https://doc.devpi.net'
|
||||||
|
|
||||||
|
# This is the file name suffix for HTML files (e.g. ".xhtml").
|
||||||
|
#html_file_suffix = None
|
||||||
|
|
||||||
|
# Output file base name for HTML help builder.
|
||||||
|
htmlhelp_basename = 'deltachat-python'
|
||||||
|
|
||||||
|
# -- Options for LaTeX output --------------------------------------------------
|
||||||
|
|
||||||
|
latex_elements = {
|
||||||
|
# The paper size ('letterpaper' or 'a4paper').
|
||||||
|
#'papersize': 'letterpaper',
|
||||||
|
|
||||||
|
# The font size ('10pt', '11pt' or '12pt').
|
||||||
|
'pointsize': '12pt',
|
||||||
|
|
||||||
|
# Additional stuff for the LaTeX preamble.
|
||||||
|
#'preamble': '',
|
||||||
|
}
|
||||||
|
|
||||||
|
# Grouping the document tree into LaTeX files. List of tuples
|
||||||
|
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||||
|
latex_documents = [
|
||||||
|
('index', 'devpi.tex', u'deltachat documentation',
|
||||||
|
u'holger krekel', 'manual'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# The name of an image file (relative to this directory) to place at the top of
|
||||||
|
# the title page.
|
||||||
|
#latex_logo = None
|
||||||
|
|
||||||
|
# For "manual" documents, if this is true, then toplevel headings are parts,
|
||||||
|
# not chapters.
|
||||||
|
#latex_use_parts = False
|
||||||
|
|
||||||
|
# If true, show page references after internal links.
|
||||||
|
#latex_show_pagerefs = False
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#latex_show_urls = False
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#latex_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#latex_domain_indices = True
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for manual page output --------------------------------------------
|
||||||
|
|
||||||
|
# One entry per manual page. List of tuples
|
||||||
|
# (source start file, name, description, authors, manual section).
|
||||||
|
man_pages = [
|
||||||
|
('index', 'deltachat', u'deltachat documentation',
|
||||||
|
[u'holger krekel'], 1)
|
||||||
|
]
|
||||||
|
|
||||||
|
# If true, show URL addresses after external links.
|
||||||
|
#man_show_urls = False
|
||||||
|
|
||||||
|
|
||||||
|
# -- Options for Texinfo output ------------------------------------------------
|
||||||
|
|
||||||
|
# Grouping the document tree into Texinfo files. List of tuples
|
||||||
|
# (source start file, target name, title, author,
|
||||||
|
# dir menu entry, description, category)
|
||||||
|
texinfo_documents = [
|
||||||
|
('index', 'devpi', u'devpi Documentation',
|
||||||
|
u'holger krekel', 'devpi', 'One line description of project.',
|
||||||
|
'Miscellaneous'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# Documents to append as an appendix to all manuals.
|
||||||
|
#texinfo_appendices = []
|
||||||
|
|
||||||
|
# If false, no module index is generated.
|
||||||
|
#texinfo_domain_indices = True
|
||||||
|
|
||||||
|
# How to display URL addresses: 'footnote', 'no', or 'inline'.
|
||||||
|
#texinfo_show_urls = 'footnote'
|
||||||
|
|
||||||
|
|
||||||
|
# Example configuration for intersphinx: refer to the Python standard library.
|
||||||
|
intersphinx_mapping = {'http://docs.python.org/': None}
|
||||||
|
|
||||||
|
# autodoc options
|
||||||
|
autodoc_member_order = "bysource"
|
||||||
|
# always document __init__ functions
|
||||||
|
def skip(app, what, name, obj, skip, options):
|
||||||
|
import attr
|
||||||
|
if name == "__init__":
|
||||||
|
if not hasattr(obj.im_class, "__attrs_attrs__"):
|
||||||
|
return False
|
||||||
|
return skip
|
||||||
|
|
||||||
|
def setup(app):
|
||||||
|
app.connect("autodoc-skip-member", skip)
|
||||||
|
|
||||||
38
python/doc/examples.rst
Normal file
38
python/doc/examples.rst
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
|
||||||
|
|
||||||
|
examples
|
||||||
|
========
|
||||||
|
|
||||||
|
|
||||||
|
Playing around on the commandline
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
Once you have :doc:`installed deltachat bindings <install>`
|
||||||
|
you can start playing from the python interpreter commandline::
|
||||||
|
|
||||||
|
For example you can type ``python`` and then::
|
||||||
|
|
||||||
|
# instantiate and configure deltachat account
|
||||||
|
import deltachat
|
||||||
|
ac = deltachat.Account("/tmp/db")
|
||||||
|
|
||||||
|
# start configuration activity and smtp/imap threads
|
||||||
|
ac.start_threads()
|
||||||
|
ac.configure(addr="test2@hq5.merlinux.eu", mail_pw="********")
|
||||||
|
|
||||||
|
# create a contact and send a message
|
||||||
|
contact = ac.create_contact("someother@email.address")
|
||||||
|
chat = ac.create_chat_by_contact(contact)
|
||||||
|
chat.send_text_message("hi from the python interpreter command line")
|
||||||
|
|
||||||
|
Checkout our :doc:`api` for the various high-level things you can do
|
||||||
|
to send/receive messages, create contacts and chats.
|
||||||
|
|
||||||
|
|
||||||
|
Looking at a real example
|
||||||
|
-------------------------
|
||||||
|
|
||||||
|
The `deltabot repository <https://github.com/deltachat/deltabot#deltachat-example-bot>`_
|
||||||
|
contains a real-life example of Python bindings usage.
|
||||||
|
|
||||||
|
|
||||||
39
python/doc/index.rst
Normal file
39
python/doc/index.rst
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
deltachat python bindings
|
||||||
|
=========================
|
||||||
|
|
||||||
|
The ``deltachat`` Python package provides two bindings for the core C-library
|
||||||
|
of the https://delta.chat messaging ecosystem:
|
||||||
|
|
||||||
|
- :doc:`capi` is a lowlevel CFFI-binding to the
|
||||||
|
`deltachat-core C-API <https://c.delta.chat>`_.
|
||||||
|
|
||||||
|
- :doc:`api` [work-in-progress] is a high level interface to deltachat-core which aims
|
||||||
|
to be memory safe and thoroughly tested through continous tox/pytest runs.
|
||||||
|
|
||||||
|
|
||||||
|
getting started
|
||||||
|
---------------
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:maxdepth: 2
|
||||||
|
|
||||||
|
install
|
||||||
|
examples
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
:hidden:
|
||||||
|
|
||||||
|
links
|
||||||
|
changelog
|
||||||
|
api
|
||||||
|
capi
|
||||||
|
lapi
|
||||||
|
|
||||||
|
..
|
||||||
|
Indices and tables
|
||||||
|
==================
|
||||||
|
|
||||||
|
* :ref:`genindex`
|
||||||
|
* :ref:`modindex`
|
||||||
|
* :ref:`search`
|
||||||
|
|
||||||
2
python/doc/install.rst
Normal file
2
python/doc/install.rst
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
.. include:: ../README.rst
|
||||||
10
python/doc/lapi.rst
Normal file
10
python/doc/lapi.rst
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
|
||||||
|
low level API reference
|
||||||
|
===================================
|
||||||
|
|
||||||
|
for full C-docs, defines and function checkout :doc:`capi`
|
||||||
|
|
||||||
|
|
||||||
|
.. automodule:: deltachat.capi.lib
|
||||||
|
:members:
|
||||||
|
|
||||||
11
python/doc/links.rst
Normal file
11
python/doc/links.rst
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
|
||||||
|
links
|
||||||
|
================================
|
||||||
|
|
||||||
|
.. _`deltachat`: https://delta.chat
|
||||||
|
.. _`deltachat-core repo`: https://github.com/deltachat
|
||||||
|
.. _pip: http://pypi.org/project/pip/
|
||||||
|
.. _virtualenv: http://pypi.org/project/virtualenv/
|
||||||
|
.. _merlinux: http://merlinux.eu
|
||||||
|
.. _pypi: http://pypi.org/
|
||||||
|
.. _`issue-tracker`: https://github.com/deltachat/deltachat-core
|
||||||
190
python/doc/make.bat
Normal file
190
python/doc/make.bat
Normal file
@@ -0,0 +1,190 @@
|
|||||||
|
@ECHO OFF
|
||||||
|
|
||||||
|
REM Command file for Sphinx documentation
|
||||||
|
|
||||||
|
if "%SPHINXBUILD%" == "" (
|
||||||
|
set SPHINXBUILD=sphinx-build
|
||||||
|
)
|
||||||
|
set BUILDDIR=_build
|
||||||
|
set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% .
|
||||||
|
set I18NSPHINXOPTS=%SPHINXOPTS% .
|
||||||
|
if NOT "%PAPER%" == "" (
|
||||||
|
set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS%
|
||||||
|
set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS%
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "" goto help
|
||||||
|
|
||||||
|
if "%1" == "help" (
|
||||||
|
:help
|
||||||
|
echo.Please use `make ^<target^>` where ^<target^> is one of
|
||||||
|
echo. html to make standalone HTML files
|
||||||
|
echo. dirhtml to make HTML files named index.html in directories
|
||||||
|
echo. singlehtml to make a single large HTML file
|
||||||
|
echo. pickle to make pickle files
|
||||||
|
echo. json to make JSON files
|
||||||
|
echo. htmlhelp to make HTML files and a HTML help project
|
||||||
|
echo. qthelp to make HTML files and a qthelp project
|
||||||
|
echo. devhelp to make HTML files and a Devhelp project
|
||||||
|
echo. epub to make an epub
|
||||||
|
echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter
|
||||||
|
echo. text to make text files
|
||||||
|
echo. man to make manual pages
|
||||||
|
echo. texinfo to make Texinfo files
|
||||||
|
echo. gettext to make PO message catalogs
|
||||||
|
echo. changes to make an overview over all changed/added/deprecated items
|
||||||
|
echo. linkcheck to check all external links for integrity
|
||||||
|
echo. doctest to run all doctests embedded in the documentation if enabled
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "clean" (
|
||||||
|
for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i
|
||||||
|
del /q /s %BUILDDIR%\*
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "html" (
|
||||||
|
%SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/html.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "dirhtml" (
|
||||||
|
%SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "singlehtml" (
|
||||||
|
%SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "pickle" (
|
||||||
|
%SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can process the pickle files.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "json" (
|
||||||
|
%SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can process the JSON files.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "htmlhelp" (
|
||||||
|
%SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can run HTML Help Workshop with the ^
|
||||||
|
.hhp project file in %BUILDDIR%/htmlhelp.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "qthelp" (
|
||||||
|
%SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||||
|
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||||
|
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\devpi.qhcp
|
||||||
|
echo.To view the help file:
|
||||||
|
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\devpi.ghc
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "devhelp" (
|
||||||
|
%SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "epub" (
|
||||||
|
%SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The epub file is in %BUILDDIR%/epub.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "latex" (
|
||||||
|
%SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished; the LaTeX files are in %BUILDDIR%/latex.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "text" (
|
||||||
|
%SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The text files are in %BUILDDIR%/text.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "man" (
|
||||||
|
%SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The manual pages are in %BUILDDIR%/man.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "texinfo" (
|
||||||
|
%SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "gettext" (
|
||||||
|
%SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Build finished. The message catalogs are in %BUILDDIR%/locale.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "changes" (
|
||||||
|
%SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.The overview file is in %BUILDDIR%/changes.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "linkcheck" (
|
||||||
|
%SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Link check complete; look for any errors in the above output ^
|
||||||
|
or in %BUILDDIR%/linkcheck/output.txt.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
if "%1" == "doctest" (
|
||||||
|
%SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest
|
||||||
|
if errorlevel 1 exit /b 1
|
||||||
|
echo.
|
||||||
|
echo.Testing of doctests in the sources finished, look at the ^
|
||||||
|
results in %BUILDDIR%/doctest/output.txt.
|
||||||
|
goto end
|
||||||
|
)
|
||||||
|
|
||||||
|
:end
|
||||||
14
python/install_py_bindings.sh
Executable file
14
python/install_py_bindings.sh
Executable file
@@ -0,0 +1,14 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -ex
|
||||||
|
|
||||||
|
#cd ..
|
||||||
|
#cargo build -p deltachat_ffi --release
|
||||||
|
#cd python
|
||||||
|
|
||||||
|
export CFLAGS=-I`realpath ../deltachat-ffi`
|
||||||
|
# the followine line results in "libdeltachat.so" not found
|
||||||
|
# export LDFLAGS='-Wl,-rpath=$ORIGIN/../target/release -Wl,--enable-new-dtags'
|
||||||
|
pip install -e .
|
||||||
|
export LD_LIBRARY_PATH=`realpath ../target/release`
|
||||||
|
python -c "import deltachat"
|
||||||
4
python/setup.cfg
Normal file
4
python/setup.cfg
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[devpi:upload]
|
||||||
|
formats = sdist.tgz
|
||||||
|
no-vcs = 1
|
||||||
|
|
||||||
44
python/setup.py
Normal file
44
python/setup.py
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import setuptools
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
long_description, version = read_meta()
|
||||||
|
setuptools.setup(
|
||||||
|
name='deltachat',
|
||||||
|
version=version,
|
||||||
|
description='Python bindings for deltachat-core using CFFI',
|
||||||
|
long_description=long_description,
|
||||||
|
author='holger krekel, Floris Bruynooghe, Bjoern Petersen and contributors',
|
||||||
|
setup_requires=['cffi>=1.0.0'],
|
||||||
|
install_requires=['cffi>=1.0.0', 'requests', 'attrs', 'six'],
|
||||||
|
packages=setuptools.find_packages('src'),
|
||||||
|
package_dir={'': 'src'},
|
||||||
|
cffi_modules=['src/deltachat/_build.py:ffibuilder'],
|
||||||
|
classifiers=[
|
||||||
|
'Development Status :: 4 - Beta',
|
||||||
|
'Intended Audience :: Developers',
|
||||||
|
'License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0)',
|
||||||
|
'Programming Language :: Python :: 3',
|
||||||
|
'Topic :: Communications :: Email',
|
||||||
|
'Topic :: Software Development :: Libraries',
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def read_meta():
|
||||||
|
with open(os.path.join("src", "deltachat", "__init__.py")) as f:
|
||||||
|
for line in f:
|
||||||
|
m = re.match('__version__ = "(\S*).*"', line)
|
||||||
|
if m:
|
||||||
|
version, = m.groups()
|
||||||
|
break
|
||||||
|
|
||||||
|
with open("README.rst") as f:
|
||||||
|
long_desc = f.read()
|
||||||
|
return long_desc, version
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
64
python/src/deltachat/__init__.py
Normal file
64
python/src/deltachat/__init__.py
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
from deltachat import capi, const
|
||||||
|
from deltachat.capi import ffi
|
||||||
|
from deltachat.account import Account # noqa
|
||||||
|
|
||||||
|
__version__ = "0.10.0dev1"
|
||||||
|
|
||||||
|
|
||||||
|
_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:
|
||||||
|
try:
|
||||||
|
data2 = ffi.string(ffi.cast('char*', data2)).decode("utf8")
|
||||||
|
except UnicodeDecodeError:
|
||||||
|
# XXX ignoring this error is not quite correct but for now
|
||||||
|
# i don't want to hunt down encoding problems in the c lib
|
||||||
|
data2 = ffi.string(ffi.cast('char*', data2))
|
||||||
|
|
||||||
|
try:
|
||||||
|
ret = callback(ctx, evt_name, data1, data2)
|
||||||
|
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):
|
||||||
|
_DC_CALLBACK_MAP.pop(dc_context, None)
|
||||||
|
|
||||||
|
|
||||||
|
def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
|
||||||
|
if not _DC_EVENTNAME_MAP:
|
||||||
|
for name, val in vars(const).items():
|
||||||
|
if name.startswith("DC_EVENT_"):
|
||||||
|
_DC_EVENTNAME_MAP[val] = name
|
||||||
|
return _DC_EVENTNAME_MAP[integer]
|
||||||
73
python/src/deltachat/_build.py
Normal file
73
python/src/deltachat/_build.py
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import distutils.ccompiler
|
||||||
|
import distutils.sysconfig
|
||||||
|
import tempfile
|
||||||
|
import os
|
||||||
|
import cffi
|
||||||
|
|
||||||
|
# XXX hack out the header and library dirs
|
||||||
|
# relying on CFLAGS and LD_LIBRARY_PATH being set properly is not good
|
||||||
|
# (but we also don't want to rely on global installs of headers and libs)
|
||||||
|
HEADERDIR = os.environ["CFLAGS"].split("-I", 1)[1]
|
||||||
|
LIBDIR = os.environ["LD_LIBRARY_PATH"]
|
||||||
|
|
||||||
|
|
||||||
|
def ffibuilder():
|
||||||
|
builder = cffi.FFI()
|
||||||
|
builder.set_source(
|
||||||
|
'deltachat.capi',
|
||||||
|
"""
|
||||||
|
#include <deltachat.h>
|
||||||
|
const char * dupstring_helper(const char* string)
|
||||||
|
{
|
||||||
|
return strdup(string);
|
||||||
|
}
|
||||||
|
int dc_get_event_signature_types(int e)
|
||||||
|
{
|
||||||
|
int result = 0;
|
||||||
|
if (DC_EVENT_DATA1_IS_STRING(e))
|
||||||
|
result |= 1;
|
||||||
|
if (DC_EVENT_DATA2_IS_STRING(e))
|
||||||
|
result |= 2;
|
||||||
|
if (DC_EVENT_RETURNS_STRING(e))
|
||||||
|
result |= 4;
|
||||||
|
if (DC_EVENT_RETURNS_INT(e))
|
||||||
|
result |= 8;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
""",
|
||||||
|
libraries=['deltachat'],
|
||||||
|
include_dirs=[HEADERDIR],
|
||||||
|
library_dirs=[LIBDIR],
|
||||||
|
)
|
||||||
|
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);
|
||||||
|
""")
|
||||||
|
cc = distutils.ccompiler.new_compiler(force=True)
|
||||||
|
distutils.sysconfig.customize_compiler(cc)
|
||||||
|
with tempfile.NamedTemporaryFile(mode='w', suffix='.h') as src_fp:
|
||||||
|
src_fp.write('#include <deltachat.h>')
|
||||||
|
src_fp.flush()
|
||||||
|
with tempfile.NamedTemporaryFile(mode='r') as dst_fp:
|
||||||
|
cc.preprocess(source=src_fp.name,
|
||||||
|
output_file=dst_fp.name,
|
||||||
|
include_dirs=[HEADERDIR],
|
||||||
|
macros=[('PY_CFFI', '1')])
|
||||||
|
builder.cdef(dst_fp.read())
|
||||||
|
builder.cdef("""
|
||||||
|
extern "Python" uintptr_t py_dc_callback(
|
||||||
|
dc_context_t* context,
|
||||||
|
int event,
|
||||||
|
uintptr_t data1,
|
||||||
|
uintptr_t data2);
|
||||||
|
""")
|
||||||
|
return builder
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
import os.path
|
||||||
|
pkgdir = os.path.join(os.path.dirname(__file__), '..')
|
||||||
|
builder = ffibuilder()
|
||||||
|
builder.compile(tmpdir=pkgdir, verbose=True)
|
||||||
429
python/src/deltachat/account.py
Normal file
429
python/src/deltachat/account.py
Normal file
@@ -0,0 +1,429 @@
|
|||||||
|
""" Account class implementation. """
|
||||||
|
|
||||||
|
from __future__ import print_function
|
||||||
|
import threading
|
||||||
|
import re
|
||||||
|
import time
|
||||||
|
import requests
|
||||||
|
from array import array
|
||||||
|
try:
|
||||||
|
from queue import Queue
|
||||||
|
except ImportError:
|
||||||
|
from Queue import Queue
|
||||||
|
import attr
|
||||||
|
from attr import validators as v
|
||||||
|
|
||||||
|
import deltachat
|
||||||
|
from . import const
|
||||||
|
from .capi import ffi, lib
|
||||||
|
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
|
||||||
|
from .chatting import Contact, Chat, Message
|
||||||
|
|
||||||
|
|
||||||
|
class Account(object):
|
||||||
|
""" Each account is tied to a sqlite database file which is fully managed
|
||||||
|
by the underlying deltachat c-library. All public Account methods are
|
||||||
|
meant to be memory-safe and return memory-safe objects.
|
||||||
|
"""
|
||||||
|
def __init__(self, db_path, logid=None):
|
||||||
|
""" initialize account object.
|
||||||
|
|
||||||
|
:param db_path: a path to the account database. The database
|
||||||
|
will be created if it doesn't exist.
|
||||||
|
:param logid: an optional logging prefix that should be used with
|
||||||
|
the default internal logging.
|
||||||
|
"""
|
||||||
|
self._dc_context = ffi.gc(
|
||||||
|
lib.dc_context_new(lib.py_dc_callback, ffi.NULL, ffi.NULL),
|
||||||
|
_destroy_dc_context,
|
||||||
|
)
|
||||||
|
if hasattr(db_path, "encode"):
|
||||||
|
db_path = db_path.encode("utf8")
|
||||||
|
if not lib.dc_open(self._dc_context, db_path, ffi.NULL):
|
||||||
|
raise ValueError("Could not dc_open: {}".format(db_path))
|
||||||
|
self._evhandler = EventHandler(self._dc_context)
|
||||||
|
self._evlogger = EventLogger(self._dc_context, logid)
|
||||||
|
deltachat.set_context_callback(self._dc_context, self._process_event)
|
||||||
|
self._threads = IOThreads(self._dc_context)
|
||||||
|
self._configkeys = self.get_config("sys.config_keys").split()
|
||||||
|
|
||||||
|
def _check_config_key(self, name):
|
||||||
|
if name not in self._configkeys:
|
||||||
|
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(
|
||||||
|
name, self._configkeys))
|
||||||
|
|
||||||
|
def get_info(self):
|
||||||
|
""" return dictionary of built config parameters. """
|
||||||
|
lines = from_dc_charpointer(lib.dc_get_info(self._dc_context))
|
||||||
|
d = {}
|
||||||
|
for line in lines.split("\n"):
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
key, value = line.split("=", 1)
|
||||||
|
d[key.lower()] = value
|
||||||
|
return d
|
||||||
|
|
||||||
|
def set_config(self, name, value):
|
||||||
|
""" set configuration values.
|
||||||
|
|
||||||
|
:param name: config key name (unicode)
|
||||||
|
:param value: value to set (unicode)
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
self._check_config_key(name)
|
||||||
|
name = name.encode("utf8")
|
||||||
|
value = value.encode("utf8")
|
||||||
|
if name == b"addr" and self.is_configured():
|
||||||
|
raise ValueError("can not change 'addr' after account is configured.")
|
||||||
|
lib.dc_set_config(self._dc_context, name, value)
|
||||||
|
|
||||||
|
def get_config(self, name):
|
||||||
|
""" return unicode string value.
|
||||||
|
|
||||||
|
:param name: configuration key to lookup (eg "addr" or "mail_pw")
|
||||||
|
:returns: unicode value
|
||||||
|
:raises: KeyError if no config value was found.
|
||||||
|
"""
|
||||||
|
if name != "sys.config_keys":
|
||||||
|
self._check_config_key(name)
|
||||||
|
name = name.encode("utf8")
|
||||||
|
res = lib.dc_get_config(self._dc_context, name)
|
||||||
|
assert res != ffi.NULL, "config value not found for: {!r}".format(name)
|
||||||
|
return from_dc_charpointer(res)
|
||||||
|
|
||||||
|
def configure(self, **kwargs):
|
||||||
|
""" set config values and configure this account.
|
||||||
|
|
||||||
|
:param kwargs: name=value config settings for this account.
|
||||||
|
values need to be unicode.
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
for name, value in kwargs.items():
|
||||||
|
self.set_config(name, value)
|
||||||
|
lib.dc_configure(self._dc_context)
|
||||||
|
|
||||||
|
def is_configured(self):
|
||||||
|
""" determine if the account is configured already; an initial connection
|
||||||
|
to SMTP/IMAP has been verified.
|
||||||
|
|
||||||
|
:returns: True if account is configured.
|
||||||
|
"""
|
||||||
|
return lib.dc_is_configured(self._dc_context)
|
||||||
|
|
||||||
|
def check_is_configured(self):
|
||||||
|
""" Raise ValueError if this account is not configured. """
|
||||||
|
if not self.is_configured():
|
||||||
|
raise ValueError("need to configure first")
|
||||||
|
|
||||||
|
def get_infostring(self):
|
||||||
|
""" return info of the configured account. """
|
||||||
|
self.check_is_configured()
|
||||||
|
return from_dc_charpointer(lib.dc_get_info(self._dc_context))
|
||||||
|
|
||||||
|
def get_blobdir(self):
|
||||||
|
""" return the directory for files.
|
||||||
|
|
||||||
|
All sent files are copied to this directory if necessary.
|
||||||
|
Place files there directly to avoid copying.
|
||||||
|
"""
|
||||||
|
return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context))
|
||||||
|
|
||||||
|
def get_self_contact(self):
|
||||||
|
""" return this account's identity as a :class:`deltachat.chatting.Contact`.
|
||||||
|
|
||||||
|
:returns: :class:`deltachat.chatting.Contact`
|
||||||
|
"""
|
||||||
|
self.check_is_configured()
|
||||||
|
return Contact(self._dc_context, const.DC_CONTACT_ID_SELF)
|
||||||
|
|
||||||
|
def create_message(self, view_type):
|
||||||
|
""" create a new non persistent message.
|
||||||
|
|
||||||
|
:param view_type: a string specifying "text", "video",
|
||||||
|
"image", "audio" or "file".
|
||||||
|
:returns: :class:`deltachat.message.Message` instance.
|
||||||
|
"""
|
||||||
|
return Message.new(self._dc_context, view_type)
|
||||||
|
|
||||||
|
def create_contact(self, email, name=None):
|
||||||
|
""" create a (new) Contact. If there already is a Contact
|
||||||
|
with that e-mail address, it is unblocked and its name is
|
||||||
|
updated.
|
||||||
|
|
||||||
|
:param email: email-address (text type)
|
||||||
|
:param name: display name for this contact (optional)
|
||||||
|
:returns: :class:`deltachat.chatting.Contact` instance.
|
||||||
|
"""
|
||||||
|
name = as_dc_charpointer(name)
|
||||||
|
email = as_dc_charpointer(email)
|
||||||
|
contact_id = lib.dc_create_contact(self._dc_context, name, email)
|
||||||
|
assert contact_id > const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
|
return Contact(self._dc_context, contact_id)
|
||||||
|
|
||||||
|
def get_contacts(self, query=None, with_self=False, only_verified=False):
|
||||||
|
""" get a (filtered) list of contacts.
|
||||||
|
|
||||||
|
:param query: if a string is specified, only return contacts
|
||||||
|
whose name or e-mail matches query.
|
||||||
|
:param only_verified: if true only return verified contacts.
|
||||||
|
:param with_self: if true the self-contact is also returned.
|
||||||
|
:returns: list of :class:`deltachat.message.Message` objects.
|
||||||
|
"""
|
||||||
|
flags = 0
|
||||||
|
query = as_dc_charpointer(query)
|
||||||
|
if only_verified:
|
||||||
|
flags |= const.DC_GCL_VERIFIED_ONLY
|
||||||
|
if with_self:
|
||||||
|
flags |= const.DC_GCL_ADD_SELF
|
||||||
|
dc_array = ffi.gc(
|
||||||
|
lib.dc_get_contacts(self._dc_context, flags, query),
|
||||||
|
lib.dc_array_unref
|
||||||
|
)
|
||||||
|
return list(iter_array(dc_array, lambda x: Contact(self._dc_context, x)))
|
||||||
|
|
||||||
|
def create_chat_by_contact(self, contact):
|
||||||
|
""" create or get an existing 1:1 chat object for the specified contact.
|
||||||
|
|
||||||
|
:param contact: chat_id (int) or contact object.
|
||||||
|
:returns: a :class:`deltachat.chatting.Chat` object.
|
||||||
|
"""
|
||||||
|
contact_id = getattr(contact, "id", contact)
|
||||||
|
assert isinstance(contact_id, int)
|
||||||
|
chat_id = lib.dc_create_chat_by_contact_id(
|
||||||
|
self._dc_context, contact_id)
|
||||||
|
return Chat(self._dc_context, chat_id)
|
||||||
|
|
||||||
|
def create_chat_by_message(self, message):
|
||||||
|
""" create or get an existing chat object for the
|
||||||
|
the specified message.
|
||||||
|
|
||||||
|
:param message: messsage id or message instance.
|
||||||
|
:returns: a :class:`deltachat.chatting.Chat` object.
|
||||||
|
"""
|
||||||
|
msg_id = getattr(message, "id", message)
|
||||||
|
assert isinstance(msg_id, int)
|
||||||
|
chat_id = lib.dc_create_chat_by_msg_id(self._dc_context, msg_id)
|
||||||
|
return Chat(self._dc_context, 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.
|
||||||
|
|
||||||
|
:param verified: if true only verified contacts can be added.
|
||||||
|
:returns: a :class:`deltachat.chatting.Chat` object.
|
||||||
|
"""
|
||||||
|
bytes_name = name.encode("utf8")
|
||||||
|
chat_id = lib.dc_create_group_chat(self._dc_context, verified, bytes_name)
|
||||||
|
return Chat(self._dc_context, chat_id)
|
||||||
|
|
||||||
|
def get_chats(self):
|
||||||
|
""" return list of chats.
|
||||||
|
|
||||||
|
:returns: a list of :class:`deltachat.chatting.Chat` objects.
|
||||||
|
"""
|
||||||
|
dc_chatlist = ffi.gc(
|
||||||
|
lib.dc_get_chatlist(self._dc_context, 0, ffi.NULL, 0),
|
||||||
|
lib.dc_chatlist_unref
|
||||||
|
)
|
||||||
|
|
||||||
|
assert dc_chatlist != ffi.NULL
|
||||||
|
chatlist = []
|
||||||
|
for i in range(0, lib.dc_chatlist_get_cnt(dc_chatlist)):
|
||||||
|
chat_id = lib.dc_chatlist_get_chat_id(dc_chatlist, i)
|
||||||
|
chatlist.append(Chat(self._dc_context, chat_id))
|
||||||
|
return chatlist
|
||||||
|
|
||||||
|
def get_deaddrop_chat(self):
|
||||||
|
return Chat(self._dc_context, const.DC_CHAT_ID_DEADDROP)
|
||||||
|
|
||||||
|
def get_message_by_id(self, msg_id):
|
||||||
|
""" return Message instance. """
|
||||||
|
return Message.from_db(self._dc_context, msg_id)
|
||||||
|
|
||||||
|
def mark_seen_messages(self, messages):
|
||||||
|
""" mark the given set of messages as seen.
|
||||||
|
|
||||||
|
:param messages: a list of message ids or Message instances.
|
||||||
|
"""
|
||||||
|
arr = array("i")
|
||||||
|
for msg in messages:
|
||||||
|
msg = getattr(msg, "id", msg)
|
||||||
|
arr.append(msg)
|
||||||
|
msg_ids = ffi.cast("uint32_t*", ffi.from_buffer(arr))
|
||||||
|
lib.dc_markseen_msgs(self._dc_context, msg_ids, len(messages))
|
||||||
|
|
||||||
|
def forward_messages(self, messages, chat):
|
||||||
|
""" Forward list of messages to a chat.
|
||||||
|
|
||||||
|
:param messages: list of :class:`deltachat.message.Message` object.
|
||||||
|
:param chat: :class:`deltachat.chatting.Chat` object.
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
msg_ids = [msg.id for msg in messages]
|
||||||
|
lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id)
|
||||||
|
|
||||||
|
def delete_messages(self, messages):
|
||||||
|
""" delete messages (local and remote).
|
||||||
|
|
||||||
|
:param messages: list of :class:`deltachat.message.Message` object.
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
msg_ids = [msg.id for msg in messages]
|
||||||
|
lib.dc_delete_msgs(self._dc_context, msg_ids, len(msg_ids))
|
||||||
|
|
||||||
|
def start_threads(self):
|
||||||
|
""" 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()
|
||||||
|
|
||||||
|
def stop_threads(self):
|
||||||
|
""" stop IMAP/SMTP threads. """
|
||||||
|
self._threads.stop(wait=True)
|
||||||
|
|
||||||
|
def _process_event(self, ctx, evt_name, data1, data2):
|
||||||
|
assert ctx == self._dc_context
|
||||||
|
self._evlogger(evt_name, data1, data2)
|
||||||
|
method = getattr(self._evhandler, evt_name.lower(), None)
|
||||||
|
if method is not None:
|
||||||
|
return method(data1, data2) or 0
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
class IOThreads:
|
||||||
|
def __init__(self, dc_context):
|
||||||
|
self._dc_context = dc_context
|
||||||
|
self._thread_quitflag = False
|
||||||
|
self._name2thread = {}
|
||||||
|
|
||||||
|
def start(self, imap=True, smtp=True):
|
||||||
|
assert not self._name2thread
|
||||||
|
if imap:
|
||||||
|
self._start_one_thread("imap", self.imap_thread_run)
|
||||||
|
if smtp:
|
||||||
|
self._start_one_thread("smtp", self.smtp_thread_run)
|
||||||
|
|
||||||
|
def _start_one_thread(self, name, func):
|
||||||
|
self._name2thread[name] = t = threading.Thread(target=func, name=name)
|
||||||
|
t.setDaemon(1)
|
||||||
|
t.start()
|
||||||
|
|
||||||
|
def stop(self, wait=False):
|
||||||
|
self._thread_quitflag = True
|
||||||
|
lib.dc_interrupt_imap_idle(self._dc_context)
|
||||||
|
lib.dc_interrupt_smtp_idle(self._dc_context)
|
||||||
|
if wait:
|
||||||
|
for name, thread in self._name2thread.items():
|
||||||
|
thread.join()
|
||||||
|
|
||||||
|
def imap_thread_run(self):
|
||||||
|
print ("starting imap thread")
|
||||||
|
while not self._thread_quitflag:
|
||||||
|
lib.dc_perform_imap_jobs(self._dc_context)
|
||||||
|
lib.dc_perform_imap_fetch(self._dc_context)
|
||||||
|
lib.dc_perform_imap_idle(self._dc_context)
|
||||||
|
|
||||||
|
def smtp_thread_run(self):
|
||||||
|
print ("starting smtp thread")
|
||||||
|
while not self._thread_quitflag:
|
||||||
|
lib.dc_perform_smtp_jobs(self._dc_context)
|
||||||
|
lib.dc_perform_smtp_idle(self._dc_context)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class EventHandler(object):
|
||||||
|
_dc_context = attr.ib(validator=v.instance_of(ffi.CData))
|
||||||
|
|
||||||
|
def read_url(self, url):
|
||||||
|
try:
|
||||||
|
r = requests.get(url)
|
||||||
|
except requests.ConnectionError:
|
||||||
|
return ''
|
||||||
|
else:
|
||||||
|
return r.content
|
||||||
|
|
||||||
|
def dc_event_http_get(self, data1, data2):
|
||||||
|
url = data1
|
||||||
|
content = self.read_url(url)
|
||||||
|
if not isinstance(content, bytes):
|
||||||
|
content = content.encode("utf8")
|
||||||
|
# we need to return a fresh pointer that the core owns
|
||||||
|
return lib.dupstring_helper(content)
|
||||||
|
|
||||||
|
def dc_event_is_offline(self, data1, data2):
|
||||||
|
return 0 # always online
|
||||||
|
|
||||||
|
|
||||||
|
class EventLogger:
|
||||||
|
_loglock = threading.RLock()
|
||||||
|
|
||||||
|
def __init__(self, dc_context, logid=None, debug=True):
|
||||||
|
self._dc_context = dc_context
|
||||||
|
self._event_queue = Queue()
|
||||||
|
self._debug = debug
|
||||||
|
if logid is None:
|
||||||
|
logid = str(self._dc_context).strip(">").split()[-1]
|
||||||
|
self.logid = logid
|
||||||
|
self._timeout = None
|
||||||
|
self.init_time = time.time()
|
||||||
|
|
||||||
|
def __call__(self, evt_name, data1, data2):
|
||||||
|
self._log_event(evt_name, data1, data2)
|
||||||
|
self._event_queue.put((evt_name, data1, data2))
|
||||||
|
|
||||||
|
def set_timeout(self, timeout):
|
||||||
|
self._timeout = timeout
|
||||||
|
|
||||||
|
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 get_matching(self, event_name_regex):
|
||||||
|
self._log("-- waiting for event with regex: {} --".format(event_name_regex))
|
||||||
|
rex = re.compile("(?:{}).*".format(event_name_regex))
|
||||||
|
while 1:
|
||||||
|
ev = self.get()
|
||||||
|
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))
|
||||||
|
|
||||||
|
|
||||||
|
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
|
||||||
258
python/src/deltachat/chatting.py
Normal file
258
python/src/deltachat/chatting.py
Normal file
@@ -0,0 +1,258 @@
|
|||||||
|
""" chatting related objects: Contact, Chat, Message. """
|
||||||
|
|
||||||
|
import os
|
||||||
|
|
||||||
|
from . import props
|
||||||
|
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
|
||||||
|
from .capi import lib, ffi
|
||||||
|
from . import const
|
||||||
|
import attr
|
||||||
|
from attr import validators as v
|
||||||
|
from .message import Message
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class Contact(object):
|
||||||
|
""" Delta-Chat Contact.
|
||||||
|
|
||||||
|
You obtain instances of it through :class:`deltachat.account.Account`.
|
||||||
|
"""
|
||||||
|
_dc_context = attr.ib(validator=v.instance_of(ffi.CData))
|
||||||
|
id = attr.ib(validator=v.instance_of(int))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _dc_contact(self):
|
||||||
|
return ffi.gc(
|
||||||
|
lib.dc_get_contact(self._dc_context, self.id),
|
||||||
|
lib.dc_contact_unref
|
||||||
|
)
|
||||||
|
|
||||||
|
@props.with_doc
|
||||||
|
def addr(self):
|
||||||
|
""" normalized e-mail address for this account. """
|
||||||
|
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
|
||||||
|
|
||||||
|
@props.with_doc
|
||||||
|
def display_name(self):
|
||||||
|
""" display name for this contact. """
|
||||||
|
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
|
||||||
|
|
||||||
|
def is_blocked(self):
|
||||||
|
""" Return True if the contact is blocked. """
|
||||||
|
return lib.dc_contact_is_blocked(self._dc_contact)
|
||||||
|
|
||||||
|
def is_verified(self):
|
||||||
|
""" Return True if the contact is verified. """
|
||||||
|
return lib.dc_contact_is_verified(self._dc_contact)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class Chat(object):
|
||||||
|
""" Chat object which manages members and through which you can send and retrieve messages.
|
||||||
|
|
||||||
|
You obtain instances of it through :class:`deltachat.account.Account`.
|
||||||
|
"""
|
||||||
|
_dc_context = attr.ib(validator=v.instance_of(ffi.CData))
|
||||||
|
id = attr.ib(validator=v.instance_of(int))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _dc_chat(self):
|
||||||
|
return ffi.gc(
|
||||||
|
lib.dc_get_chat(self._dc_context, self.id),
|
||||||
|
lib.dc_chat_unref
|
||||||
|
)
|
||||||
|
|
||||||
|
def delete(self):
|
||||||
|
"""Delete this chat and all its messages.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
|
||||||
|
- does not delete messages on server
|
||||||
|
- the chat or contact is not blocked, new message will arrive
|
||||||
|
"""
|
||||||
|
lib.dc_delete_chat(self._dc_context, self.id)
|
||||||
|
|
||||||
|
# ------ chat status/metadata API ------------------------------
|
||||||
|
|
||||||
|
def is_deaddrop(self):
|
||||||
|
""" return true if this chat is a deaddrop chat.
|
||||||
|
|
||||||
|
:returns: True if chat is the deaddrop chat, False otherwise.
|
||||||
|
"""
|
||||||
|
return self.id == const.DC_CHAT_ID_DEADDROP
|
||||||
|
|
||||||
|
def is_promoted(self):
|
||||||
|
""" return True if this chat is promoted, i.e.
|
||||||
|
the member contacts are aware of their membership,
|
||||||
|
have been sent messages.
|
||||||
|
|
||||||
|
:returns: True if chat is promoted, False otherwise.
|
||||||
|
"""
|
||||||
|
return not lib.dc_chat_is_unpromoted(self._dc_chat)
|
||||||
|
|
||||||
|
def get_name(self):
|
||||||
|
""" return name of this chat.
|
||||||
|
|
||||||
|
:returns: unicode name
|
||||||
|
"""
|
||||||
|
return from_dc_charpointer(lib.dc_chat_get_name(self._dc_chat))
|
||||||
|
|
||||||
|
def set_name(self, name):
|
||||||
|
""" set name of this chat.
|
||||||
|
|
||||||
|
:param: name as a unicode string.
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
name = as_dc_charpointer(name)
|
||||||
|
return lib.dc_set_chat_name(self._dc_context, self.id, name)
|
||||||
|
|
||||||
|
def get_type(self):
|
||||||
|
""" return type of this chat.
|
||||||
|
|
||||||
|
:returns: one of const.DC_CHAT_TYPE_*
|
||||||
|
"""
|
||||||
|
return lib.dc_chat_get_type(self._dc_chat)
|
||||||
|
|
||||||
|
# ------ chat messaging API ------------------------------
|
||||||
|
|
||||||
|
def send_text(self, text):
|
||||||
|
""" send a text message and return the resulting Message instance.
|
||||||
|
|
||||||
|
:param msg: unicode text
|
||||||
|
:raises ValueError: if message can not be send/chat does not exist.
|
||||||
|
:returns: the resulting :class:`deltachat.message.Message` instance
|
||||||
|
"""
|
||||||
|
msg = as_dc_charpointer(text)
|
||||||
|
msg_id = lib.dc_send_text_msg(self._dc_context, self.id, msg)
|
||||||
|
if msg_id == 0:
|
||||||
|
raise ValueError("message could not be send, does chat exist?")
|
||||||
|
return Message.from_db(self._dc_context, msg_id)
|
||||||
|
|
||||||
|
def send_file(self, path, mime_type="application/octet-stream"):
|
||||||
|
""" send a file and return the resulting Message instance.
|
||||||
|
|
||||||
|
:param path: path to the file.
|
||||||
|
:param mime_type: the mime-type of this file, defaults to application/octet-stream.
|
||||||
|
:raises ValueError: if message can not be send/chat does not exist.
|
||||||
|
:returns: the resulting :class:`deltachat.message.Message` instance
|
||||||
|
"""
|
||||||
|
path = as_dc_charpointer(path)
|
||||||
|
mtype = as_dc_charpointer(mime_type)
|
||||||
|
msg = Message.new(self._dc_context, "file")
|
||||||
|
msg.set_file(path, mtype)
|
||||||
|
msg_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
|
||||||
|
if msg_id == 0:
|
||||||
|
raise ValueError("message could not be send, does chat exist?")
|
||||||
|
return Message.from_db(self._dc_context, msg_id)
|
||||||
|
|
||||||
|
def send_image(self, path):
|
||||||
|
""" send an image message and return the resulting Message instance.
|
||||||
|
|
||||||
|
:param path: path to an image file.
|
||||||
|
:raises ValueError: if message can not be send/chat does not exist.
|
||||||
|
:returns: the resulting :class:`deltachat.message.Message` instance
|
||||||
|
"""
|
||||||
|
if not os.path.exists(path):
|
||||||
|
raise ValueError("path does not exist: {!r}".format(path))
|
||||||
|
msg = Message.new(self._dc_context, "image")
|
||||||
|
msg.set_file(path)
|
||||||
|
msg_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
|
||||||
|
return Message.from_db(self._dc_context, msg_id)
|
||||||
|
|
||||||
|
def prepare_file(self, path, mime_type=None, view_type="file"):
|
||||||
|
""" prepare a message for sending and return the resulting Message instance.
|
||||||
|
|
||||||
|
To actually send the message, call :meth:`send_prepared`.
|
||||||
|
The file must be inside the blob directory.
|
||||||
|
|
||||||
|
:param path: path to the file.
|
||||||
|
:param mime_type: the mime-type of this file, defaults to auto-detection.
|
||||||
|
:param view_type: passed to :meth:`MessageType.new`.
|
||||||
|
:raises ValueError: if message can not be prepared/chat does not exist.
|
||||||
|
:returns: the resulting :class:`Message` instance
|
||||||
|
"""
|
||||||
|
path = as_dc_charpointer(path)
|
||||||
|
mtype = as_dc_charpointer(mime_type)
|
||||||
|
msg = Message.new(self._dc_context, view_type)
|
||||||
|
msg.set_file(path, mtype)
|
||||||
|
msg_id = lib.dc_prepare_msg(self._dc_context, self.id, msg._dc_msg)
|
||||||
|
if msg_id == 0:
|
||||||
|
raise ValueError("message could not be prepared, does chat exist?")
|
||||||
|
return Message.from_db(self._dc_context, msg_id)
|
||||||
|
|
||||||
|
def send_prepared(self, message):
|
||||||
|
""" send a previously prepared message.
|
||||||
|
|
||||||
|
:param message: a :class:`Message` instance previously returned by
|
||||||
|
:meth:`prepare_file`.
|
||||||
|
:raises ValueError: if message can not be sent.
|
||||||
|
:returns: a :class:`deltachat.message.Message` instance with updated state
|
||||||
|
"""
|
||||||
|
msg_id = lib.dc_send_msg(self._dc_context, 0, message._dc_msg)
|
||||||
|
if msg_id == 0:
|
||||||
|
raise ValueError("message could not be sent")
|
||||||
|
return Message.from_db(self._dc_context, msg_id)
|
||||||
|
|
||||||
|
def get_messages(self):
|
||||||
|
""" return list of messages in this chat.
|
||||||
|
|
||||||
|
:returns: list of :class:`deltachat.message.Message` objects for this chat.
|
||||||
|
"""
|
||||||
|
dc_array = ffi.gc(
|
||||||
|
lib.dc_get_chat_msgs(self._dc_context, self.id, 0, 0),
|
||||||
|
lib.dc_array_unref
|
||||||
|
)
|
||||||
|
return list(iter_array(dc_array, lambda x: Message.from_db(self._dc_context, x)))
|
||||||
|
|
||||||
|
def count_fresh_messages(self):
|
||||||
|
""" return number of fresh messages in this chat.
|
||||||
|
|
||||||
|
:returns: number of fresh messages
|
||||||
|
"""
|
||||||
|
return lib.dc_get_fresh_msg_cnt(self._dc_context, self.id)
|
||||||
|
|
||||||
|
def mark_noticed(self):
|
||||||
|
""" mark all messages in this chat as noticed.
|
||||||
|
|
||||||
|
Noticed messages are no longer fresh.
|
||||||
|
"""
|
||||||
|
return lib.dc_marknoticed_chat(self._dc_context, self.id)
|
||||||
|
|
||||||
|
# ------ group management API ------------------------------
|
||||||
|
|
||||||
|
def add_contact(self, contact):
|
||||||
|
""" add a contact to this chat.
|
||||||
|
|
||||||
|
:params: contact object.
|
||||||
|
:raises ValueError: if contact could not be added
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
ret = lib.dc_add_contact_to_chat(self._dc_context, self.id, contact.id)
|
||||||
|
if ret != 1:
|
||||||
|
raise ValueError("could not add contact {!r} to chat".format(contact))
|
||||||
|
|
||||||
|
def remove_contact(self, contact):
|
||||||
|
""" remove a contact from this chat.
|
||||||
|
|
||||||
|
:params: contact object.
|
||||||
|
:raises ValueError: if contact could not be removed
|
||||||
|
:returns: None
|
||||||
|
"""
|
||||||
|
ret = lib.dc_remove_contact_from_chat(self._dc_context, self.id, contact.id)
|
||||||
|
if ret != 1:
|
||||||
|
raise ValueError("could not remove contact {!r} from chat".format(contact))
|
||||||
|
|
||||||
|
def get_contacts(self):
|
||||||
|
""" get all contacts for this chat.
|
||||||
|
:params: contact object.
|
||||||
|
:raises ValueError: if contact could not be added
|
||||||
|
:returns: list of :class:`deltachat.chatting.Contact` objects for this chat
|
||||||
|
|
||||||
|
"""
|
||||||
|
dc_array = ffi.gc(
|
||||||
|
lib.dc_get_chat_contacts(self._dc_context, self.id),
|
||||||
|
lib.dc_array_unref
|
||||||
|
)
|
||||||
|
return list(iter_array(
|
||||||
|
dc_array, lambda id: Contact(self._dc_context, id))
|
||||||
|
)
|
||||||
113
python/src/deltachat/const.py
Normal file
113
python/src/deltachat/const.py
Normal file
@@ -0,0 +1,113 @@
|
|||||||
|
import sys
|
||||||
|
import re
|
||||||
|
import os
|
||||||
|
from os.path import dirname, abspath
|
||||||
|
from os.path import join as joinpath
|
||||||
|
|
||||||
|
# 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_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_DEVICE = 2
|
||||||
|
DC_CONTACT_ID_LAST_SPECIAL = 9
|
||||||
|
DC_MSG_TEXT = 10
|
||||||
|
DC_MSG_IMAGE = 20
|
||||||
|
DC_MSG_GIF = 21
|
||||||
|
DC_MSG_AUDIO = 40
|
||||||
|
DC_MSG_VOICE = 41
|
||||||
|
DC_MSG_VIDEO = 50
|
||||||
|
DC_MSG_FILE = 60
|
||||||
|
DC_EVENT_INFO = 100
|
||||||
|
DC_EVENT_SMTP_CONNECTED = 101
|
||||||
|
DC_EVENT_IMAP_CONNECTED = 102
|
||||||
|
DC_EVENT_SMTP_MESSAGE_SENT = 103
|
||||||
|
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_GET_STRING = 2091
|
||||||
|
DC_EVENT_HTTP_GET = 2100
|
||||||
|
DC_EVENT_HTTP_POST = 2110
|
||||||
|
DC_EVENT_FILE_COPIED = 2055
|
||||||
|
DC_EVENT_IS_OFFLINE = 2081
|
||||||
|
# end const generated
|
||||||
|
|
||||||
|
|
||||||
|
def read_event_defines(f):
|
||||||
|
rex = re.compile(r'#define\s+((?:DC_EVENT_|DC_MSG|DC_STATE_|DC_CONTACT_ID_|DC_GCL|DC_CHAT)\S+)\s+([x\d]+).*')
|
||||||
|
for line in f:
|
||||||
|
m = rex.match(line)
|
||||||
|
if m:
|
||||||
|
yield m.groups()
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
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))), "src", "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)
|
||||||
19
python/src/deltachat/cutil.py
Normal file
19
python/src/deltachat/cutil.py
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
from .capi import lib
|
||||||
|
from .capi import ffi
|
||||||
|
|
||||||
|
|
||||||
|
def as_dc_charpointer(obj):
|
||||||
|
if obj == ffi.NULL or obj is None:
|
||||||
|
return ffi.NULL
|
||||||
|
if not isinstance(obj, bytes):
|
||||||
|
return obj.encode("utf8")
|
||||||
|
return obj
|
||||||
|
|
||||||
|
|
||||||
|
def iter_array(dc_array_t, constructor):
|
||||||
|
for i in range(0, lib.dc_array_get_cnt(dc_array_t)):
|
||||||
|
yield constructor(lib.dc_array_get_id(dc_array_t, i))
|
||||||
|
|
||||||
|
|
||||||
|
def from_dc_charpointer(obj):
|
||||||
|
return ffi.string(obj).decode("utf8")
|
||||||
262
python/src/deltachat/message.py
Normal file
262
python/src/deltachat/message.py
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
""" chatting related objects: Contact, Chat, Message. """
|
||||||
|
|
||||||
|
import os
|
||||||
|
from . import props
|
||||||
|
from .cutil import from_dc_charpointer, as_dc_charpointer
|
||||||
|
from .capi import lib, ffi
|
||||||
|
from . import const
|
||||||
|
from datetime import datetime
|
||||||
|
import attr
|
||||||
|
from attr import validators as v
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class Message(object):
|
||||||
|
""" Message object.
|
||||||
|
|
||||||
|
You obtain instances of it through :class:`deltachat.account.Account` or
|
||||||
|
:class:`deltachat.chatting.Chat`.
|
||||||
|
"""
|
||||||
|
_dc_context = attr.ib(validator=v.instance_of(ffi.CData))
|
||||||
|
try:
|
||||||
|
id = attr.ib(validator=v.instance_of((int, long)))
|
||||||
|
except NameError: # py35
|
||||||
|
id = attr.ib(validator=v.instance_of(int))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _dc_msg(self):
|
||||||
|
if self.id > 0:
|
||||||
|
return ffi.gc(
|
||||||
|
lib.dc_get_msg(self._dc_context, self.id),
|
||||||
|
lib.dc_msg_unref
|
||||||
|
)
|
||||||
|
return self._dc_msg_volatile
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_db(cls, _dc_context, id):
|
||||||
|
assert id > 0
|
||||||
|
return cls(_dc_context, id)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def new(cls, dc_context, view_type):
|
||||||
|
""" create a non-persistent method. """
|
||||||
|
msg = cls(dc_context, 0)
|
||||||
|
view_type_code = MessageType.get_typecode(view_type)
|
||||||
|
msg._dc_msg_volatile = ffi.gc(
|
||||||
|
lib.dc_msg_new(dc_context, view_type_code),
|
||||||
|
lib.dc_msg_unref
|
||||||
|
)
|
||||||
|
return msg
|
||||||
|
|
||||||
|
def get_state(self):
|
||||||
|
""" get the message in/out state.
|
||||||
|
|
||||||
|
:returns: :class:`deltachat.message.MessageState`
|
||||||
|
"""
|
||||||
|
return MessageState(self)
|
||||||
|
|
||||||
|
@props.with_doc
|
||||||
|
def text(self):
|
||||||
|
"""unicode text of this messages (might be empty if not a text message). """
|
||||||
|
return from_dc_charpointer(lib.dc_msg_get_text(self._dc_msg))
|
||||||
|
|
||||||
|
def set_text(self, text):
|
||||||
|
"""set text of this message. """
|
||||||
|
return lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text))
|
||||||
|
|
||||||
|
@props.with_doc
|
||||||
|
def filename(self):
|
||||||
|
"""filename if there was an attachment, otherwise empty string. """
|
||||||
|
return from_dc_charpointer(lib.dc_msg_get_file(self._dc_msg))
|
||||||
|
|
||||||
|
def set_file(self, path, mime_type=None):
|
||||||
|
"""set file for this message. """
|
||||||
|
mtype = ffi.NULL if mime_type is None else mime_type
|
||||||
|
assert os.path.exists(path)
|
||||||
|
lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype)
|
||||||
|
|
||||||
|
@props.with_doc
|
||||||
|
def basename(self):
|
||||||
|
"""basename of the attachment if it exists, otherwise empty string. """
|
||||||
|
return from_dc_charpointer(lib.dc_msg_get_filename(self._dc_msg))
|
||||||
|
|
||||||
|
@props.with_doc
|
||||||
|
def filemime(self):
|
||||||
|
"""mime type of the file (if it exists)"""
|
||||||
|
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
|
||||||
|
|
||||||
|
@props.with_doc
|
||||||
|
def view_type(self):
|
||||||
|
"""the view type of this message.
|
||||||
|
|
||||||
|
:returns: a :class:`deltachat.message.MessageType` instance.
|
||||||
|
"""
|
||||||
|
return MessageType(lib.dc_msg_get_viewtype(self._dc_msg))
|
||||||
|
|
||||||
|
@props.with_doc
|
||||||
|
def time_sent(self):
|
||||||
|
"""UTC time when the message was sent.
|
||||||
|
|
||||||
|
:returns: naive datetime.datetime() object.
|
||||||
|
"""
|
||||||
|
ts = lib.dc_msg_get_timestamp(self._dc_msg)
|
||||||
|
return datetime.utcfromtimestamp(ts)
|
||||||
|
|
||||||
|
@props.with_doc
|
||||||
|
def time_received(self):
|
||||||
|
"""UTC time when the message was received.
|
||||||
|
|
||||||
|
:returns: naive datetime.datetime() object or None if message is an outgoing one.
|
||||||
|
"""
|
||||||
|
ts = lib.dc_msg_get_received_timestamp(self._dc_msg)
|
||||||
|
if ts:
|
||||||
|
return datetime.utcfromtimestamp(ts)
|
||||||
|
|
||||||
|
def get_mime_headers(self):
|
||||||
|
""" return mime-header object for an incoming message.
|
||||||
|
|
||||||
|
This only returns a non-None object if ``save_mime_headers``
|
||||||
|
config option was set and the message is incoming.
|
||||||
|
|
||||||
|
:returns: email-mime message object (with headers only, no body).
|
||||||
|
"""
|
||||||
|
import email.parser
|
||||||
|
mime_headers = lib.dc_get_mime_headers(self._dc_context, self.id)
|
||||||
|
if mime_headers:
|
||||||
|
s = ffi.string(mime_headers)
|
||||||
|
if isinstance(s, bytes):
|
||||||
|
s = s.decode("ascii")
|
||||||
|
return email.message_from_string(s)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def chat(self):
|
||||||
|
"""chat this message was posted in.
|
||||||
|
|
||||||
|
:returns: :class:`deltachat.chatting.Chat` object
|
||||||
|
"""
|
||||||
|
from .chatting import Chat
|
||||||
|
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
|
||||||
|
return Chat(self._dc_context, chat_id)
|
||||||
|
|
||||||
|
def get_sender_contact(self):
|
||||||
|
"""return the contact of who wrote the message.
|
||||||
|
|
||||||
|
:returns: :class:`deltachat.chatting.Contact` instance
|
||||||
|
"""
|
||||||
|
from .chatting import Contact
|
||||||
|
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
||||||
|
return Contact(self._dc_context, contact_id)
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class MessageType(object):
|
||||||
|
""" DeltaChat message type, with is_* methods. """
|
||||||
|
_type = attr.ib(validator=v.instance_of(int))
|
||||||
|
_mapping = {
|
||||||
|
const.DC_MSG_TEXT: 'text',
|
||||||
|
const.DC_MSG_IMAGE: 'image',
|
||||||
|
const.DC_MSG_GIF: 'gif',
|
||||||
|
const.DC_MSG_AUDIO: 'audio',
|
||||||
|
const.DC_MSG_VIDEO: 'video',
|
||||||
|
const.DC_MSG_FILE: 'file'
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_typecode(cls, view_type):
|
||||||
|
for code, value in cls._mapping.items():
|
||||||
|
if value == view_type:
|
||||||
|
return code
|
||||||
|
raise ValueError("message typecode not found for {!r}".format(view_type))
|
||||||
|
|
||||||
|
@props.with_doc
|
||||||
|
def name(self):
|
||||||
|
""" human readable type name. """
|
||||||
|
return self._mapping.get(self._type, "")
|
||||||
|
|
||||||
|
def is_text(self):
|
||||||
|
""" return True if it's a text message. """
|
||||||
|
return self._type == const.DC_MSG_TEXT
|
||||||
|
|
||||||
|
def is_image(self):
|
||||||
|
""" return True if it's an image message. """
|
||||||
|
return self._type == const.DC_MSG_IMAGE
|
||||||
|
|
||||||
|
def is_gif(self):
|
||||||
|
""" return True if it's a gif message. """
|
||||||
|
return self._type == const.DC_MSG_GIF
|
||||||
|
|
||||||
|
def is_audio(self):
|
||||||
|
""" return True if it's an audio message. """
|
||||||
|
return self._type == const.DC_MSG_AUDIO
|
||||||
|
|
||||||
|
def is_video(self):
|
||||||
|
""" return True if it's a video message. """
|
||||||
|
return self._type == const.DC_MSG_VIDEO
|
||||||
|
|
||||||
|
def is_file(self):
|
||||||
|
""" return True if it's a file message. """
|
||||||
|
return self._type == const.DC_MSG_FILE
|
||||||
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class MessageState(object):
|
||||||
|
""" Current Message In/Out state, updated on each call of is_* methods.
|
||||||
|
"""
|
||||||
|
message = attr.ib(validator=v.instance_of(Message))
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _msgstate(self):
|
||||||
|
return lib.dc_msg_get_state(self.message._dc_msg)
|
||||||
|
|
||||||
|
def is_in_fresh(self):
|
||||||
|
""" return True if Message is incoming fresh message (un-noticed).
|
||||||
|
|
||||||
|
Fresh messages are not noticed nor seen and are typically
|
||||||
|
shown in notifications.
|
||||||
|
"""
|
||||||
|
return self._msgstate == const.DC_STATE_IN_FRESH
|
||||||
|
|
||||||
|
def is_in_noticed(self):
|
||||||
|
"""Return True if Message is incoming and noticed.
|
||||||
|
|
||||||
|
Eg. chat opened but message not yet read - noticed messages
|
||||||
|
are not counted as unread but were not marked as read nor resulted in MDNs.
|
||||||
|
"""
|
||||||
|
return self._msgstate == const.DC_STATE_IN_NOTICED
|
||||||
|
|
||||||
|
def is_in_seen(self):
|
||||||
|
"""Return True if Message is incoming, noticed and has been seen.
|
||||||
|
|
||||||
|
Eg. chat opened but message not yet read - noticed messages
|
||||||
|
are not counted as unread but were not marked as read nor resulted in MDNs.
|
||||||
|
"""
|
||||||
|
return self._msgstate == const.DC_STATE_IN_SEEN
|
||||||
|
|
||||||
|
def is_out_preparing(self):
|
||||||
|
"""Return True if Message is outgoing, but its file is being prepared.
|
||||||
|
"""
|
||||||
|
return self._msgstate == const.DC_STATE_OUT_PREPARING
|
||||||
|
|
||||||
|
def is_out_pending(self):
|
||||||
|
"""Return True if Message is outgoing, but is pending (no single checkmark).
|
||||||
|
"""
|
||||||
|
return self._msgstate == const.DC_STATE_OUT_PENDING
|
||||||
|
|
||||||
|
def is_out_failed(self):
|
||||||
|
"""Return True if Message is unrecoverably failed.
|
||||||
|
"""
|
||||||
|
return self._msgstate == const.DC_STATE_OUT_FAILED
|
||||||
|
|
||||||
|
def is_out_delivered(self):
|
||||||
|
"""Return True if Message was successfully delivered to the server (one checkmark).
|
||||||
|
|
||||||
|
Note, that already delivered messages may get into the state is_out_failed().
|
||||||
|
"""
|
||||||
|
return self._msgstate == const.DC_STATE_OUT_DELIVERED
|
||||||
|
|
||||||
|
def is_out_mdn_received(self):
|
||||||
|
"""Return True if message was marked as read by the recipient(s) (two checkmarks;
|
||||||
|
this requires goodwill on the receiver's side). If a sent message changes to this
|
||||||
|
state, you'll receive the event DC_EVENT_MSG_READ.
|
||||||
|
"""
|
||||||
|
return self._msgstate == const.DC_STATE_OUT_MDN_RCVD
|
||||||
30
python/src/deltachat/props.py
Normal file
30
python/src/deltachat/props.py
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
"""Helpers for properties."""
|
||||||
|
|
||||||
|
|
||||||
|
def with_doc(f):
|
||||||
|
return property(f, None, None, f.__doc__)
|
||||||
|
|
||||||
|
|
||||||
|
# copied over unmodified from
|
||||||
|
# https://github.com/devpi/devpi/blob/master/common/devpi_common/types.py
|
||||||
|
def cached(f):
|
||||||
|
"""returns a cached property that is calculated by function f"""
|
||||||
|
def get(self):
|
||||||
|
try:
|
||||||
|
return self._property_cache[f]
|
||||||
|
except AttributeError:
|
||||||
|
self._property_cache = {}
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
x = self._property_cache[f] = f(self)
|
||||||
|
return x
|
||||||
|
|
||||||
|
def set(self, val):
|
||||||
|
propcache = self.__dict__.setdefault("_property_cache", {})
|
||||||
|
propcache[f] = val
|
||||||
|
|
||||||
|
def fdel(self):
|
||||||
|
propcache = self.__dict__.setdefault("_property_cache", {})
|
||||||
|
del propcache[f]
|
||||||
|
|
||||||
|
return property(get, set, fdel)
|
||||||
13
python/tests/auditwheels.py
Normal file
13
python/tests/auditwheels.py
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
assert len(sys.argv) == 2
|
||||||
|
workspacedir = sys.argv[1]
|
||||||
|
for relpath in os.listdir(workspacedir):
|
||||||
|
if relpath.startswith("deltachat"):
|
||||||
|
p = os.path.join(workspacedir, relpath)
|
||||||
|
subprocess.check_call(["auditwheel", "repair", p, "-w", workspacedir])
|
||||||
155
python/tests/conftest.py
Normal file
155
python/tests/conftest.py
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
import os
|
||||||
|
import pytest
|
||||||
|
import time
|
||||||
|
from deltachat import Account
|
||||||
|
from deltachat import props
|
||||||
|
from deltachat.capi import lib
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_addoption(parser):
|
||||||
|
parser.addoption(
|
||||||
|
"--liveconfig", action="store", default=None,
|
||||||
|
help="a file with >=2 lines where each line "
|
||||||
|
"contains NAME=VALUE config settings for one account"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def pytest_report_header(config, startdir):
|
||||||
|
t = tempfile.mktemp()
|
||||||
|
try:
|
||||||
|
ac = Account(t)
|
||||||
|
info = ac.get_info()
|
||||||
|
del ac
|
||||||
|
finally:
|
||||||
|
os.remove(t)
|
||||||
|
return "Deltachat core={} sqlite={}".format(
|
||||||
|
info['deltachat_core_version'],
|
||||||
|
info['sqlite_version'],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def data():
|
||||||
|
class Data:
|
||||||
|
def __init__(self):
|
||||||
|
self.path = os.path.join(os.path.dirname(__file__), "data")
|
||||||
|
|
||||||
|
def get_path(self, bn):
|
||||||
|
fn = os.path.join(self.path, bn)
|
||||||
|
assert os.path.exists(fn)
|
||||||
|
return fn
|
||||||
|
return Data()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def acfactory(pytestconfig, tmpdir, request):
|
||||||
|
fn = pytestconfig.getoption("--liveconfig")
|
||||||
|
|
||||||
|
class AccountMaker:
|
||||||
|
def __init__(self):
|
||||||
|
self.live_count = 0
|
||||||
|
self.offline_count = 0
|
||||||
|
self._finalizers = []
|
||||||
|
request.addfinalizer(self.finalize)
|
||||||
|
self.init_time = time.time()
|
||||||
|
|
||||||
|
def finalize(self):
|
||||||
|
while self._finalizers:
|
||||||
|
fin = self._finalizers.pop()
|
||||||
|
fin()
|
||||||
|
|
||||||
|
@props.cached
|
||||||
|
def configlist(self):
|
||||||
|
configlist = []
|
||||||
|
for line in open(fn):
|
||||||
|
if line.strip():
|
||||||
|
d = {}
|
||||||
|
for part in line.split():
|
||||||
|
name, value = part.split("=")
|
||||||
|
d[name] = value
|
||||||
|
configlist.append(d)
|
||||||
|
return configlist
|
||||||
|
|
||||||
|
def get_unconfigured_account(self):
|
||||||
|
self.offline_count += 1
|
||||||
|
tmpdb = tmpdir.join("offlinedb%d" % self.offline_count)
|
||||||
|
ac = Account(tmpdb.strpath, logid="ac{}".format(self.offline_count))
|
||||||
|
ac._evlogger.init_time = self.init_time
|
||||||
|
ac._evlogger.set_timeout(2)
|
||||||
|
return ac
|
||||||
|
|
||||||
|
def get_configured_offline_account(self):
|
||||||
|
ac = self.get_unconfigured_account()
|
||||||
|
|
||||||
|
# do a pseudo-configured account
|
||||||
|
addr = "addr{}@offline.org".format(self.offline_count)
|
||||||
|
ac.set_config("addr", addr)
|
||||||
|
lib.dc_set_config(ac._dc_context, b"configured_addr", addr.encode("ascii"))
|
||||||
|
ac.set_config("mail_pw", "123")
|
||||||
|
lib.dc_set_config(ac._dc_context, b"configured_mail_pw", b"123")
|
||||||
|
lib.dc_set_config(ac._dc_context, b"configured", b"1")
|
||||||
|
return ac
|
||||||
|
|
||||||
|
def get_online_configuring_account(self):
|
||||||
|
if not fn:
|
||||||
|
pytest.skip("specify a --liveconfig file to run tests with real accounts")
|
||||||
|
self.live_count += 1
|
||||||
|
configdict = self.configlist.pop(0)
|
||||||
|
tmpdb = tmpdir.join("livedb%d" % self.live_count)
|
||||||
|
ac = Account(tmpdb.strpath, logid="ac{}".format(self.live_count))
|
||||||
|
ac._evlogger.init_time = self.init_time
|
||||||
|
ac._evlogger.set_timeout(30)
|
||||||
|
ac.configure(**configdict)
|
||||||
|
ac.start_threads()
|
||||||
|
self._finalizers.append(ac.stop_threads)
|
||||||
|
return ac
|
||||||
|
|
||||||
|
return AccountMaker()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def tmp_db_path(tmpdir):
|
||||||
|
return tmpdir.join("test.db").strpath
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def lp():
|
||||||
|
class Printer:
|
||||||
|
def sec(self, msg):
|
||||||
|
print()
|
||||||
|
print("=" * 10, msg, "=" * 10)
|
||||||
|
|
||||||
|
def step(self, msg):
|
||||||
|
print("-" * 5, "step " + msg, "-" * 5)
|
||||||
|
return Printer()
|
||||||
|
|
||||||
|
|
||||||
|
def wait_configuration_progress(account, target):
|
||||||
|
while 1:
|
||||||
|
evt_name, data1, data2 = \
|
||||||
|
account._evlogger.get_matching("DC_EVENT_CONFIGURE_PROGRESS")
|
||||||
|
if data1 >= target:
|
||||||
|
print("** CONFIG PROGRESS {}".format(target), account)
|
||||||
|
break
|
||||||
|
|
||||||
|
|
||||||
|
def wait_successful_IMAP_SMTP_connection(account):
|
||||||
|
imap_ok = smtp_ok = False
|
||||||
|
while not imap_ok or not smtp_ok:
|
||||||
|
evt_name, data1, data2 = \
|
||||||
|
account._evlogger.get_matching("DC_EVENT_(IMAP|SMTP)_CONNECTED")
|
||||||
|
if evt_name == "DC_EVENT_IMAP_CONNECTED":
|
||||||
|
imap_ok = True
|
||||||
|
if evt_name == "DC_EVENT_SMTP_CONNECTED":
|
||||||
|
smtp_ok = True
|
||||||
|
print("** IMAP and SMTP logins successful", account)
|
||||||
|
|
||||||
|
|
||||||
|
def wait_msgs_changed(account, chat_id, msg_id=None):
|
||||||
|
ev = account._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
|
assert ev[1] == chat_id
|
||||||
|
if msg_id is not None:
|
||||||
|
assert ev[2] == msg_id
|
||||||
|
return ev[2]
|
||||||
BIN
python/tests/data/d.png
Normal file
BIN
python/tests/data/d.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
2
python/tests/data/r.txt
Normal file
2
python/tests/data/r.txt
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
|
||||||
|
hello
|
||||||
335
python/tests/test_account.py
Normal file
335
python/tests/test_account.py
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
import pytest
|
||||||
|
import os
|
||||||
|
from deltachat import const
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from conftest import wait_configuration_progress, wait_successful_IMAP_SMTP_connection
|
||||||
|
|
||||||
|
|
||||||
|
class TestOfflineAccount:
|
||||||
|
def test_getinfo(self, acfactory):
|
||||||
|
ac1 = acfactory.get_unconfigured_account()
|
||||||
|
d = ac1.get_info()
|
||||||
|
assert d["compile_date"]
|
||||||
|
assert d["arch"]
|
||||||
|
assert d["number_of_chats"] == "0"
|
||||||
|
|
||||||
|
def test_is_not_configured(self, acfactory):
|
||||||
|
ac1 = acfactory.get_unconfigured_account()
|
||||||
|
assert not ac1.is_configured()
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ac1.check_is_configured()
|
||||||
|
|
||||||
|
def test_wrong_config_keys(self, acfactory):
|
||||||
|
ac1 = acfactory.get_unconfigured_account()
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
ac1.set_config("lqkwje", "value")
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
ac1.get_config("lqkwje")
|
||||||
|
|
||||||
|
def test_has_savemime(self, acfactory):
|
||||||
|
ac1 = acfactory.get_unconfigured_account()
|
||||||
|
assert "save_mime_headers" in ac1.get_config("sys.config_keys").split()
|
||||||
|
|
||||||
|
def test_selfcontact_if_unconfigured(self, acfactory):
|
||||||
|
ac1 = acfactory.get_unconfigured_account()
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ac1.get_self_contact()
|
||||||
|
|
||||||
|
def test_get_info(self, acfactory):
|
||||||
|
ac1 = acfactory.get_configured_offline_account()
|
||||||
|
out = ac1.get_infostring()
|
||||||
|
assert "number_of_chats=0" in out
|
||||||
|
|
||||||
|
def test_selfcontact_configured(self, acfactory):
|
||||||
|
ac1 = acfactory.get_configured_offline_account()
|
||||||
|
me = ac1.get_self_contact()
|
||||||
|
assert me.display_name
|
||||||
|
assert me.addr
|
||||||
|
|
||||||
|
def test_get_config_fails(self, acfactory):
|
||||||
|
ac1 = acfactory.get_unconfigured_account()
|
||||||
|
with pytest.raises(KeyError):
|
||||||
|
ac1.get_config("123123")
|
||||||
|
|
||||||
|
def test_contact_attr(self, acfactory):
|
||||||
|
ac1 = acfactory.get_configured_offline_account()
|
||||||
|
contact1 = ac1.create_contact(email="some1@hello.com", name="some1")
|
||||||
|
assert contact1.id
|
||||||
|
assert contact1.addr == "some1@hello.com"
|
||||||
|
assert contact1.display_name == "some1"
|
||||||
|
assert not contact1.is_blocked()
|
||||||
|
assert not contact1.is_verified()
|
||||||
|
|
||||||
|
def test_get_contacts(self, acfactory):
|
||||||
|
ac1 = acfactory.get_configured_offline_account()
|
||||||
|
contact1 = ac1.create_contact(email="some1@hello.com", name="some1")
|
||||||
|
contacts = ac1.get_contacts()
|
||||||
|
assert len(contacts) == 1
|
||||||
|
assert contact1 in contacts
|
||||||
|
|
||||||
|
assert not ac1.get_contacts(query="some2")
|
||||||
|
assert ac1.get_contacts(query="some1")
|
||||||
|
assert not ac1.get_contacts(only_verified=True)
|
||||||
|
contacts = ac1.get_contacts(with_self=True)
|
||||||
|
assert len(contacts) == 2
|
||||||
|
|
||||||
|
def test_chat(self, acfactory):
|
||||||
|
ac1 = acfactory.get_configured_offline_account()
|
||||||
|
contact1 = ac1.create_contact("some1@hello.com", name="some1")
|
||||||
|
chat = ac1.create_chat_by_contact(contact1)
|
||||||
|
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL, chat.id
|
||||||
|
|
||||||
|
chat2 = ac1.create_chat_by_contact(contact1.id)
|
||||||
|
assert chat2.id == chat.id
|
||||||
|
assert chat2.get_name() == chat.get_name()
|
||||||
|
assert chat == chat2
|
||||||
|
assert not (chat != chat2)
|
||||||
|
|
||||||
|
for ichat in ac1.get_chats():
|
||||||
|
if ichat.id == chat.id:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
pytest.fail("could not find chat")
|
||||||
|
|
||||||
|
def test_group_chat_creation(self, acfactory):
|
||||||
|
ac1 = acfactory.get_configured_offline_account()
|
||||||
|
contact1 = ac1.create_contact("some1@hello.com", name="some1")
|
||||||
|
contact2 = ac1.create_contact("some2@hello.com", name="some2")
|
||||||
|
chat = ac1.create_group_chat(name="title1")
|
||||||
|
chat.add_contact(contact1)
|
||||||
|
chat.add_contact(contact2)
|
||||||
|
assert chat.get_name() == "title1"
|
||||||
|
assert contact1 in chat.get_contacts()
|
||||||
|
assert contact2 in chat.get_contacts()
|
||||||
|
assert not chat.is_promoted()
|
||||||
|
chat.set_name("title2")
|
||||||
|
assert chat.get_name() == "title2"
|
||||||
|
|
||||||
|
def test_delete_and_send_fails(self, acfactory):
|
||||||
|
ac1 = acfactory.get_configured_offline_account()
|
||||||
|
contact1 = ac1.create_contact("some1@hello.com", name="some1")
|
||||||
|
chat = ac1.create_chat_by_contact(contact1)
|
||||||
|
chat.delete()
|
||||||
|
ac1._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
chat.send_text("msg1")
|
||||||
|
|
||||||
|
def test_create_message(self, acfactory):
|
||||||
|
ac1 = acfactory.get_configured_offline_account()
|
||||||
|
message = ac1.create_message("text")
|
||||||
|
assert message.id == 0
|
||||||
|
assert message._dc_msg is message._dc_msg
|
||||||
|
message.set_text("hello")
|
||||||
|
assert message.text == "hello"
|
||||||
|
assert message.id == 0
|
||||||
|
|
||||||
|
def test_message(self, acfactory):
|
||||||
|
ac1 = acfactory.get_configured_offline_account()
|
||||||
|
contact1 = ac1.create_contact("some1@hello.com", name="some1")
|
||||||
|
chat = ac1.create_chat_by_contact(contact1)
|
||||||
|
msg = chat.send_text("msg1")
|
||||||
|
assert msg
|
||||||
|
assert msg.view_type.is_text()
|
||||||
|
assert msg.view_type.name == "text"
|
||||||
|
assert not msg.view_type.is_audio()
|
||||||
|
assert not msg.view_type.is_video()
|
||||||
|
assert not msg.view_type.is_gif()
|
||||||
|
assert not msg.view_type.is_file()
|
||||||
|
assert not msg.view_type.is_image()
|
||||||
|
msg_state = msg.get_state()
|
||||||
|
assert not msg_state.is_in_fresh()
|
||||||
|
assert not msg_state.is_in_noticed()
|
||||||
|
assert not msg_state.is_in_seen()
|
||||||
|
assert msg_state.is_out_pending()
|
||||||
|
assert not msg_state.is_out_failed()
|
||||||
|
assert not msg_state.is_out_delivered()
|
||||||
|
assert not msg_state.is_out_mdn_received()
|
||||||
|
|
||||||
|
def test_message_image(self, acfactory, data, lp):
|
||||||
|
ac1 = acfactory.get_configured_offline_account()
|
||||||
|
contact1 = ac1.create_contact("some1@hello.com", name="some1")
|
||||||
|
chat = ac1.create_chat_by_contact(contact1)
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
chat.send_image(path="notexists")
|
||||||
|
fn = data.get_path("d.png")
|
||||||
|
lp.sec("sending image")
|
||||||
|
msg = chat.send_image(fn)
|
||||||
|
assert msg.view_type.name == "image"
|
||||||
|
assert msg
|
||||||
|
assert msg.id > 0
|
||||||
|
assert os.path.exists(msg.filename)
|
||||||
|
assert msg.filemime == "image/png"
|
||||||
|
|
||||||
|
@pytest.mark.parametrize("typein,typeout", [
|
||||||
|
(None, "application/octet-stream"),
|
||||||
|
("text/plain", "text/plain"),
|
||||||
|
("image/png", "image/png"),
|
||||||
|
])
|
||||||
|
def test_message_file(self, acfactory, data, lp, typein, typeout):
|
||||||
|
ac1 = acfactory.get_configured_offline_account()
|
||||||
|
contact1 = ac1.create_contact("some1@hello.com", name="some1")
|
||||||
|
chat = ac1.create_chat_by_contact(contact1)
|
||||||
|
lp.sec("sending file")
|
||||||
|
fn = data.get_path("r.txt")
|
||||||
|
msg = chat.send_file(fn, typein)
|
||||||
|
assert msg
|
||||||
|
assert msg.id > 0
|
||||||
|
assert msg.view_type.name == "file"
|
||||||
|
assert msg.view_type.is_file()
|
||||||
|
assert os.path.exists(msg.filename)
|
||||||
|
assert msg.filename.endswith(msg.basename)
|
||||||
|
assert msg.filemime == typeout
|
||||||
|
|
||||||
|
def test_chat_message_distinctions(self, acfactory):
|
||||||
|
ac1 = acfactory.get_configured_offline_account()
|
||||||
|
contact1 = ac1.create_contact("some1@hello.com", name="some1")
|
||||||
|
chat = ac1.create_chat_by_contact(contact1)
|
||||||
|
past1s = datetime.utcnow() - timedelta(seconds=1)
|
||||||
|
msg = chat.send_text("msg1")
|
||||||
|
ts = msg.time_sent
|
||||||
|
assert msg.time_received is None
|
||||||
|
assert ts.strftime("Y")
|
||||||
|
assert past1s < ts
|
||||||
|
contact = msg.get_sender_contact()
|
||||||
|
assert contact == ac1.get_self_contact()
|
||||||
|
|
||||||
|
def test_basic_configure_ok_addr_setting_forbidden(self, acfactory):
|
||||||
|
ac1 = acfactory.get_configured_offline_account()
|
||||||
|
assert ac1.get_config("mail_pw")
|
||||||
|
assert ac1.is_configured()
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ac1.set_config("addr", "123@example.org")
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
ac1.configure(addr="123@example.org")
|
||||||
|
|
||||||
|
|
||||||
|
class TestOnlineAccount:
|
||||||
|
def test_forward_messages(self, acfactory):
|
||||||
|
ac1 = acfactory.get_online_configuring_account()
|
||||||
|
ac2 = acfactory.get_online_configuring_account()
|
||||||
|
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
||||||
|
chat = ac1.create_chat_by_contact(c2)
|
||||||
|
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
|
wait_successful_IMAP_SMTP_connection(ac1)
|
||||||
|
wait_configuration_progress(ac1, 1000)
|
||||||
|
wait_successful_IMAP_SMTP_connection(ac2)
|
||||||
|
wait_configuration_progress(ac2, 1000)
|
||||||
|
|
||||||
|
msg_out = chat.send_text("message2")
|
||||||
|
|
||||||
|
# wait for other account to receive
|
||||||
|
ev = ac2._evlogger.get_matching("DC_EVENT_INCOMING_MSG|DC_EVENT_MSGS_CHANGED")
|
||||||
|
assert ev[2] == msg_out.id
|
||||||
|
msg_in = ac2.get_message_by_id(msg_out.id)
|
||||||
|
assert msg_in.text == "message2"
|
||||||
|
|
||||||
|
# check the message arrived in contact-requests/deaddrop
|
||||||
|
chat2 = msg_in.chat
|
||||||
|
assert msg_in in chat2.get_messages()
|
||||||
|
assert chat2.is_deaddrop()
|
||||||
|
assert chat2 == ac2.get_deaddrop_chat()
|
||||||
|
chat3 = ac2.create_group_chat("newgroup")
|
||||||
|
assert not chat3.is_promoted()
|
||||||
|
ac2.forward_messages([msg_in], chat3)
|
||||||
|
assert chat3.is_promoted()
|
||||||
|
messages = chat3.get_messages()
|
||||||
|
ac2.delete_messages(messages)
|
||||||
|
assert not chat3.get_messages()
|
||||||
|
|
||||||
|
def test_send_and_receive_message(self, acfactory, lp):
|
||||||
|
lp.sec("starting accounts, waiting for configuration")
|
||||||
|
ac1 = acfactory.get_online_configuring_account()
|
||||||
|
ac2 = acfactory.get_online_configuring_account()
|
||||||
|
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
||||||
|
chat = ac1.create_chat_by_contact(c2)
|
||||||
|
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
|
|
||||||
|
wait_configuration_progress(ac1, 1000)
|
||||||
|
wait_configuration_progress(ac2, 1000)
|
||||||
|
|
||||||
|
lp.sec("sending text message from ac1 to ac2")
|
||||||
|
msg_out = chat.send_text("message1")
|
||||||
|
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||||
|
evt_name, data1, data2 = ev
|
||||||
|
assert data1 == chat.id
|
||||||
|
assert data2 == msg_out.id
|
||||||
|
assert msg_out.get_state().is_out_delivered()
|
||||||
|
|
||||||
|
lp.sec("wait for ac2 to receive message")
|
||||||
|
ev = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
|
assert ev[2] == msg_out.id
|
||||||
|
msg_in = ac2.get_message_by_id(msg_out.id)
|
||||||
|
assert msg_in.text == "message1"
|
||||||
|
|
||||||
|
lp.sec("check the message arrived in contact-requets/deaddrop")
|
||||||
|
chat2 = msg_in.chat
|
||||||
|
assert msg_in in chat2.get_messages()
|
||||||
|
assert chat2.is_deaddrop()
|
||||||
|
assert chat2.count_fresh_messages() == 0
|
||||||
|
assert msg_in.time_received > msg_in.time_sent
|
||||||
|
|
||||||
|
lp.sec("create new chat with contact and verify it's proper")
|
||||||
|
chat2b = ac2.create_chat_by_message(msg_in)
|
||||||
|
assert not chat2b.is_deaddrop()
|
||||||
|
assert chat2b.count_fresh_messages() == 1
|
||||||
|
|
||||||
|
lp.sec("mark chat as noticed")
|
||||||
|
chat2b.mark_noticed()
|
||||||
|
assert chat2b.count_fresh_messages() == 0
|
||||||
|
|
||||||
|
lp.sec("mark message as seen on ac2, wait for changes on ac1")
|
||||||
|
ac2.mark_seen_messages([msg_in])
|
||||||
|
lp.step("1")
|
||||||
|
ac1._evlogger.get_matching("DC_EVENT_MSG_READ")
|
||||||
|
lp.step("2")
|
||||||
|
# ac1._evlogger.get_info_matching("Message marked as seen")
|
||||||
|
assert msg_out.get_state().is_out_mdn_received()
|
||||||
|
|
||||||
|
def test_saved_mime_on_received_message(self, acfactory, lp):
|
||||||
|
lp.sec("starting accounts, waiting for configuration")
|
||||||
|
ac1 = acfactory.get_online_configuring_account()
|
||||||
|
ac2 = acfactory.get_online_configuring_account()
|
||||||
|
ac2.set_config("save_mime_headers", "1")
|
||||||
|
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
||||||
|
chat = ac1.create_chat_by_contact(c2)
|
||||||
|
wait_configuration_progress(ac1, 1000)
|
||||||
|
wait_configuration_progress(ac2, 1000)
|
||||||
|
lp.sec("sending text message from ac1 to ac2")
|
||||||
|
msg_out = chat.send_text("message1")
|
||||||
|
ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||||
|
assert msg_out.get_mime_headers() is None
|
||||||
|
|
||||||
|
lp.sec("wait for ac2 to receive message")
|
||||||
|
ev = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
|
in_id = ev[2]
|
||||||
|
mime = ac2.get_message_by_id(in_id).get_mime_headers()
|
||||||
|
assert mime.get_all("From")
|
||||||
|
assert mime.get_all("Received")
|
||||||
|
|
||||||
|
def test_send_and_receive_image(self, acfactory, lp, data):
|
||||||
|
lp.sec("starting accounts, waiting for configuration")
|
||||||
|
ac1 = acfactory.get_online_configuring_account()
|
||||||
|
ac2 = acfactory.get_online_configuring_account()
|
||||||
|
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
||||||
|
chat = ac1.create_chat_by_contact(c2)
|
||||||
|
|
||||||
|
wait_configuration_progress(ac1, 1000)
|
||||||
|
wait_configuration_progress(ac2, 1000)
|
||||||
|
|
||||||
|
lp.sec("sending image message from ac1 to ac2")
|
||||||
|
path = data.get_path("d.png")
|
||||||
|
msg_out = chat.send_image(path)
|
||||||
|
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||||
|
evt_name, data1, data2 = ev
|
||||||
|
assert data1 == chat.id
|
||||||
|
assert data2 == msg_out.id
|
||||||
|
assert msg_out.get_state().is_out_delivered()
|
||||||
|
|
||||||
|
lp.sec("wait for ac2 to receive message")
|
||||||
|
ev = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
|
assert ev[2] == msg_out.id
|
||||||
|
msg_in = ac2.get_message_by_id(msg_out.id)
|
||||||
|
assert msg_in.view_type.is_image()
|
||||||
|
assert os.path.exists(msg_in.filename)
|
||||||
|
assert os.stat(msg_in.filename).st_size == os.stat(path).st_size
|
||||||
69
python/tests/test_increation.py
Normal file
69
python/tests/test_increation.py
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from filecmp import cmp
|
||||||
|
from deltachat import const
|
||||||
|
from conftest import wait_configuration_progress, wait_msgs_changed
|
||||||
|
|
||||||
|
|
||||||
|
class TestInCreation:
|
||||||
|
def test_forward_increation(self, acfactory, data, lp):
|
||||||
|
ac1 = acfactory.get_online_configuring_account()
|
||||||
|
ac2 = acfactory.get_online_configuring_account()
|
||||||
|
wait_configuration_progress(ac1, 1000)
|
||||||
|
wait_configuration_progress(ac2, 1000)
|
||||||
|
|
||||||
|
blobdir = ac1.get_blobdir()
|
||||||
|
|
||||||
|
c2 = ac1.create_contact(email=ac2.get_config("addr"))
|
||||||
|
chat = ac1.create_chat_by_contact(c2)
|
||||||
|
assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
|
wait_msgs_changed(ac1, 0, 0) # why no chat id?
|
||||||
|
|
||||||
|
lp.sec("create a message with a file in creation")
|
||||||
|
path = os.path.join(blobdir, "d.png")
|
||||||
|
open(path, 'a').close()
|
||||||
|
prepared_original = chat.prepare_file(path)
|
||||||
|
assert prepared_original.get_state().is_out_preparing()
|
||||||
|
wait_msgs_changed(ac1, chat.id, prepared_original.id)
|
||||||
|
|
||||||
|
lp.sec("forward the message while still in creation")
|
||||||
|
chat2 = ac1.create_group_chat("newgroup")
|
||||||
|
chat2.add_contact(c2)
|
||||||
|
wait_msgs_changed(ac1, 0, 0) # why not chat id?
|
||||||
|
ac1.forward_messages([prepared_original], chat2)
|
||||||
|
forwarded_id = wait_msgs_changed(ac1, chat2.id)
|
||||||
|
forwarded_msg = ac1.get_message_by_id(forwarded_id)
|
||||||
|
assert forwarded_msg.get_state().is_out_preparing()
|
||||||
|
|
||||||
|
lp.sec("finish creating the file and send it")
|
||||||
|
shutil.copy(data.get_path("d.png"), path)
|
||||||
|
sent_original = chat.send_prepared(prepared_original)
|
||||||
|
assert sent_original.id == prepared_original.id
|
||||||
|
state = sent_original.get_state()
|
||||||
|
assert state.is_out_pending() or state.is_out_delivered()
|
||||||
|
wait_msgs_changed(ac1, chat.id, sent_original.id)
|
||||||
|
|
||||||
|
lp.sec("expect the forwarded message to be sent now too")
|
||||||
|
wait_msgs_changed(ac1, chat2.id, forwarded_id)
|
||||||
|
state = ac1.get_message_by_id(forwarded_id).get_state()
|
||||||
|
assert state.is_out_pending() or state.is_out_delivered()
|
||||||
|
|
||||||
|
lp.sec("wait for the messages to be delivered to SMTP")
|
||||||
|
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||||
|
assert ev[1] == chat.id
|
||||||
|
assert ev[2] == sent_original.id
|
||||||
|
ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED")
|
||||||
|
assert ev[1] == chat2.id
|
||||||
|
assert ev[2] == forwarded_id
|
||||||
|
|
||||||
|
lp.sec("wait for both messages to arrive")
|
||||||
|
ev1 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
|
assert ev1[1] >= const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
|
received_original = ac2.get_message_by_id(ev1[2])
|
||||||
|
assert cmp(received_original.filename, path, False)
|
||||||
|
ev2 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED")
|
||||||
|
assert ev2[1] >= const.DC_CHAT_ID_LAST_SPECIAL
|
||||||
|
assert ev2[1] != ev1[1]
|
||||||
|
received_copy = ac2.get_message_by_id(ev2[2])
|
||||||
|
assert cmp(received_copy.filename, path, False)
|
||||||
29
python/tests/test_lowlevel.py
Normal file
29
python/tests/test_lowlevel.py
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
import pytest
|
||||||
|
from deltachat import capi, Account, const
|
||||||
|
|
||||||
|
|
||||||
|
def test_empty_context():
|
||||||
|
ctx = capi.lib.dc_context_new(capi.ffi.NULL, capi.ffi.NULL, capi.ffi.NULL)
|
||||||
|
capi.lib.dc_close(ctx)
|
||||||
|
|
||||||
|
|
||||||
|
def test_wrong_db(tmpdir):
|
||||||
|
tmpdir.join("hello.db").write("123")
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
Account(db_path=tmpdir.strpath)
|
||||||
|
|
||||||
|
|
||||||
|
def test_event_defines():
|
||||||
|
assert const.DC_EVENT_INFO == 100
|
||||||
|
assert const.DC_CONTACT_ID_SELF
|
||||||
|
|
||||||
|
|
||||||
|
def test_sig():
|
||||||
|
sig = capi.lib.dc_get_event_signature_types
|
||||||
|
assert sig(const.DC_EVENT_INFO) == 2
|
||||||
|
assert sig(const.DC_EVENT_WARNING) == 2
|
||||||
|
assert sig(const.DC_EVENT_ERROR) == 2
|
||||||
|
assert sig(const.DC_EVENT_SMTP_CONNECTED) == 2
|
||||||
|
assert sig(const.DC_EVENT_IMAP_CONNECTED) == 2
|
||||||
|
assert sig(const.DC_EVENT_SMTP_MESSAGE_SENT) == 2
|
||||||
60
python/tox.ini
Normal file
60
python/tox.ini
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
[tox]
|
||||||
|
# make sure to update environment list in travis.yml and appveyor.yml
|
||||||
|
envlist =
|
||||||
|
py27
|
||||||
|
py35
|
||||||
|
lint
|
||||||
|
auditwheels
|
||||||
|
|
||||||
|
[testenv]
|
||||||
|
commands =
|
||||||
|
pytest -rsXx {posargs:tests}
|
||||||
|
pip wheel . -w {toxworkdir}/wheelhouse
|
||||||
|
|
||||||
|
passenv =
|
||||||
|
TRAVIS
|
||||||
|
LD_LIBRARY_PATH
|
||||||
|
CFLAGS
|
||||||
|
deps =
|
||||||
|
pytest
|
||||||
|
pytest-faulthandler
|
||||||
|
pdbpp
|
||||||
|
|
||||||
|
[testenv:auditwheels]
|
||||||
|
skipsdist = True
|
||||||
|
commands =
|
||||||
|
python tests/auditwheels.py {toxworkdir}/wheelhouse
|
||||||
|
|
||||||
|
|
||||||
|
[testenv:lint]
|
||||||
|
skipsdist = True
|
||||||
|
usedevelop = True
|
||||||
|
basepython = python2.7
|
||||||
|
deps =
|
||||||
|
flake8
|
||||||
|
# pygments required by rst-lint
|
||||||
|
pygments
|
||||||
|
restructuredtext_lint
|
||||||
|
commands =
|
||||||
|
flake8 src/deltachat
|
||||||
|
flake8 tests/
|
||||||
|
rst-lint --encoding 'utf-8' README.rst
|
||||||
|
|
||||||
|
[testenv:doc]
|
||||||
|
basepython = python3.5
|
||||||
|
deps =
|
||||||
|
sphinx==2.0.1
|
||||||
|
breathe
|
||||||
|
|
||||||
|
changedir = doc
|
||||||
|
commands =
|
||||||
|
sphinx-build -w docker-toxdoc-warnings.log -b html . _build/html
|
||||||
|
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
python_files = tests/test_*.py
|
||||||
|
norecursedirs = .tox
|
||||||
|
xfail_strict=true
|
||||||
|
|
||||||
|
[flake8]
|
||||||
|
max-line-length = 120
|
||||||
Reference in New Issue
Block a user