mirror of
https://github.com/chatmail/core.git
synced 2026-04-05 23:22:11 +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
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