Merge pull request #3381 from deltachat/adb/apply-black-and-isort

apply isort and black formatters, add format checking to CI
This commit is contained in:
Asiel Díaz Benítez
2022-06-02 08:01:17 -04:00
committed by GitHub
27 changed files with 674 additions and 549 deletions

View File

@@ -1,4 +1,3 @@
# content of echo_and_quit.py # content of echo_and_quit.py
from deltachat import account_hookimpl, run_cmdline from deltachat import account_hookimpl, run_cmdline

View File

@@ -1,4 +1,3 @@
# content of group_tracking.py # content of group_tracking.py
from deltachat import account_hookimpl, run_cmdline from deltachat import account_hookimpl, run_cmdline
@@ -33,15 +32,21 @@ class GroupTrackingPlugin:
@account_hookimpl @account_hookimpl
def ac_member_added(self, chat, contact, actor, message): def ac_member_added(self, chat, contact, actor, message):
print("ac_member_added {} to chat {} from {}".format( print(
contact.addr, chat.id, actor or message.get_sender_contact().addr)) "ac_member_added {} to chat {} from {}".format(
contact.addr, chat.id, actor or message.get_sender_contact().addr
)
)
for member in chat.get_contacts(): for member in chat.get_contacts():
print("chat member: {}".format(member.addr)) print("chat member: {}".format(member.addr))
@account_hookimpl @account_hookimpl
def ac_member_removed(self, chat, contact, actor, message): def ac_member_removed(self, chat, contact, actor, message):
print("ac_member_removed {} from chat {} by {}".format( print(
contact.addr, chat.id, actor or message.get_sender_contact().addr)) "ac_member_removed {} from chat {} by {}".format(
contact.addr, chat.id, actor or message.get_sender_contact().addr
)
)
def main(argv=None): def main(argv=None):

View File

@@ -1,20 +1,20 @@
import pytest
import py
import echo_and_quit import echo_and_quit
import group_tracking import group_tracking
import py
import pytest
from deltachat.events import FFIEventLogger from deltachat.events import FFIEventLogger
@pytest.fixture(scope='session') @pytest.fixture(scope="session")
def datadir(): def datadir():
"""The py.path.local object of the test-data/ directory.""" """The py.path.local object of the test-data/ directory."""
for path in reversed(py.path.local(__file__).parts()): for path in reversed(py.path.local(__file__).parts()):
datadir = path.join('test-data') datadir = path.join("test-data")
if datadir.isdir(): if datadir.isdir():
return datadir return datadir
else: else:
pytest.skip('test-data directory not found') pytest.skip("test-data directory not found")
def test_echo_quit_plugin(acfactory, lp): def test_echo_quit_plugin(acfactory, lp):
@@ -22,7 +22,7 @@ def test_echo_quit_plugin(acfactory, lp):
botproc = acfactory.run_bot_process(echo_and_quit) botproc = acfactory.run_bot_process(echo_and_quit)
lp.sec("creating a temp account to contact the bot") lp.sec("creating a temp account to contact the bot")
ac1, = acfactory.get_online_accounts(1) (ac1,) = acfactory.get_online_accounts(1)
lp.sec("sending a message to the bot") lp.sec("sending a message to the bot")
bot_contact = ac1.create_contact(botproc.addr) bot_contact = ac1.create_contact(botproc.addr)
@@ -44,9 +44,11 @@ def test_group_tracking_plugin(acfactory, lp):
ac1, ac2 = acfactory.get_online_accounts(2) ac1, ac2 = acfactory.get_online_accounts(2)
botproc.fnmatch_lines(""" botproc.fnmatch_lines(
"""
*ac_configure_completed* *ac_configure_completed*
""") """
)
ac1.add_account_plugin(FFIEventLogger(ac1)) ac1.add_account_plugin(FFIEventLogger(ac1))
ac2.add_account_plugin(FFIEventLogger(ac2)) ac2.add_account_plugin(FFIEventLogger(ac2))
@@ -56,9 +58,11 @@ def test_group_tracking_plugin(acfactory, lp):
ch.add_contact(bot_contact) ch.add_contact(bot_contact)
ch.send_text("hello") ch.send_text("hello")
botproc.fnmatch_lines(""" botproc.fnmatch_lines(
"""
*ac_chat_modified*bot test group* *ac_chat_modified*bot test group*
""") """
)
lp.sec("adding third member {}".format(ac2.get_config("addr"))) lp.sec("adding third member {}".format(ac2.get_config("addr")))
contact3 = ac1.create_contact(ac2.get_config("addr")) contact3 = ac1.create_contact(ac2.get_config("addr"))
@@ -68,12 +72,20 @@ def test_group_tracking_plugin(acfactory, lp):
assert "hello" in reply.text assert "hello" in reply.text
lp.sec("now looking at what the bot received") lp.sec("now looking at what the bot received")
botproc.fnmatch_lines(""" botproc.fnmatch_lines(
"""
*ac_member_added {}*from*{}* *ac_member_added {}*from*{}*
""".format(contact3.addr, ac1.get_config("addr"))) """.format(
contact3.addr, ac1.get_config("addr")
)
)
lp.sec("contact successfully added, now removing") lp.sec("contact successfully added, now removing")
ch.remove_contact(contact3) ch.remove_contact(contact3)
botproc.fnmatch_lines(""" botproc.fnmatch_lines(
"""
*ac_member_removed {}*from*{}* *ac_member_removed {}*from*{}*
""".format(contact3.addr, ac1.get_config("addr"))) """.format(
contact3.addr, ac1.get_config("addr")
)
)

View File

@@ -6,3 +6,6 @@ build-backend = "setuptools.build_meta"
root = ".." root = ".."
tag_regex = '^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$' tag_regex = '^(?P<prefix>py-)?(?P<version>[^\+]+)(?P<suffix>.*)?$'
git_describe_command = "git describe --dirty --tags --long --match py-*.*" git_describe_command = "git describe --dirty --tags --long --match py-*.*"
[tool.black]
line-length = 120

View File

@@ -1,15 +1,15 @@
import sys import sys
from . import capi, const, hookspec # noqa from pkg_resources import DistributionNotFound, get_distribution
from .capi import ffi # noqa
from .account import Account, get_core_info # noqa from . import capi, const, events, hookspec # noqa
from .message import Message # noqa from .account import Account, get_core_info # noqa
from .contact import Contact # noqa from .capi import ffi # noqa
from .chat import Chat # noqa from .chat import Chat # noqa
from .hookspec import account_hookimpl, global_hookimpl # noqa from .contact import Contact # noqa
from . import events from .hookspec import account_hookimpl, global_hookimpl # noqa
from .message import Message # noqa
from pkg_resources import get_distribution, DistributionNotFound
try: try:
__version__ = get_distribution(__name__).version __version__ = get_distribution(__name__).version
except DistributionNotFound: except DistributionNotFound:
@@ -26,7 +26,7 @@ def get_dc_event_name(integer, _DC_EVENTNAME_MAP={}):
def register_global_plugin(plugin): def register_global_plugin(plugin):
""" Register a global plugin which implements one or more """Register a global plugin which implements one or more
of the :class:`deltachat.hookspec.Global` hooks. of the :class:`deltachat.hookspec.Global` hooks.
""" """
gm = hookspec.Global._get_plugin_manager() gm = hookspec.Global._get_plugin_manager()
@@ -43,9 +43,10 @@ register_global_plugin(events)
def run_cmdline(argv=None, account_plugins=None): def run_cmdline(argv=None, account_plugins=None):
""" Run a simple default command line app, registering the specified """Run a simple default command line app, registering the specified
account plugins. """ account plugins."""
import argparse import argparse
if argv is None: if argv is None:
argv = sys.argv argv = sys.argv
@@ -69,9 +70,9 @@ def run_cmdline(argv=None, account_plugins=None):
ac.add_account_plugin(plugin) ac.add_account_plugin(plugin)
if not ac.is_configured(): if not ac.is_configured():
assert args.email and args.password, ( assert (
"you must specify --email and --password once to configure this database/account" args.email and args.password
) ), "you must specify --email and --password once to configure this database/account"
ac.set_config("addr", args.email) ac.set_config("addr", args.email)
ac.set_config("mail_pw", args.password) ac.set_config("mail_pw", args.password)
ac.set_config("mvbox_move", "0") ac.set_config("mvbox_move", "0")

View File

@@ -20,30 +20,33 @@ def local_build_flags(projdir, target):
:param target: The rust build target, `debug` or `release`. :param target: The rust build target, `debug` or `release`.
""" """
flags = {} flags = {}
if platform.system() == 'Darwin': if platform.system() == "Darwin":
flags['libraries'] = ['resolv', 'dl'] flags["libraries"] = ["resolv", "dl"]
flags['extra_link_args'] = [ flags["extra_link_args"] = [
'-framework', 'CoreFoundation', "-framework",
'-framework', 'CoreServices', "CoreFoundation",
'-framework', 'Security', "-framework",
"CoreServices",
"-framework",
"Security",
] ]
elif platform.system() == 'Linux': elif platform.system() == "Linux":
flags['libraries'] = ['rt', 'dl', 'm'] flags["libraries"] = ["rt", "dl", "m"]
flags['extra_link_args'] = [] flags["extra_link_args"] = []
else: else:
raise NotImplementedError("Compilation not supported yet on Windows, can you help?") raise NotImplementedError("Compilation not supported yet on Windows, can you help?")
target_dir = os.environ.get("CARGO_TARGET_DIR") target_dir = os.environ.get("CARGO_TARGET_DIR")
if target_dir is None: if target_dir is None:
target_dir = os.path.join(projdir, 'target') target_dir = os.path.join(projdir, "target")
flags['extra_objects'] = [os.path.join(target_dir, target, 'libdeltachat.a')] flags["extra_objects"] = [os.path.join(target_dir, target, "libdeltachat.a")]
assert os.path.exists(flags['extra_objects'][0]), flags['extra_objects'] assert os.path.exists(flags["extra_objects"][0]), flags["extra_objects"]
flags['include_dirs'] = [os.path.join(projdir, 'deltachat-ffi')] flags["include_dirs"] = [os.path.join(projdir, "deltachat-ffi")]
return flags return flags
def system_build_flags(): def system_build_flags():
"""Construct build flags for building against an installed libdeltachat.""" """Construct build flags for building against an installed libdeltachat."""
return pkgconfig.parse('deltachat') return pkgconfig.parse("deltachat")
def extract_functions(flags): def extract_functions(flags):
@@ -61,11 +64,13 @@ def extract_functions(flags):
src_name = os.path.join(tmpdir, "include.h") src_name = os.path.join(tmpdir, "include.h")
dst_name = os.path.join(tmpdir, "expanded.h") dst_name = os.path.join(tmpdir, "expanded.h")
with open(src_name, "w") as src_fp: with open(src_name, "w") as src_fp:
src_fp.write('#include <deltachat.h>') src_fp.write("#include <deltachat.h>")
cc.preprocess(source=src_name, cc.preprocess(
output_file=dst_name, source=src_name,
include_dirs=flags['include_dirs'], output_file=dst_name,
macros=[('PY_CFFI', '1')]) include_dirs=flags["include_dirs"],
macros=[("PY_CFFI", "1")],
)
with open(dst_name, "r") as dst_fp: with open(dst_name, "r") as dst_fp:
return dst_fp.read() return dst_fp.read()
finally: finally:
@@ -87,7 +92,9 @@ def find_header(flags):
obj_name = os.path.join(tmpdir, "where.o") obj_name = os.path.join(tmpdir, "where.o")
dst_name = os.path.join(tmpdir, "where") dst_name = os.path.join(tmpdir, "where")
with open(src_name, "w") as src_fp: with open(src_name, "w") as src_fp:
src_fp.write(textwrap.dedent(""" src_fp.write(
textwrap.dedent(
"""
#include <stdio.h> #include <stdio.h>
#include <deltachat.h> #include <deltachat.h>
@@ -95,18 +102,20 @@ def find_header(flags):
printf("%s", _dc_header_file_location()); printf("%s", _dc_header_file_location());
return 0; return 0;
} }
""")) """
)
)
cwd = os.getcwd() cwd = os.getcwd()
try: try:
os.chdir(tmpdir) os.chdir(tmpdir)
cc.compile(sources=["where.c"], cc.compile(
include_dirs=flags['include_dirs'], sources=["where.c"],
macros=[("PY_CFFI_INC", "1")]) include_dirs=flags["include_dirs"],
macros=[("PY_CFFI_INC", "1")],
)
finally: finally:
os.chdir(cwd) os.chdir(cwd)
cc.link_executable(objects=[obj_name], cc.link_executable(objects=[obj_name], output_progname="where", output_dir=tmpdir)
output_progname="where",
output_dir=tmpdir)
return subprocess.check_output(dst_name) return subprocess.check_output(dst_name)
finally: finally:
shutil.rmtree(tmpdir) shutil.rmtree(tmpdir)
@@ -123,7 +132,8 @@ def extract_defines(flags):
cdef() method. cdef() method.
""" """
header = find_header(flags) header = find_header(flags)
defines_re = re.compile(r""" defines_re = re.compile(
r"""
\#define\s+ # The start of a define. \#define\s+ # The start of a define.
( # Begin capturing group which captures the define name. ( # Begin capturing group which captures the define name.
(?: # A nested group which is not captured, this allows us (?: # A nested group which is not captured, this allows us
@@ -151,26 +161,28 @@ def extract_defines(flags):
) # Close the capturing group, this contains ) # Close the capturing group, this contains
# the entire name e.g. DC_MSG_TEXT. # the entire name e.g. DC_MSG_TEXT.
\s+\S+ # Ensure there is whitespace followed by a value. \s+\S+ # Ensure there is whitespace followed by a value.
""", re.VERBOSE) """,
re.VERBOSE,
)
defines = [] defines = []
with open(header) as fp: with open(header) as fp:
for line in fp: for line in fp:
match = defines_re.match(line) match = defines_re.match(line)
if match: if match:
defines.append(match.group(1)) defines.append(match.group(1))
return '\n'.join('#define {} ...'.format(d) for d in defines) return "\n".join("#define {} ...".format(d) for d in defines)
def ffibuilder(): def ffibuilder():
projdir = os.environ.get('DCC_RS_DEV') projdir = os.environ.get("DCC_RS_DEV")
if projdir: if projdir:
target = os.environ.get('DCC_RS_TARGET', 'release') target = os.environ.get("DCC_RS_TARGET", "release")
flags = local_build_flags(projdir, target) flags = local_build_flags(projdir, target)
else: else:
flags = system_build_flags() flags = system_build_flags()
builder = cffi.FFI() builder = cffi.FFI()
builder.set_source( builder.set_source(
'deltachat.capi', "deltachat.capi",
""" """
#include <deltachat.h> #include <deltachat.h>
int dc_event_has_string_data(int e) int dc_event_has_string_data(int e)
@@ -180,11 +192,13 @@ def ffibuilder():
""", """,
**flags, **flags,
) )
builder.cdef(""" builder.cdef(
"""
typedef int... time_t; typedef int... time_t;
void free(void *ptr); void free(void *ptr);
extern int dc_event_has_string_data(int); extern int dc_event_has_string_data(int);
""") """
)
function_defs = extract_functions(flags) function_defs = extract_functions(flags)
defines = extract_defines(flags) defines = extract_defines(flags)
builder.cdef(function_defs) builder.cdef(function_defs)
@@ -192,8 +206,9 @@ def ffibuilder():
return builder return builder
if __name__ == '__main__': if __name__ == "__main__":
import os.path import os.path
pkgdir = os.path.join(os.path.dirname(__file__), '..')
pkgdir = os.path.join(os.path.dirname(__file__), "..")
builder = ffibuilder() builder = ffibuilder()
builder.compile(tmpdir=pkgdir, verbose=True) builder.compile(tmpdir=pkgdir, verbose=True)

View File

@@ -1,37 +1,46 @@
""" Account class implementation. """ """ Account class implementation. """
from __future__ import print_function from __future__ import print_function
import os
from array import array
from contextlib import contextmanager from contextlib import contextmanager
from email.utils import parseaddr from email.utils import parseaddr
from threading import Event from threading import Event
import os from typing import Any, Dict, Generator, List, Optional, Union
from array import array
from . import const from . import const, hookspec
from .capi import ffi, lib from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array, DCLot
from .chat import Chat from .chat import Chat
from .message import Message
from .contact import Contact from .contact import Contact
from .tracker import ImexTracker, ConfigureTracker from .cutil import (
from . import hookspec DCLot,
as_dc_charpointer,
from_dc_charpointer,
from_optional_dc_charpointer,
iter_array,
)
from .events import EventThread from .events import EventThread
from typing import Union, Any, Dict, Optional, List, Generator from .message import Message
from .tracker import ConfigureTracker, ImexTracker
class MissingCredentials(ValueError): class MissingCredentials(ValueError):
""" Account is missing `addr` and `mail_pw` config values. """ """Account is missing `addr` and `mail_pw` config values."""
def get_core_info(): def get_core_info():
""" get some system info. """ """get some system info."""
from tempfile import NamedTemporaryFile from tempfile import NamedTemporaryFile
with NamedTemporaryFile() as path: with NamedTemporaryFile() as path:
path.close() path.close()
return get_dc_info_as_dict(ffi.gc( return get_dc_info_as_dict(
lib.dc_context_new(as_dc_charpointer(""), as_dc_charpointer(path.name), ffi.NULL), ffi.gc(
lib.dc_context_unref, lib.dc_context_new(as_dc_charpointer(""), as_dc_charpointer(path.name), ffi.NULL),
)) lib.dc_context_unref,
)
)
def get_dc_info_as_dict(dc_context): def get_dc_info_as_dict(dc_context):
@@ -46,14 +55,15 @@ def get_dc_info_as_dict(dc_context):
class Account(object): class Account(object):
""" Each account is tied to a sqlite database file which is fully managed """Each account is tied to a sqlite database file which is fully managed
by the underlying deltachat core library. All public Account methods are by the underlying deltachat core library. All public Account methods are
meant to be memory-safe and return memory-safe objects. meant to be memory-safe and return memory-safe objects.
""" """
MissingCredentials = MissingCredentials MissingCredentials = MissingCredentials
def __init__(self, db_path, os_name=None, logging=True) -> None: def __init__(self, db_path, os_name=None, logging=True) -> None:
""" initialize account object. """initialize account object.
:param db_path: a path to the account database. The database :param db_path: a path to the account database. The database
will be created if it doesn't exist. will be created if it doesn't exist.
@@ -83,11 +93,11 @@ class Account(object):
hook.dc_account_init(account=self) hook.dc_account_init(account=self)
def disable_logging(self) -> None: def disable_logging(self) -> None:
""" disable logging. """ """disable logging."""
self._logging = False self._logging = False
def enable_logging(self) -> None: def enable_logging(self) -> None:
""" re-enable logging. """ """re-enable logging."""
self._logging = True self._logging = True
def __repr__(self): def __repr__(self):
@@ -102,11 +112,10 @@ class Account(object):
def _check_config_key(self, name: str) -> None: def _check_config_key(self, name: str) -> None:
if name not in self._configkeys: if name not in self._configkeys:
raise KeyError("{!r} not a valid config key, existing keys: {!r}".format( raise KeyError("{!r} not a valid config key, existing keys: {!r}".format(name, self._configkeys))
name, self._configkeys))
def get_info(self) -> Dict[str, str]: def get_info(self) -> Dict[str, str]:
""" return dictionary of built config parameters. """ """return dictionary of built config parameters."""
return get_dc_info_as_dict(self._dc_context) return get_dc_info_as_dict(self._dc_context)
def dump_account_info(self, logfile): def dump_account_info(self, logfile):
@@ -126,7 +135,7 @@ class Account(object):
log("") log("")
def set_stock_translation(self, id: int, string: str) -> None: def set_stock_translation(self, id: int, string: str) -> None:
""" set stock translation string. """set stock translation string.
:param id: id of stock string (const.DC_STR_*) :param id: id of stock string (const.DC_STR_*)
:param value: string to set as new transalation :param value: string to set as new transalation
@@ -138,7 +147,7 @@ class Account(object):
raise ValueError("could not set translation string") raise ValueError("could not set translation string")
def set_config(self, name: str, value: Optional[str]) -> None: def set_config(self, name: str, value: Optional[str]) -> None:
""" set configuration values. """set configuration values.
:param name: config key name (unicode) :param name: config key name (unicode)
:param value: value to set (unicode) :param value: value to set (unicode)
@@ -157,7 +166,7 @@ class Account(object):
lib.dc_set_config(self._dc_context, namebytes, valuebytes) lib.dc_set_config(self._dc_context, namebytes, valuebytes)
def get_config(self, name: str) -> str: def get_config(self, name: str) -> str:
""" return unicode string value. """return unicode string value.
:param name: configuration key to lookup (eg "addr" or "mail_pw") :param name: configuration key to lookup (eg "addr" or "mail_pw")
:returns: unicode value :returns: unicode value
@@ -175,15 +184,17 @@ class Account(object):
In other words, you don't need this. In other words, you don't need this.
""" """
res = lib.dc_preconfigure_keypair(self._dc_context, res = lib.dc_preconfigure_keypair(
as_dc_charpointer(addr), self._dc_context,
as_dc_charpointer(public), as_dc_charpointer(addr),
as_dc_charpointer(secret)) as_dc_charpointer(public),
as_dc_charpointer(secret),
)
if res == 0: if res == 0:
raise Exception("Failed to set key") raise Exception("Failed to set key")
def update_config(self, kwargs: Dict[str, Any]) -> None: def update_config(self, kwargs: Dict[str, Any]) -> None:
""" update config values. """update config values.
:param kwargs: name=value config settings for this account. :param kwargs: name=value config settings for this account.
values need to be unicode. values need to be unicode.
@@ -193,7 +204,7 @@ class Account(object):
self.set_config(key, value) self.set_config(key, value)
def is_configured(self) -> bool: def is_configured(self) -> bool:
""" determine if the account is configured already; an initial connection """determine if the account is configured already; an initial connection
to SMTP/IMAP has been verified. to SMTP/IMAP has been verified.
:returns: True if account is configured. :returns: True if account is configured.
@@ -219,18 +230,17 @@ class Account(object):
self.set_config("selfavatar", img_path) self.set_config("selfavatar", img_path)
def check_is_configured(self) -> None: def check_is_configured(self) -> None:
""" Raise ValueError if this account is not configured. """ """Raise ValueError if this account is not configured."""
if not self.is_configured(): if not self.is_configured():
raise ValueError("need to configure first") raise ValueError("need to configure first")
def get_latest_backupfile(self, backupdir) -> Optional[str]: def get_latest_backupfile(self, backupdir) -> Optional[str]:
""" return the latest backup file in a given directory. """return the latest backup file in a given directory."""
"""
res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir)) res = lib.dc_imex_has_backup(self._dc_context, as_dc_charpointer(backupdir))
return from_optional_dc_charpointer(res) return from_optional_dc_charpointer(res)
def get_blobdir(self) -> str: def get_blobdir(self) -> str:
""" return the directory for files. """return the directory for files.
All sent files are copied to this directory if necessary. All sent files are copied to this directory if necessary.
Place files there directly to avoid copying. Place files there directly to avoid copying.
@@ -238,7 +248,7 @@ class Account(object):
return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context)) return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context))
def get_self_contact(self) -> Contact: def get_self_contact(self) -> Contact:
""" return this account's identity as a :class:`deltachat.contact.Contact`. """return this account's identity as a :class:`deltachat.contact.Contact`.
:returns: :class:`deltachat.contact.Contact` :returns: :class:`deltachat.contact.Contact`
""" """
@@ -280,14 +290,14 @@ class Account(object):
elif isinstance(obj, str): elif isinstance(obj, str):
displayname, addr = parseaddr(obj) displayname, addr = parseaddr(obj)
else: else:
raise TypeError("don't know how to create chat for %r" % (obj, )) raise TypeError("don't know how to create chat for %r" % (obj,))
if name is None and displayname: if name is None and displayname:
name = displayname name = displayname
return (name, addr) return (name, addr)
def delete_contact(self, contact: Contact) -> bool: def delete_contact(self, contact: Contact) -> bool:
""" delete a Contact. """delete a Contact.
:param contact: contact object obtained :param contact: contact object obtained
:returns: True if deletion succeeded (contact was deleted) :returns: True if deletion succeeded (contact was deleted)
@@ -298,7 +308,7 @@ class Account(object):
return bool(lib.dc_delete_contact(self._dc_context, contact_id)) return bool(lib.dc_delete_contact(self._dc_context, contact_id))
def get_contact_by_addr(self, email: str) -> Optional[Contact]: def get_contact_by_addr(self, email: str) -> Optional[Contact]:
""" get a contact for the email address or None if it's blocked or doesn't exist. """ """get a contact for the email address or None if it's blocked or doesn't exist."""
_, addr = parseaddr(email) _, addr = parseaddr(email)
addr = as_dc_charpointer(addr) addr = as_dc_charpointer(addr)
contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr) contact_id = lib.dc_lookup_contact_id_by_addr(self._dc_context, addr)
@@ -307,21 +317,18 @@ class Account(object):
return None return None
def get_contact_by_id(self, contact_id: int) -> Contact: def get_contact_by_id(self, contact_id: int) -> Contact:
""" return Contact instance or raise an exception. """return Contact instance or raise an exception.
:param contact_id: integer id of this contact. :param contact_id: integer id of this contact.
:returns: :class:`deltachat.contact.Contact` instance. :returns: :class:`deltachat.contact.Contact` instance.
""" """
return Contact(self, contact_id) return Contact(self, contact_id)
def get_blocked_contacts(self) -> List[Contact]: def get_blocked_contacts(self) -> List[Contact]:
""" return a list of all blocked contacts. """return a list of all blocked contacts.
:returns: list of :class:`deltachat.contact.Contact` objects. :returns: list of :class:`deltachat.contact.Contact` objects.
""" """
dc_array = ffi.gc( dc_array = ffi.gc(lib.dc_get_blocked_contacts(self._dc_context), lib.dc_array_unref)
lib.dc_get_blocked_contacts(self._dc_context),
lib.dc_array_unref
)
return list(iter_array(dc_array, lambda x: Contact(self, x))) return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_contacts( def get_contacts(
@@ -344,22 +351,16 @@ class Account(object):
flags |= const.DC_GCL_VERIFIED_ONLY flags |= const.DC_GCL_VERIFIED_ONLY
if with_self: if with_self:
flags |= const.DC_GCL_ADD_SELF flags |= const.DC_GCL_ADD_SELF
dc_array = ffi.gc( dc_array = ffi.gc(lib.dc_get_contacts(self._dc_context, flags, query), lib.dc_array_unref)
lib.dc_get_contacts(self._dc_context, flags, query),
lib.dc_array_unref
)
return list(iter_array(dc_array, lambda x: Contact(self, x))) return list(iter_array(dc_array, lambda x: Contact(self, x)))
def get_fresh_messages(self) -> Generator[Message, None, None]: def get_fresh_messages(self) -> Generator[Message, None, None]:
""" yield all fresh messages from all chats. """ """yield all fresh messages from all chats."""
dc_array = ffi.gc( dc_array = ffi.gc(lib.dc_get_fresh_msgs(self._dc_context), lib.dc_array_unref)
lib.dc_get_fresh_msgs(self._dc_context),
lib.dc_array_unref
)
yield from iter_array(dc_array, lambda x: Message.from_db(self, x)) yield from iter_array(dc_array, lambda x: Message.from_db(self, x))
def create_chat(self, obj) -> Chat: def create_chat(self, obj) -> Chat:
""" Create a 1:1 chat with Account, Contact or e-mail address. """ """Create a 1:1 chat with Account, Contact or e-mail address."""
return self.create_contact(obj).create_chat() return self.create_contact(obj).create_chat()
def create_group_chat( def create_group_chat(
@@ -385,14 +386,11 @@ class Account(object):
return chat return chat
def get_chats(self) -> List[Chat]: def get_chats(self) -> List[Chat]:
""" return list of chats. """return list of chats.
:returns: a list of :class:`deltachat.chat.Chat` objects. :returns: a list of :class:`deltachat.chat.Chat` objects.
""" """
dc_chatlist = ffi.gc( dc_chatlist = ffi.gc(lib.dc_get_chatlist(self._dc_context, 0, ffi.NULL, 0), lib.dc_chatlist_unref)
lib.dc_get_chatlist(self._dc_context, 0, ffi.NULL, 0),
lib.dc_chatlist_unref
)
assert dc_chatlist != ffi.NULL assert dc_chatlist != ffi.NULL
chatlist = [] chatlist = []
@@ -405,14 +403,14 @@ class Account(object):
return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat() return Contact(self, const.DC_CONTACT_ID_DEVICE).create_chat()
def get_message_by_id(self, msg_id: int) -> Message: def get_message_by_id(self, msg_id: int) -> Message:
""" return Message instance. """return Message instance.
:param msg_id: integer id of this message. :param msg_id: integer id of this message.
:returns: :class:`deltachat.message.Message` instance. :returns: :class:`deltachat.message.Message` instance.
""" """
return Message.from_db(self, msg_id) return Message.from_db(self, msg_id)
def get_chat_by_id(self, chat_id: int) -> Chat: def get_chat_by_id(self, chat_id: int) -> Chat:
""" return Chat instance. """return Chat instance.
:param chat_id: integer id of this chat. :param chat_id: integer id of this chat.
:returns: :class:`deltachat.chat.Chat` instance. :returns: :class:`deltachat.chat.Chat` instance.
:raises: ValueError if chat does not exist. :raises: ValueError if chat does not exist.
@@ -424,7 +422,7 @@ class Account(object):
return Chat(self, chat_id) return Chat(self, chat_id)
def mark_seen_messages(self, messages: List[Union[int, Message]]) -> None: def mark_seen_messages(self, messages: List[Union[int, Message]]) -> None:
""" mark the given set of messages as seen. """mark the given set of messages as seen.
:param messages: a list of message ids or Message instances. :param messages: a list of message ids or Message instances.
""" """
@@ -438,7 +436,7 @@ class Account(object):
lib.dc_markseen_msgs(self._dc_context, msg_ids, len(messages)) lib.dc_markseen_msgs(self._dc_context, msg_ids, len(messages))
def forward_messages(self, messages: List[Message], chat: Chat) -> None: def forward_messages(self, messages: List[Message], chat: Chat) -> None:
""" Forward list of messages to a chat. """Forward list of messages to a chat.
:param messages: list of :class:`deltachat.message.Message` object. :param messages: list of :class:`deltachat.message.Message` object.
:param chat: :class:`deltachat.chat.Chat` object. :param chat: :class:`deltachat.chat.Chat` object.
@@ -448,7 +446,7 @@ class Account(object):
lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id) lib.dc_forward_msgs(self._dc_context, msg_ids, len(msg_ids), chat.id)
def delete_messages(self, messages: List[Message]) -> None: def delete_messages(self, messages: List[Message]) -> None:
""" delete messages (local and remote). """delete messages (local and remote).
:param messages: list of :class:`deltachat.message.Message` object. :param messages: list of :class:`deltachat.message.Message` object.
:returns: None :returns: None
@@ -457,7 +455,7 @@ class Account(object):
lib.dc_delete_msgs(self._dc_context, msg_ids, len(msg_ids)) lib.dc_delete_msgs(self._dc_context, msg_ids, len(msg_ids))
def export_self_keys(self, path): def export_self_keys(self, path):
""" export public and private keys to the specified directory. """export public and private keys to the specified directory.
Note that the account does not have to be started. Note that the account does not have to be started.
""" """
@@ -481,7 +479,7 @@ class Account(object):
return imex_tracker.wait_finish() return imex_tracker.wait_finish()
def import_self_keys(self, path): def import_self_keys(self, path):
""" Import private keys found in the `path` directory. """Import private keys found in the `path` directory.
The last imported key is made the default keys unless its name The last imported key is made the default keys unless its name
contains the string legacy. Public keys are not imported. contains the string legacy. Public keys are not imported.
@@ -517,7 +515,7 @@ class Account(object):
return from_dc_charpointer(res) return from_dc_charpointer(res)
def get_setup_contact_qr(self) -> str: def get_setup_contact_qr(self) -> str:
""" get/create Setup-Contact QR Code as ascii-string. """get/create Setup-Contact QR Code as ascii-string.
this string needs to be transferred to another DC account this string needs to be transferred to another DC account
in a second channel (typically used by mobiles with QRcode-show + scan UX) in a second channel (typically used by mobiles with QRcode-show + scan UX)
@@ -527,18 +525,15 @@ class Account(object):
return from_dc_charpointer(res) return from_dc_charpointer(res)
def check_qr(self, qr): def check_qr(self, qr):
""" check qr code and return :class:`ScannedQRCode` instance representing the result""" """check qr code and return :class:`ScannedQRCode` instance representing the result"""
res = ffi.gc( res = ffi.gc(lib.dc_check_qr(self._dc_context, as_dc_charpointer(qr)), lib.dc_lot_unref)
lib.dc_check_qr(self._dc_context, as_dc_charpointer(qr)),
lib.dc_lot_unref
)
lot = DCLot(res) lot = DCLot(res)
if lot.state() == const.DC_QR_ERROR: if lot.state() == const.DC_QR_ERROR:
raise ValueError("invalid or unknown QR code: {}".format(lot.text1())) raise ValueError("invalid or unknown QR code: {}".format(lot.text1()))
return ScannedQRCode(lot) return ScannedQRCode(lot)
def qr_setup_contact(self, qr): def qr_setup_contact(self, qr):
""" setup contact and return a Chat after contact is established. """setup contact and return a Chat after contact is established.
Note that this function may block for a long time as messages are exchanged Note that this function may block for a long time as messages are exchanged
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
@@ -552,7 +547,7 @@ class Account(object):
return Chat(self, chat_id) return Chat(self, chat_id)
def qr_join_chat(self, qr): def qr_join_chat(self, qr):
""" join a chat group through a QR code. """join a chat group through a QR code.
Note that this function may block for a long time as messages are exchanged Note that this function may block for a long time as messages are exchanged
with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance with the emitter of the QR code. On success a :class:`deltachat.chat.Chat` instance
@@ -566,9 +561,7 @@ class Account(object):
raise ValueError("could not join group") raise ValueError("could not join group")
return Chat(self, chat_id) return Chat(self, chat_id)
def set_location( def set_location(self, latitude: float = 0.0, longitude: float = 0.0, accuracy: float = 0.0) -> None:
self, latitude: float = 0.0, longitude: float = 0.0, accuracy: float = 0.0
) -> None:
"""set a new location. It effects all chats where we currently """set a new location. It effects all chats where we currently
have enabled location streaming. have enabled location streaming.
@@ -587,7 +580,7 @@ class Account(object):
# #
def add_account_plugin(self, plugin, name=None): def add_account_plugin(self, plugin, name=None):
""" add an account plugin which implements one or more of """add an account plugin which implements one or more of
the :class:`deltachat.hookspec.PerAccount` hooks. the :class:`deltachat.hookspec.PerAccount` hooks.
""" """
if name and self._pm.has_plugin(name=name): if name and self._pm.has_plugin(name=name):
@@ -597,18 +590,18 @@ class Account(object):
return plugin return plugin
def remove_account_plugin(self, plugin, name=None): def remove_account_plugin(self, plugin, name=None):
""" remove an account plugin. """ """remove an account plugin."""
self._pm.unregister(plugin, name=name) self._pm.unregister(plugin, name=name)
@contextmanager @contextmanager
def temp_plugin(self, plugin): def temp_plugin(self, plugin):
""" run a with-block with the given plugin temporarily registered. """ """run a with-block with the given plugin temporarily registered."""
self._pm.register(plugin) self._pm.register(plugin)
yield plugin yield plugin
self._pm.unregister(plugin) self._pm.unregister(plugin)
def stop_ongoing(self): def stop_ongoing(self):
""" Stop ongoing securejoin, configuration or other core jobs. """ """Stop ongoing securejoin, configuration or other core jobs."""
lib.dc_stop_ongoing_process(self._dc_context) lib.dc_stop_ongoing_process(self._dc_context)
def get_connectivity(self): def get_connectivity(self):
@@ -621,7 +614,7 @@ class Account(object):
return lib.dc_all_work_done(self._dc_context) return lib.dc_all_work_done(self._dc_context)
def start_io(self): def start_io(self):
""" start this account's IO scheduling (Rust-core async scheduler) """start this account's IO scheduling (Rust-core async scheduler)
If this account is not configured an Exception is raised. If this account is not configured an Exception is raised.
You need to call account.configure() and account.wait_configure_finish() You need to call account.configure() and account.wait_configure_finish()
@@ -665,7 +658,7 @@ class Account(object):
lib.dc_maybe_network(self._dc_context) lib.dc_maybe_network(self._dc_context)
def configure(self, reconfigure: bool = False) -> ConfigureTracker: def configure(self, reconfigure: bool = False) -> ConfigureTracker:
""" Start configuration process and return a Configtracker instance """Start configuration process and return a Configtracker instance
on which you can block with wait_finish() to get a True/False success on which you can block with wait_finish() to get a True/False success
value for the configuration process. value for the configuration process.
""" """
@@ -678,11 +671,11 @@ class Account(object):
return configtracker return configtracker
def wait_shutdown(self) -> None: def wait_shutdown(self) -> None:
""" wait until shutdown of this account has completed. """ """wait until shutdown of this account has completed."""
self._shutdown_event.wait() self._shutdown_event.wait()
def stop_io(self) -> None: def stop_io(self) -> None:
""" stop core IO scheduler if it is running. """ """stop core IO scheduler if it is running."""
self.log("stop_ongoing") self.log("stop_ongoing")
self.stop_ongoing() self.stop_ongoing()
@@ -690,7 +683,7 @@ class Account(object):
lib.dc_stop_io(self._dc_context) lib.dc_stop_io(self._dc_context)
def shutdown(self) -> None: def shutdown(self) -> None:
""" shutdown and destroy account (stop callback thread, close and remove """shutdown and destroy account (stop callback thread, close and remove
underlying dc_context).""" underlying dc_context)."""
if self._dc_context is None: if self._dc_context is None:
return return

View File

@@ -1,32 +1,38 @@
""" Chat and Location related API. """ """ Chat and Location related API. """
import mimetypes
import calendar import calendar
import json import json
from datetime import datetime, timezone import mimetypes
import os import os
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer, iter_array from datetime import datetime, timezone
from .capi import lib, ffi
from . import const
from .message import Message
from typing import Optional from typing import Optional
from . import const
from .capi import ffi, lib
from .cutil import (
as_dc_charpointer,
from_dc_charpointer,
from_optional_dc_charpointer,
iter_array,
)
from .message import Message
class Chat(object): class Chat(object):
""" Chat object which manages members and through which you can send and retrieve messages. """Chat object which manages members and through which you can send and retrieve messages.
You obtain instances of it through :class:`deltachat.account.Account`. You obtain instances of it through :class:`deltachat.account.Account`.
""" """
def __init__(self, account, id) -> None: def __init__(self, account, id) -> None:
from .account import Account from .account import Account
assert isinstance(account, Account), repr(account) assert isinstance(account, Account), repr(account)
self.account = account self.account = account
self.id = id self.id = id
def __eq__(self, other) -> bool: def __eq__(self, other) -> bool:
return self.id == getattr(other, "id", None) and \ return self.id == getattr(other, "id", None) and self.account._dc_context == other.account._dc_context
self.account._dc_context == other.account._dc_context
def __ne__(self, other) -> bool: def __ne__(self, other) -> bool:
return not (self == other) return not (self == other)
@@ -36,10 +42,7 @@ class Chat(object):
@property @property
def _dc_chat(self): def _dc_chat(self):
return ffi.gc( return ffi.gc(lib.dc_get_chat(self.account._dc_context, self.id), lib.dc_chat_unref)
lib.dc_get_chat(self.account._dc_context, self.id),
lib.dc_chat_unref
)
def delete(self) -> None: def delete(self) -> None:
"""Delete this chat and all its messages. """Delete this chat and all its messages.
@@ -62,28 +65,28 @@ class Chat(object):
# ------ chat status/metadata API ------------------------------ # ------ chat status/metadata API ------------------------------
def is_group(self) -> bool: def is_group(self) -> bool:
""" return true if this chat is a group chat. """return true if this chat is a group chat.
:returns: True if chat is a group-chat, false otherwise :returns: True if chat is a group-chat, false otherwise
""" """
return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP return lib.dc_chat_get_type(self._dc_chat) == const.DC_CHAT_TYPE_GROUP
def is_muted(self) -> bool: def is_muted(self) -> bool:
""" return true if this chat is muted. """return true if this chat is muted.
:returns: True if chat is muted, False otherwise. :returns: True if chat is muted, False otherwise.
""" """
return lib.dc_chat_is_muted(self._dc_chat) return lib.dc_chat_is_muted(self._dc_chat)
def is_contact_request(self): def is_contact_request(self):
""" return True if this chat is a contact request chat. """return True if this chat is a contact request chat.
:returns: True if chat is a contact request chat, False otherwise. :returns: True if chat is a contact request chat, False otherwise.
""" """
return lib.dc_chat_is_contact_request(self._dc_chat) return lib.dc_chat_is_contact_request(self._dc_chat)
def is_promoted(self): def is_promoted(self):
""" return True if this chat is promoted, i.e. """return True if this chat is promoted, i.e.
the member contacts are aware of their membership, the member contacts are aware of their membership,
have been sent messages. have been sent messages.
@@ -100,21 +103,21 @@ class Chat(object):
return lib.dc_chat_can_send(self._dc_chat) return lib.dc_chat_can_send(self._dc_chat)
def is_protected(self) -> bool: def is_protected(self) -> bool:
""" return True if this chat is a protected chat. """return True if this chat is a protected chat.
:returns: True if chat is protected, False otherwise. :returns: True if chat is protected, False otherwise.
""" """
return lib.dc_chat_is_protected(self._dc_chat) return lib.dc_chat_is_protected(self._dc_chat)
def get_name(self) -> Optional[str]: def get_name(self) -> Optional[str]:
""" return name of this chat. """return name of this chat.
:returns: unicode name :returns: unicode name
""" """
return from_dc_charpointer(lib.dc_chat_get_name(self._dc_chat)) return from_dc_charpointer(lib.dc_chat_get_name(self._dc_chat))
def set_name(self, name: str) -> bool: def set_name(self, name: str) -> bool:
""" set name of this chat. """set name of this chat.
:param name: as a unicode string. :param name: as a unicode string.
:returns: True on success, False otherwise :returns: True on success, False otherwise
@@ -123,7 +126,7 @@ class Chat(object):
return bool(lib.dc_set_chat_name(self.account._dc_context, self.id, name)) return bool(lib.dc_set_chat_name(self.account._dc_context, self.id, name))
def mute(self, duration: Optional[int] = None) -> None: def mute(self, duration: Optional[int] = None) -> None:
""" mutes the chat """mutes the chat
:param duration: Number of seconds to mute the chat for. None to mute until unmuted again. :param duration: Number of seconds to mute the chat for. None to mute until unmuted again.
:returns: None :returns: None
@@ -137,7 +140,7 @@ class Chat(object):
raise ValueError("Call to dc_set_chat_mute_duration failed") raise ValueError("Call to dc_set_chat_mute_duration failed")
def unmute(self) -> None: def unmute(self) -> None:
""" unmutes the chat """unmutes the chat
:returns: None :returns: None
""" """
@@ -146,7 +149,7 @@ class Chat(object):
raise ValueError("Failed to unmute chat") raise ValueError("Failed to unmute chat")
def get_mute_duration(self) -> int: def get_mute_duration(self) -> int:
""" Returns the number of seconds until the mute of this chat is lifted. """Returns the number of seconds until the mute of this chat is lifted.
:param duration: :param duration:
:returns: Returns the number of seconds the chat is still muted for. (0 for not muted, -1 forever muted) :returns: Returns the number of seconds the chat is still muted for. (0 for not muted, -1 forever muted)
@@ -154,14 +157,14 @@ class Chat(object):
return lib.dc_chat_get_remaining_mute_duration(self._dc_chat) return lib.dc_chat_get_remaining_mute_duration(self._dc_chat)
def get_ephemeral_timer(self) -> int: def get_ephemeral_timer(self) -> int:
""" get ephemeral timer. """get ephemeral timer.
:returns: ephemeral timer value in seconds :returns: ephemeral timer value in seconds
""" """
return lib.dc_get_chat_ephemeral_timer(self.account._dc_context, self.id) return lib.dc_get_chat_ephemeral_timer(self.account._dc_context, self.id)
def set_ephemeral_timer(self, timer: int) -> bool: def set_ephemeral_timer(self, timer: int) -> bool:
""" set ephemeral timer. """set ephemeral timer.
:param: timer value in seconds :param: timer value in seconds
@@ -170,7 +173,7 @@ class Chat(object):
return bool(lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer)) return bool(lib.dc_set_chat_ephemeral_timer(self.account._dc_context, self.id, timer))
def get_type(self) -> int: def get_type(self) -> int:
""" (deprecated) return type of this chat. """(deprecated) return type of this chat.
:returns: one of const.DC_CHAT_TYPE_* :returns: one of const.DC_CHAT_TYPE_*
""" """
@@ -184,7 +187,7 @@ class Chat(object):
return from_dc_charpointer(res) return from_dc_charpointer(res)
def get_join_qr(self) -> Optional[str]: def get_join_qr(self) -> Optional[str]:
""" get/create Join-Group QR Code as ascii-string. """get/create Join-Group QR Code as ascii-string.
this string needs to be transferred to another DC account this string needs to be transferred to another DC account
in a second channel (typically used by mobiles with QRcode-show + scan UX) in a second channel (typically used by mobiles with QRcode-show + scan UX)
@@ -220,7 +223,7 @@ class Chat(object):
return msg return msg
def send_text(self, text): def send_text(self, text):
""" send a text message and return the resulting Message instance. """send a text message and return the resulting Message instance.
:param msg: unicode text :param msg: unicode text
:raises ValueError: if message can not be send/chat does not exist. :raises ValueError: if message can not be send/chat does not exist.
@@ -233,7 +236,7 @@ class Chat(object):
return Message.from_db(self.account, msg_id) return Message.from_db(self.account, msg_id)
def send_file(self, path, mime_type="application/octet-stream"): def send_file(self, path, mime_type="application/octet-stream"):
""" send a file and return the resulting Message instance. """send a file and return the resulting Message instance.
:param path: path to the file. :param path: path to the file.
:param mime_type: the mime-type of this file, defaults to application/octet-stream. :param mime_type: the mime-type of this file, defaults to application/octet-stream.
@@ -248,7 +251,7 @@ class Chat(object):
return Message.from_db(self.account, sent_id) return Message.from_db(self.account, sent_id)
def send_image(self, path): def send_image(self, path):
""" send an image message and return the resulting Message instance. """send an image message and return the resulting Message instance.
:param path: path to an image file. :param path: path to an image file.
:raises ValueError: if message can not be send/chat does not exist. :raises ValueError: if message can not be send/chat does not exist.
@@ -263,7 +266,7 @@ class Chat(object):
return Message.from_db(self.account, sent_id) return Message.from_db(self.account, sent_id)
def prepare_message(self, msg): def prepare_message(self, msg):
""" prepare a message for sending. """prepare a message for sending.
:param msg: the message to be prepared. :param msg: the message to be prepared.
:returns: a :class:`deltachat.message.Message` instance. :returns: a :class:`deltachat.message.Message` instance.
@@ -278,7 +281,7 @@ class Chat(object):
return msg return msg
def prepare_message_file(self, path, mime_type=None, view_type="file"): def prepare_message_file(self, path, mime_type=None, view_type="file"):
""" prepare a message for sending and return the resulting Message instance. """prepare a message for sending and return the resulting Message instance.
To actually send the message, call :meth:`send_prepared`. To actually send the message, call :meth:`send_prepared`.
The file must be inside the blob directory. The file must be inside the blob directory.
@@ -294,7 +297,7 @@ class Chat(object):
return self.prepare_message(msg) return self.prepare_message(msg)
def send_prepared(self, message): def send_prepared(self, message):
""" send a previously prepared message. """send a previously prepared message.
:param message: a :class:`Message` instance previously returned by :param message: a :class:`Message` instance previously returned by
:meth:`prepare_file`. :meth:`prepare_file`.
@@ -314,7 +317,7 @@ class Chat(object):
msg._dc_msg = Message.from_db(self.account, sent_id)._dc_msg msg._dc_msg = Message.from_db(self.account, sent_id)._dc_msg
def set_draft(self, message): def set_draft(self, message):
""" set message as draft. """set message as draft.
:param message: a :class:`Message` instance :param message: a :class:`Message` instance
:returns: None :returns: None
@@ -325,7 +328,7 @@ class Chat(object):
lib.dc_set_draft(self.account._dc_context, self.id, message._dc_msg) lib.dc_set_draft(self.account._dc_context, self.id, message._dc_msg)
def get_draft(self): def get_draft(self):
""" get draft message for this chat. """get draft message for this chat.
:param message: a :class:`Message` instance :param message: a :class:`Message` instance
:returns: Message object or None (if no draft available) :returns: Message object or None (if no draft available)
@@ -337,32 +340,32 @@ class Chat(object):
return Message(self.account, dc_msg) return Message(self.account, dc_msg)
def get_messages(self): def get_messages(self):
""" return list of messages in this chat. """return list of messages in this chat.
:returns: list of :class:`deltachat.message.Message` objects for this chat. :returns: list of :class:`deltachat.message.Message` objects for this chat.
""" """
dc_array = ffi.gc( dc_array = ffi.gc(
lib.dc_get_chat_msgs(self.account._dc_context, self.id, 0, 0), lib.dc_get_chat_msgs(self.account._dc_context, self.id, 0, 0),
lib.dc_array_unref lib.dc_array_unref,
) )
return list(iter_array(dc_array, lambda x: Message.from_db(self.account, x))) return list(iter_array(dc_array, lambda x: Message.from_db(self.account, x)))
def count_fresh_messages(self): def count_fresh_messages(self):
""" return number of fresh messages in this chat. """return number of fresh messages in this chat.
:returns: number of fresh messages :returns: number of fresh messages
""" """
return lib.dc_get_fresh_msg_cnt(self.account._dc_context, self.id) return lib.dc_get_fresh_msg_cnt(self.account._dc_context, self.id)
def mark_noticed(self): def mark_noticed(self):
""" mark all messages in this chat as noticed. """mark all messages in this chat as noticed.
Noticed messages are no longer fresh. Noticed messages are no longer fresh.
""" """
return lib.dc_marknoticed_chat(self.account._dc_context, self.id) return lib.dc_marknoticed_chat(self.account._dc_context, self.id)
def get_summary(self): def get_summary(self):
""" return dictionary with summary information. """ """return dictionary with summary information."""
dc_res = lib.dc_chat_get_info_json(self.account._dc_context, self.id) dc_res = lib.dc_chat_get_info_json(self.account._dc_context, self.id)
s = from_dc_charpointer(dc_res) s = from_dc_charpointer(dc_res)
return json.loads(s) return json.loads(s)
@@ -370,7 +373,7 @@ class Chat(object):
# ------ group management API ------------------------------ # ------ group management API ------------------------------
def add_contact(self, obj): def add_contact(self, obj):
""" add a contact to this chat. """add a contact to this chat.
:params obj: Contact, Account or e-mail address. :params obj: Contact, Account or e-mail address.
:raises ValueError: if contact could not be added :raises ValueError: if contact could not be added
@@ -383,7 +386,7 @@ class Chat(object):
return contact return contact
def remove_contact(self, obj): def remove_contact(self, obj):
""" remove a contact from this chat. """remove a contact from this chat.
:params obj: Contact, Account or e-mail address. :params obj: Contact, Account or e-mail address.
:raises ValueError: if contact could not be removed :raises ValueError: if contact could not be removed
@@ -395,23 +398,22 @@ class Chat(object):
raise ValueError("could not remove contact {!r} from chat".format(contact)) raise ValueError("could not remove contact {!r} from chat".format(contact))
def get_contacts(self): def get_contacts(self):
""" get all contacts for this chat. """get all contacts for this chat.
:returns: list of :class:`deltachat.contact.Contact` objects for this chat :returns: list of :class:`deltachat.contact.Contact` objects for this chat
""" """
from .contact import Contact from .contact import Contact
dc_array = ffi.gc( dc_array = ffi.gc(
lib.dc_get_chat_contacts(self.account._dc_context, self.id), lib.dc_get_chat_contacts(self.account._dc_context, self.id),
lib.dc_array_unref lib.dc_array_unref,
)
return list(iter_array(
dc_array, lambda id: Contact(self.account, id))
) )
return list(iter_array(dc_array, lambda id: Contact(self.account, id)))
def num_contacts(self): def num_contacts(self):
""" return number of contacts in this chat. """ """return number of contacts in this chat."""
dc_array = ffi.gc( dc_array = ffi.gc(
lib.dc_get_chat_contacts(self.account._dc_context, self.id), lib.dc_get_chat_contacts(self.account._dc_context, self.id),
lib.dc_array_unref lib.dc_array_unref,
) )
return lib.dc_array_get_cnt(dc_array) return lib.dc_array_get_cnt(dc_array)
@@ -513,10 +515,7 @@ class Chat(object):
latitude=lib.dc_array_get_latitude(dc_array, i), latitude=lib.dc_array_get_latitude(dc_array, i),
longitude=lib.dc_array_get_longitude(dc_array, i), longitude=lib.dc_array_get_longitude(dc_array, i),
accuracy=lib.dc_array_get_accuracy(dc_array, i), accuracy=lib.dc_array_get_accuracy(dc_array, i),
timestamp=datetime.fromtimestamp( timestamp=datetime.fromtimestamp(lib.dc_array_get_timestamp(dc_array, i), timezone.utc),
lib.dc_array_get_timestamp(dc_array, i),
timezone.utc
),
marker=from_optional_dc_charpointer(lib.dc_array_get_marker(dc_array, i)), marker=from_optional_dc_charpointer(lib.dc_array_get_marker(dc_array, i)),
) )
for i in range(lib.dc_array_get_cnt(dc_array)) for i in range(lib.dc_array_get_cnt(dc_array))

View File

@@ -10,12 +10,14 @@ from .cutil import from_dc_charpointer, from_optional_dc_charpointer
class Contact(object): class Contact(object):
""" Delta-Chat Contact. """Delta-Chat Contact.
You obtain instances of it through :class:`deltachat.account.Account`. You obtain instances of it through :class:`deltachat.account.Account`.
""" """
def __init__(self, account, id): def __init__(self, account, id):
from .account import Account from .account import Account
assert isinstance(account, Account), repr(account) assert isinstance(account, Account), repr(account)
self.account = account self.account = account
self.id = id self.id = id
@@ -31,19 +33,16 @@ class Contact(object):
@property @property
def _dc_contact(self): def _dc_contact(self):
return ffi.gc( return ffi.gc(lib.dc_get_contact(self.account._dc_context, self.id), lib.dc_contact_unref)
lib.dc_get_contact(self.account._dc_context, self.id),
lib.dc_contact_unref
)
@props.with_doc @props.with_doc
def addr(self) -> str: def addr(self) -> str:
""" normalized e-mail address for this account. """ """normalized e-mail address for this account."""
return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact)) return from_dc_charpointer(lib.dc_contact_get_addr(self._dc_contact))
@props.with_doc @props.with_doc
def name(self) -> str: def name(self) -> str:
""" display name for this contact. """ """display name for this contact."""
return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact)) return from_dc_charpointer(lib.dc_contact_get_display_name(self._dc_contact))
# deprecated alias # deprecated alias
@@ -52,28 +51,26 @@ class Contact(object):
@props.with_doc @props.with_doc
def last_seen(self) -> date: def last_seen(self) -> date:
"""Last seen timestamp.""" """Last seen timestamp."""
return datetime.fromtimestamp( return datetime.fromtimestamp(lib.dc_contact_get_last_seen(self._dc_contact), timezone.utc)
lib.dc_contact_get_last_seen(self._dc_contact), timezone.utc
)
def is_blocked(self): def is_blocked(self):
""" Return True if the contact is blocked. """ """Return True if the contact is blocked."""
return lib.dc_contact_is_blocked(self._dc_contact) return lib.dc_contact_is_blocked(self._dc_contact)
def set_blocked(self, block=True): def set_blocked(self, block=True):
""" [Deprecated, use block/unblock methods] Block or unblock a contact. """ """[Deprecated, use block/unblock methods] Block or unblock a contact."""
return lib.dc_block_contact(self.account._dc_context, self.id, block) return lib.dc_block_contact(self.account._dc_context, self.id, block)
def block(self): def block(self):
""" Block this contact. Message will not be seen/retrieved from this contact. """ """Block this contact. Message will not be seen/retrieved from this contact."""
return lib.dc_block_contact(self.account._dc_context, self.id, True) return lib.dc_block_contact(self.account._dc_context, self.id, True)
def unblock(self): def unblock(self):
""" Unblock this contact. Messages from this contact will be retrieved (again).""" """Unblock this contact. Messages from this contact will be retrieved (again)."""
return lib.dc_block_contact(self.account._dc_context, self.id, False) return lib.dc_block_contact(self.account._dc_context, self.id, False)
def is_verified(self): def is_verified(self):
""" Return True if the contact is verified. """ """Return True if the contact is verified."""
return lib.dc_contact_is_verified(self._dc_contact) return lib.dc_contact_is_verified(self._dc_contact)
def get_profile_image(self) -> Optional[str]: def get_profile_image(self) -> Optional[str]:
@@ -93,7 +90,7 @@ class Contact(object):
return from_dc_charpointer(lib.dc_contact_get_status(self._dc_contact)) return from_dc_charpointer(lib.dc_contact_get_status(self._dc_contact))
def create_chat(self): def create_chat(self):
""" create or get an existing 1:1 chat object for the specified contact or contact id. """create or get an existing 1:1 chat object for the specified contact or contact id.
:param contact: chat_id (int) or contact object. :param contact: chat_id (int) or contact object.
:returns: a :class:`deltachat.chat.Chat` object. :returns: a :class:`deltachat.chat.Chat` object.

View File

@@ -1,9 +1,9 @@
from .capi import lib
from .capi import ffi
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional, TypeVar, Generator, Callable from typing import Callable, Generator, Optional, TypeVar
T = TypeVar('T') from .capi import ffi, lib
T = TypeVar("T")
def as_dc_charpointer(obj): def as_dc_charpointer(obj):

View File

@@ -3,18 +3,27 @@ Internal Python-level IMAP handling used by the testplugin
and for cleaning up inbox/mvbox for each test function run. and for cleaning up inbox/mvbox for each test function run.
""" """
import io
import ssl
import pathlib
from contextlib import contextmanager
from imap_tools import MailBox, MailBoxTls, errors, AND, Header, MailMessageFlags, MailMessage
import imaplib import imaplib
from deltachat import const, Account import io
import pathlib
import ssl
from contextlib import contextmanager
from typing import List from typing import List
from imap_tools import (
AND,
Header,
MailBox,
MailBoxTls,
MailMessage,
MailMessageFlags,
errors,
)
FLAGS = b'FLAGS' from deltachat import Account, const
FETCH = b'FETCH'
FLAGS = b"FLAGS"
FETCH = b"FETCH"
ALL = "1:*" ALL = "1:*"
@@ -69,8 +78,8 @@ class DirectImap:
return self.conn.folder.set(foldername) return self.conn.folder.set(foldername)
def select_config_folder(self, config_name: str): def select_config_folder(self, config_name: str):
""" Return info about selected folder if it is """Return info about selected folder if it is
configured, otherwise None. """ configured, otherwise None."""
if "_" not in config_name: if "_" not in config_name:
config_name = "configured_{}_folder".format(config_name) config_name = "configured_{}_folder".format(config_name)
foldername = self.account.get_config(config_name) foldername = self.account.get_config(config_name)
@@ -78,17 +87,17 @@ class DirectImap:
return self.select_folder(foldername) return self.select_folder(foldername)
def list_folders(self) -> List[str]: def list_folders(self) -> List[str]:
""" return list of all existing folder names""" """return list of all existing folder names"""
assert not self._idling assert not self._idling
return [folder.name for folder in self.conn.folder.list()] return [folder.name for folder in self.conn.folder.list()]
def delete(self, uid_list: str, expunge=True): def delete(self, uid_list: str, expunge=True):
""" delete a range of messages (imap-syntax). """delete a range of messages (imap-syntax).
If expunge is true, perform the expunge-operation If expunge is true, perform the expunge-operation
to make sure the messages are really gone and not to make sure the messages are really gone and not
just flagged as deleted. just flagged as deleted.
""" """
self.conn.client.uid('STORE', uid_list, '+FLAGS', r'(\Deleted)') self.conn.client.uid("STORE", uid_list, "+FLAGS", r"(\Deleted)")
if expunge: if expunge:
self.conn.expunge() self.conn.expunge()
@@ -141,7 +150,13 @@ class DirectImap:
fn = path.joinpath(str(msg.uid)) fn = path.joinpath(str(msg.uid))
fn.write_bytes(body) fn.write_bytes(body)
log("Message", msg.uid, fn) log("Message", msg.uid, fn)
log("Message", msg.uid, msg.flags, "Message-Id:", msg.obj.get("Message-Id")) log(
"Message",
msg.uid,
msg.flags,
"Message-Id:",
msg.obj.get("Message-Id"),
)
if empty_folders: if empty_folders:
log("--------- EMPTY FOLDERS:", empty_folders) log("--------- EMPTY FOLDERS:", empty_folders)
@@ -150,7 +165,7 @@ class DirectImap:
@contextmanager @contextmanager
def idle(self): def idle(self):
""" return Idle ContextManager. """ """return Idle ContextManager."""
idle_manager = IdleManager(self) idle_manager = IdleManager(self)
try: try:
yield idle_manager yield idle_manager
@@ -163,11 +178,11 @@ class DirectImap:
""" """
if msg.startswith("\n"): if msg.startswith("\n"):
msg = msg[1:] msg = msg[1:]
msg = '\n'.join([s.lstrip() for s in msg.splitlines()]) msg = "\n".join([s.lstrip() for s in msg.splitlines()])
self.conn.append(bytes(msg, encoding='ascii'), folder) self.conn.append(bytes(msg, encoding="ascii"), folder)
def get_uid_by_message_id(self, message_id) -> str: def get_uid_by_message_id(self, message_id) -> str:
msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header('MESSAGE-ID', message_id)))] msgs = [msg.uid for msg in self.conn.fetch(AND(header=Header("MESSAGE-ID", message_id)))]
if len(msgs) == 0: if len(msgs) == 0:
raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?") raise Exception("Did not find message " + message_id + ", maybe you forgot to select the correct folder?")
return msgs[0] return msgs[0]
@@ -183,7 +198,7 @@ class IdleManager:
self.direct_imap.conn.idle.start() self.direct_imap.conn.idle.start()
def check(self, timeout=None) -> List[bytes]: def check(self, timeout=None) -> List[bytes]:
""" (blocking) wait for next idle message from server. """ """(blocking) wait for next idle message from server."""
self.log("imap-direct: calling idle_check") self.log("imap-direct: calling idle_check")
res = self.direct_imap.conn.idle.poll(timeout=timeout) res = self.direct_imap.conn.idle.poll(timeout=timeout)
self.log("imap-direct: idle_check returned {!r}".format(res)) self.log("imap-direct: idle_check returned {!r}".format(res))
@@ -192,20 +207,19 @@ class IdleManager:
def wait_for_new_message(self, timeout=None) -> bytes: def wait_for_new_message(self, timeout=None) -> bytes:
while 1: while 1:
for item in self.check(timeout=timeout): for item in self.check(timeout=timeout):
if b'EXISTS' in item or b'RECENT' in item: if b"EXISTS" in item or b"RECENT" in item:
return item return item
def wait_for_seen(self, timeout=None) -> int: def wait_for_seen(self, timeout=None) -> int:
""" Return first message with SEEN flag from a running idle-stream. """Return first message with SEEN flag from a running idle-stream."""
"""
while 1: while 1:
for item in self.check(timeout=timeout): for item in self.check(timeout=timeout):
if FETCH in item: if FETCH in item:
self.log(str(item)) self.log(str(item))
if FLAGS in item and rb'\Seen' in item: if FLAGS in item and rb"\Seen" in item:
return int(item.split(b' ')[1]) return int(item.split(b" ")[1])
def done(self): def done(self):
""" send idle-done to server if we are currently in idle mode. """ """send idle-done to server if we are currently in idle mode."""
res = self.direct_imap.conn.idle.stop() res = self.direct_imap.conn.idle.stop()
return res return res

View File

@@ -1,18 +1,19 @@
import threading
import sys
import traceback
import time
import io import io
import re
import os import os
from queue import Queue, Empty import re
import sys
import threading
import time
import traceback
from contextlib import contextmanager
from queue import Empty, Queue
import deltachat import deltachat
from .hookspec import account_hookimpl
from contextlib import contextmanager
from .capi import ffi, lib from .capi import ffi, lib
from .message import map_system_message
from .cutil import from_optional_dc_charpointer from .cutil import from_optional_dc_charpointer
from .hookspec import account_hookimpl
from .message import map_system_message
class FFIEvent: class FFIEvent:
@@ -26,9 +27,10 @@ class FFIEvent:
class FFIEventLogger: class FFIEventLogger:
""" If you register an instance of this logger with an Account """If you register an instance of this logger with an Account
you'll get all ffi-events printed. you'll get all ffi-events printed.
""" """
# to prevent garbled logging # to prevent garbled logging
_loglock = threading.RLock() _loglock = threading.RLock()
@@ -56,9 +58,9 @@ class FFIEventLogger:
s = "{:2.2f} [{}] {}".format(elapsed, locname, message) s = "{:2.2f} [{}] {}".format(elapsed, locname, message)
if os.name == "posix": if os.name == "posix":
WARN = '\033[93m' WARN = "\033[93m"
ERROR = '\033[91m' ERROR = "\033[91m"
ENDC = '\033[0m' ENDC = "\033[0m"
if message.startswith("DC_EVENT_WARNING"): if message.startswith("DC_EVENT_WARNING"):
s = WARN + s + ENDC s = WARN + s + ENDC
if message.startswith("DC_EVENT_ERROR"): if message.startswith("DC_EVENT_ERROR"):
@@ -171,12 +173,12 @@ class FFIEventTracker:
self.get_info_contains("INBOX: Idle entering") self.get_info_contains("INBOX: Idle entering")
def wait_next_incoming_message(self): def wait_next_incoming_message(self):
""" wait for and return next incoming message. """ """wait for and return next incoming message."""
ev = self.get_matching("DC_EVENT_INCOMING_MSG") ev = self.get_matching("DC_EVENT_INCOMING_MSG")
return self.account.get_message_by_id(ev.data2) return self.account.get_message_by_id(ev.data2)
def wait_next_messages_changed(self): def wait_next_messages_changed(self):
""" wait for and return next message-changed message or None """wait for and return next message-changed message or None
if the event contains no msgid""" if the event contains no msgid"""
ev = self.get_matching("DC_EVENT_MSGS_CHANGED") ev = self.get_matching("DC_EVENT_MSGS_CHANGED")
if ev.data2 > 0: if ev.data2 > 0:
@@ -191,10 +193,11 @@ class FFIEventTracker:
class EventThread(threading.Thread): class EventThread(threading.Thread):
""" Event Thread for an account. """Event Thread for an account.
With each Account init this callback thread is started. With each Account init this callback thread is started.
""" """
def __init__(self, account) -> None: def __init__(self, account) -> None:
self.account = account self.account = account
super(EventThread, self).__init__(name="events") super(EventThread, self).__init__(name="events")
@@ -219,7 +222,7 @@ class EventThread(threading.Thread):
self.join(timeout=timeout) self.join(timeout=timeout)
def run(self) -> None: def run(self) -> None:
""" get and run events until shutdown. """ """get and run events until shutdown."""
with self.log_execution("EVENT THREAD"): with self.log_execution("EVENT THREAD"):
self._inner_run() self._inner_run()
@@ -259,8 +262,7 @@ class EventThread(threading.Thread):
except Exception as ex: except Exception as ex:
logfile = io.StringIO() logfile = io.StringIO()
traceback.print_exception(*sys.exc_info(), file=logfile) traceback.print_exception(*sys.exc_info(), file=logfile)
self.account.log("{}\nException {}\nTraceback:\n{}" self.account.log("{}\nException {}\nTraceback:\n{}".format(info, ex, logfile.getvalue()))
.format(info, ex, logfile.getvalue()))
def _map_ffi_event(self, ffi_event: FFIEvent): def _map_ffi_event(self, ffi_event: FFIEvent):
name = ffi_event.name name = ffi_event.name
@@ -282,7 +284,10 @@ class EventThread(threading.Thread):
yield res yield res
yield "ac_outgoing_message", dict(message=msg) yield "ac_outgoing_message", dict(message=msg)
elif msg.is_in_fresh(): elif msg.is_in_fresh():
yield map_system_message(msg) or ("ac_incoming_message", dict(message=msg)) yield map_system_message(msg) or (
"ac_incoming_message",
dict(message=msg),
)
elif name == "DC_EVENT_MSG_DELIVERED": elif name == "DC_EVENT_MSG_DELIVERED":
msg = account.get_message_by_id(ffi_event.data2) msg = account.get_message_by_id(ffi_event.data2)
yield "ac_message_delivered", dict(message=msg) yield "ac_message_delivered", dict(message=msg)

View File

@@ -2,7 +2,6 @@
import pluggy import pluggy
account_spec_name = "deltachat-account" account_spec_name = "deltachat-account"
account_hookspec = pluggy.HookspecMarker(account_spec_name) account_hookspec = pluggy.HookspecMarker(account_spec_name)
account_hookimpl = pluggy.HookimplMarker(account_spec_name) account_hookimpl = pluggy.HookimplMarker(account_spec_name)
@@ -13,12 +12,13 @@ global_hookimpl = pluggy.HookimplMarker(global_spec_name)
class PerAccount: class PerAccount:
""" per-Account-instance hook specifications. """per-Account-instance hook specifications.
All hooks are executed in a dedicated Event thread. All hooks are executed in a dedicated Event thread.
Hooks are generally not allowed to block/last long as this Hooks are generally not allowed to block/last long as this
blocks overall event processing on the python side. blocks overall event processing on the python side.
""" """
@classmethod @classmethod
def _make_plugin_manager(cls): def _make_plugin_manager(cls):
pm = pluggy.PluginManager(account_spec_name) pm = pluggy.PluginManager(account_spec_name)
@@ -27,7 +27,7 @@ class PerAccount:
@account_hookspec @account_hookspec
def ac_process_ffi_event(self, ffi_event): def ac_process_ffi_event(self, ffi_event):
""" process a CFFI low level events for a given account. """process a CFFI low level events for a given account.
ffi_event has "name", "data1", "data2" values as specified ffi_event has "name", "data1", "data2" values as specified
with `DC_EVENT_* <https://c.delta.chat/group__DC__EVENT.html>`_. with `DC_EVENT_* <https://c.delta.chat/group__DC__EVENT.html>`_.
@@ -35,37 +35,37 @@ class PerAccount:
@account_hookspec @account_hookspec
def ac_log_line(self, message): def ac_log_line(self, message):
""" log a message related to the account. """ """log a message related to the account."""
@account_hookspec @account_hookspec
def ac_configure_completed(self, success): def ac_configure_completed(self, success):
""" Called after a configure process completed. """ """Called after a configure process completed."""
@account_hookspec @account_hookspec
def ac_incoming_message(self, message): def ac_incoming_message(self, message):
""" Called on any incoming message (both existing chats and contact requests). """ """Called on any incoming message (both existing chats and contact requests)."""
@account_hookspec @account_hookspec
def ac_outgoing_message(self, message): def ac_outgoing_message(self, message):
""" Called on each outgoing message (both system and "normal").""" """Called on each outgoing message (both system and "normal")."""
@account_hookspec @account_hookspec
def ac_message_delivered(self, message): def ac_message_delivered(self, message):
""" Called when an outgoing message has been delivered to SMTP. """Called when an outgoing message has been delivered to SMTP.
:param message: Message that was just delivered. :param message: Message that was just delivered.
""" """
@account_hookspec @account_hookspec
def ac_chat_modified(self, chat): def ac_chat_modified(self, chat):
""" Chat was created or modified regarding membership, avatar, title. """Chat was created or modified regarding membership, avatar, title.
:param chat: Chat which was modified. :param chat: Chat which was modified.
""" """
@account_hookspec @account_hookspec
def ac_member_added(self, chat, contact, actor, message): def ac_member_added(self, chat, contact, actor, message):
""" Called for each contact added to an accepted chat. """Called for each contact added to an accepted chat.
:param chat: Chat where contact was added. :param chat: Chat where contact was added.
:param contact: Contact that was added. :param contact: Contact that was added.
@@ -75,7 +75,7 @@ class PerAccount:
@account_hookspec @account_hookspec
def ac_member_removed(self, chat, contact, actor, message): def ac_member_removed(self, chat, contact, actor, message):
""" Called for each contact removed from a chat. """Called for each contact removed from a chat.
:param chat: Chat where contact was removed. :param chat: Chat where contact was removed.
:param contact: Contact that was removed. :param contact: Contact that was removed.
@@ -85,10 +85,11 @@ class PerAccount:
class Global: class Global:
""" global hook specifications using a per-process singleton """global hook specifications using a per-process singleton
plugin manager instance. plugin manager instance.
""" """
_plugin_manager = None _plugin_manager = None
@classmethod @classmethod
@@ -100,11 +101,11 @@ class Global:
@global_hookspec @global_hookspec
def dc_account_init(self, account): def dc_account_init(self, account):
""" called when `Account::__init__()` function starts executing. """ """called when `Account::__init__()` function starts executing."""
@global_hookspec @global_hookspec
def dc_account_extra_configure(self, account): def dc_account_extra_configure(self, account):
""" Called when account configuration successfully finished. """Called when account configuration successfully finished.
This hook can be used to perform extra work before This hook can be used to perform extra work before
ac_configure_completed is called. ac_configure_completed is called.
@@ -112,4 +113,4 @@ class Global:
@global_hookspec @global_hookspec
def dc_account_after_shutdown(self, account): def dc_account_after_shutdown(self, account):
""" Called after the account has been shutdown. """ """Called after the account has been shutdown."""

View File

@@ -2,20 +2,21 @@
import os import os
import re import re
from . import props
from .cutil import from_dc_charpointer, from_optional_dc_charpointer, as_dc_charpointer
from .capi import lib, ffi
from . import const
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional from typing import Optional
from . import const, props
from .capi import ffi, lib
from .cutil import as_dc_charpointer, from_dc_charpointer, from_optional_dc_charpointer
class Message(object): class Message(object):
""" Message object. """Message object.
You obtain instances of it through :class:`deltachat.account.Account` or You obtain instances of it through :class:`deltachat.account.Account` or
:class:`deltachat.chat.Chat`. :class:`deltachat.chat.Chat`.
""" """
def __init__(self, account, dc_msg): def __init__(self, account, dc_msg):
self.account = account self.account = account
assert isinstance(self.account._dc_context, ffi.CData) assert isinstance(self.account._dc_context, ffi.CData)
@@ -32,20 +33,24 @@ class Message(object):
c = self.get_sender_contact() c = self.get_sender_contact()
typ = "outgoing" if self.is_outgoing() else "incoming" typ = "outgoing" if self.is_outgoing() else "incoming"
return "<Message {} sys={} {} id={} sender={}/{} chat={}/{}>".format( return "<Message {} sys={} {} id={} sender={}/{} chat={}/{}>".format(
typ, self.is_system_message(), repr(self.text[:10]), typ,
self.id, c.id, c.addr, self.chat.id, self.chat.get_name()) self.is_system_message(),
repr(self.text[:10]),
self.id,
c.id,
c.addr,
self.chat.id,
self.chat.get_name(),
)
@classmethod @classmethod
def from_db(cls, account, id): def from_db(cls, account, id):
assert id > 0 assert id > 0
return cls(account, ffi.gc( return cls(account, ffi.gc(lib.dc_get_msg(account._dc_context, id), lib.dc_msg_unref))
lib.dc_get_msg(account._dc_context, id),
lib.dc_msg_unref
))
@classmethod @classmethod
def new_empty(cls, account, view_type): def new_empty(cls, account, view_type):
""" create a non-persistent message. """create a non-persistent message.
:param: view_type is the message type code or one of the strings: :param: view_type is the message type code or one of the strings:
"text", "audio", "video", "file", "sticker" "text", "audio", "video", "file", "sticker"
@@ -54,13 +59,13 @@ class Message(object):
view_type_code = view_type view_type_code = view_type
else: else:
view_type_code = get_viewtype_code_from_name(view_type) view_type_code = get_viewtype_code_from_name(view_type)
return Message(account, ffi.gc( return Message(
lib.dc_msg_new(account._dc_context, view_type_code), account,
lib.dc_msg_unref ffi.gc(lib.dc_msg_new(account._dc_context, view_type_code), lib.dc_msg_unref),
)) )
def create_chat(self): def create_chat(self):
""" create or get an existing chat (group) object for this message. """create or get an existing chat (group) object for this message.
If the message is a contact request If the message is a contact request
the sender will become an accepted contact. the sender will become an accepted contact.
@@ -72,23 +77,22 @@ class Message(object):
@props.with_doc @props.with_doc
def id(self): def id(self):
"""id of this message. """ """id of this message."""
return lib.dc_msg_get_id(self._dc_msg) return lib.dc_msg_get_id(self._dc_msg)
@props.with_doc @props.with_doc
def text(self) -> str: def text(self) -> str:
"""unicode text of this messages (might be empty if not a text message). """ """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)) return from_dc_charpointer(lib.dc_msg_get_text(self._dc_msg))
def set_text(self, text): def set_text(self, text):
"""set text of this message. """ """set text of this message."""
lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text)) lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text))
@props.with_doc @props.with_doc
def html(self) -> str: def html(self) -> str:
"""html text of this messages (might be empty if not an html message). """ """html text of this messages (might be empty if not an html message)."""
return from_optional_dc_charpointer( return from_optional_dc_charpointer(lib.dc_get_msg_html(self.account._dc_context, self.id)) or ""
lib.dc_get_msg_html(self.account._dc_context, self.id)) or ""
def has_html(self): def has_html(self):
"""return True if this message has an html part, False otherwise.""" """return True if this message has an html part, False otherwise."""
@@ -103,11 +107,11 @@ class Message(object):
@props.with_doc @props.with_doc
def filename(self): def filename(self):
"""filename if there was an attachment, otherwise empty string. """ """filename if there was an attachment, otherwise empty string."""
return from_dc_charpointer(lib.dc_msg_get_file(self._dc_msg)) return from_dc_charpointer(lib.dc_msg_get_file(self._dc_msg))
def set_file(self, path, mime_type=None): def set_file(self, path, mime_type=None):
"""set file for this message from path and mime_type. """ """set file for this message from path and mime_type."""
mtype = ffi.NULL if mime_type is None else as_dc_charpointer(mime_type) mtype = ffi.NULL if mime_type is None else as_dc_charpointer(mime_type)
if not os.path.exists(path): if not os.path.exists(path):
raise ValueError("path does not exist: {!r}".format(path)) raise ValueError("path does not exist: {!r}".format(path))
@@ -115,7 +119,7 @@ class Message(object):
@props.with_doc @props.with_doc
def basename(self) -> str: def basename(self) -> str:
"""basename of the attachment if it exists, otherwise empty string. """ """basename of the attachment if it exists, otherwise empty string."""
# FIXME, it does not return basename # FIXME, it does not return basename
return from_dc_charpointer(lib.dc_msg_get_filename(self._dc_msg)) return from_dc_charpointer(lib.dc_msg_get_filename(self._dc_msg))
@@ -125,43 +129,39 @@ class Message(object):
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg)) return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
def is_system_message(self): def is_system_message(self):
""" return True if this message is a system/info message. """ """return True if this message is a system/info message."""
return bool(lib.dc_msg_is_info(self._dc_msg)) return bool(lib.dc_msg_is_info(self._dc_msg))
def is_setup_message(self): def is_setup_message(self):
""" return True if this message is a setup message. """ """return True if this message is a setup message."""
return lib.dc_msg_is_setupmessage(self._dc_msg) return lib.dc_msg_is_setupmessage(self._dc_msg)
def get_setupcodebegin(self) -> str: def get_setupcodebegin(self) -> str:
""" return the first characters of a setup code in a setup message. """ """return the first characters of a setup code in a setup message."""
return from_dc_charpointer(lib.dc_msg_get_setupcodebegin(self._dc_msg)) return from_dc_charpointer(lib.dc_msg_get_setupcodebegin(self._dc_msg))
def is_encrypted(self): def is_encrypted(self):
""" return True if this message was encrypted. """ """return True if this message was encrypted."""
return bool(lib.dc_msg_get_showpadlock(self._dc_msg)) return bool(lib.dc_msg_get_showpadlock(self._dc_msg))
def is_bot(self): def is_bot(self):
""" return True if this message is submitted automatically. """ """return True if this message is submitted automatically."""
return bool(lib.dc_msg_is_bot(self._dc_msg)) return bool(lib.dc_msg_is_bot(self._dc_msg))
def is_forwarded(self): def is_forwarded(self):
""" return True if this message was forwarded. """ """return True if this message was forwarded."""
return bool(lib.dc_msg_is_forwarded(self._dc_msg)) return bool(lib.dc_msg_is_forwarded(self._dc_msg))
def get_message_info(self) -> str: def get_message_info(self) -> str:
""" Return informational text for a single message. """Return informational text for a single message.
The text is multiline and may contain eg. the raw text of the message. The text is multiline and may contain eg. the raw text of the message.
""" """
return from_dc_charpointer(lib.dc_get_msg_info(self.account._dc_context, self.id)) return from_dc_charpointer(lib.dc_get_msg_info(self.account._dc_context, self.id))
def continue_key_transfer(self, setup_code): def continue_key_transfer(self, setup_code):
""" extract key and use it as primary key for this account. """ """extract key and use it as primary key for this account."""
res = lib.dc_continue_key_transfer( res = lib.dc_continue_key_transfer(self.account._dc_context, self.id, as_dc_charpointer(setup_code))
self.account._dc_context,
self.id,
as_dc_charpointer(setup_code)
)
if res == 0: if res == 0:
raise ValueError("could not decrypt") raise ValueError("could not decrypt")
@@ -230,7 +230,7 @@ class Message(object):
lib.dc_msg_force_plaintext(self._dc_msg) lib.dc_msg_force_plaintext(self._dc_msg)
def get_mime_headers(self): def get_mime_headers(self):
""" return mime-header object for an incoming message. """return mime-header object for an incoming message.
This only returns a non-None object if ``save_mime_headers`` This only returns a non-None object if ``save_mime_headers``
config option was set and the message is incoming. config option was set and the message is incoming.
@@ -238,6 +238,7 @@ class Message(object):
:returns: email-mime message object (with headers only, no body). :returns: email-mime message object (with headers only, no body).
""" """
import email.parser import email.parser
mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id) mime_headers = lib.dc_get_mime_headers(self.account._dc_context, self.id)
if mime_headers: if mime_headers:
s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref)) s = ffi.string(ffi.gc(mime_headers, lib.dc_str_unref))
@@ -257,6 +258,7 @@ class Message(object):
:returns: :class:`deltachat.chat.Chat` object :returns: :class:`deltachat.chat.Chat` object
""" """
from .chat import Chat from .chat import Chat
chat_id = lib.dc_msg_get_chat_id(self._dc_msg) chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
return Chat(self.account, chat_id) return Chat(self.account, chat_id)
@@ -266,13 +268,11 @@ class Message(object):
Usually used to impersonate someone else. Usually used to impersonate someone else.
""" """
return from_optional_dc_charpointer( return from_optional_dc_charpointer(lib.dc_msg_get_override_sender_name(self._dc_msg))
lib.dc_msg_get_override_sender_name(self._dc_msg))
def set_override_sender_name(self, name): def set_override_sender_name(self, name):
"""set different sender name for a message. """ """set different sender name for a message."""
lib.dc_msg_set_override_sender_name( lib.dc_msg_set_override_sender_name(self._dc_msg, as_dc_charpointer(name))
self._dc_msg, as_dc_charpointer(name))
def get_sender_chat(self): def get_sender_chat(self):
"""return the 1:1 chat with the sender of this message. """return the 1:1 chat with the sender of this message.
@@ -287,6 +287,7 @@ class Message(object):
:returns: :class:`deltachat.chat.Contact` instance :returns: :class:`deltachat.chat.Contact` instance
""" """
from .contact import Contact from .contact import Contact
contact_id = lib.dc_msg_get_from_id(self._dc_msg) contact_id = lib.dc_msg_get_from_id(self._dc_msg)
return Contact(self.account, contact_id) return Contact(self.account, contact_id)
@@ -299,14 +300,11 @@ class Message(object):
dc_msg = self._dc_msg dc_msg = self._dc_msg
else: else:
# load message from db to get a fresh/current state # load message from db to get a fresh/current state
dc_msg = ffi.gc( dc_msg = ffi.gc(lib.dc_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref)
lib.dc_get_msg(self.account._dc_context, self.id),
lib.dc_msg_unref
)
return lib.dc_msg_get_state(dc_msg) return lib.dc_msg_get_state(dc_msg)
def is_in_fresh(self): def is_in_fresh(self):
""" return True if Message is incoming fresh message (un-noticed). """return True if Message is incoming fresh message (un-noticed).
Fresh messages are not noticed nor seen and are typically Fresh messages are not noticed nor seen and are typically
shown in notifications. shown in notifications.
@@ -330,25 +328,25 @@ class Message(object):
return self._msgstate == const.DC_STATE_IN_SEEN return self._msgstate == const.DC_STATE_IN_SEEN
def is_outgoing(self): def is_outgoing(self):
"""Return True if Message is outgoing. """ """Return True if Message is outgoing."""
return self._msgstate in ( return self._msgstate in (
const.DC_STATE_OUT_PREPARING, const.DC_STATE_OUT_PENDING, const.DC_STATE_OUT_PREPARING,
const.DC_STATE_OUT_FAILED, const.DC_STATE_OUT_MDN_RCVD, const.DC_STATE_OUT_PENDING,
const.DC_STATE_OUT_DELIVERED) const.DC_STATE_OUT_FAILED,
const.DC_STATE_OUT_MDN_RCVD,
const.DC_STATE_OUT_DELIVERED,
)
def is_out_preparing(self): def is_out_preparing(self):
"""Return True if Message is outgoing, but its file is being prepared. """Return True if Message is outgoing, but its file is being prepared."""
"""
return self._msgstate == const.DC_STATE_OUT_PREPARING return self._msgstate == const.DC_STATE_OUT_PREPARING
def is_out_pending(self): def is_out_pending(self):
"""Return True if Message is outgoing, but is pending (no single checkmark). """Return True if Message is outgoing, but is pending (no single checkmark)."""
"""
return self._msgstate == const.DC_STATE_OUT_PENDING return self._msgstate == const.DC_STATE_OUT_PENDING
def is_out_failed(self): def is_out_failed(self):
"""Return True if Message is unrecoverably failed. """Return True if Message is unrecoverably failed."""
"""
return self._msgstate == const.DC_STATE_OUT_FAILED return self._msgstate == const.DC_STATE_OUT_FAILED
def is_out_delivered(self): def is_out_delivered(self):
@@ -375,48 +373,48 @@ class Message(object):
return lib.dc_msg_get_viewtype(self._dc_msg) return lib.dc_msg_get_viewtype(self._dc_msg)
def is_text(self): def is_text(self):
""" return True if it's a text message. """ """return True if it's a text message."""
return self._view_type == const.DC_MSG_TEXT return self._view_type == const.DC_MSG_TEXT
def is_image(self): def is_image(self):
""" return True if it's an image message. """ """return True if it's an image message."""
return self._view_type == const.DC_MSG_IMAGE return self._view_type == const.DC_MSG_IMAGE
def is_gif(self): def is_gif(self):
""" return True if it's a gif message. """ """return True if it's a gif message."""
return self._view_type == const.DC_MSG_GIF return self._view_type == const.DC_MSG_GIF
def is_sticker(self): def is_sticker(self):
""" return True if it's a sticker message. """ """return True if it's a sticker message."""
return self._view_type == const.DC_MSG_STICKER return self._view_type == const.DC_MSG_STICKER
def is_audio(self): def is_audio(self):
""" return True if it's an audio message. """ """return True if it's an audio message."""
return self._view_type == const.DC_MSG_AUDIO return self._view_type == const.DC_MSG_AUDIO
def is_video(self): def is_video(self):
""" return True if it's a video message. """ """return True if it's a video message."""
return self._view_type == const.DC_MSG_VIDEO return self._view_type == const.DC_MSG_VIDEO
def is_file(self): def is_file(self):
""" return True if it's a file message. """ """return True if it's a file message."""
return self._view_type == const.DC_MSG_FILE return self._view_type == const.DC_MSG_FILE
def mark_seen(self): def mark_seen(self):
""" mark this message as seen. """ """mark this message as seen."""
self.account.mark_seen_messages([self.id]) self.account.mark_seen_messages([self.id])
# some code for handling DC_MSG_* view types # some code for handling DC_MSG_* view types
_view_type_mapping = { _view_type_mapping = {
'text': const.DC_MSG_TEXT, "text": const.DC_MSG_TEXT,
'image': const.DC_MSG_IMAGE, "image": const.DC_MSG_IMAGE,
'gif': const.DC_MSG_GIF, "gif": const.DC_MSG_GIF,
'audio': const.DC_MSG_AUDIO, "audio": const.DC_MSG_AUDIO,
'video': const.DC_MSG_VIDEO, "video": const.DC_MSG_VIDEO,
'file': const.DC_MSG_FILE, "file": const.DC_MSG_FILE,
'sticker': const.DC_MSG_STICKER, "sticker": const.DC_MSG_STICKER,
} }
@@ -424,14 +422,16 @@ def get_viewtype_code_from_name(view_type_name):
code = _view_type_mapping.get(view_type_name) code = _view_type_mapping.get(view_type_name)
if code is not None: if code is not None:
return code return code
raise ValueError("message typecode not found for {!r}, " raise ValueError(
"available {!r}".format(view_type_name, list(_view_type_mapping.keys()))) "message typecode not found for {!r}, " "available {!r}".format(view_type_name, list(_view_type_mapping.keys()))
)
# #
# some helper code for turning system messages into hook events # some helper code for turning system messages into hook events
# #
def map_system_message(msg): def map_system_message(msg):
if msg.is_system_message(): if msg.is_system_message():
res = parse_system_add_remove(msg.text) res = parse_system_add_remove(msg.text)
@@ -448,7 +448,7 @@ def map_system_message(msg):
def extract_addr(text): def extract_addr(text):
m = re.match(r'.*\((.+@.+)\)', text) m = re.match(r".*\((.+@.+)\)", text)
if m: if m:
text = m.group(1) text = m.group(1)
text = text.rstrip(".") text = text.rstrip(".")
@@ -456,9 +456,9 @@ def extract_addr(text):
def parse_system_add_remove(text): def parse_system_add_remove(text):
""" return add/remove info from parsing the given system message text. """return add/remove info from parsing the given system message text.
returns a (action, affected, actor) triple """ returns a (action, affected, actor) triple"""
# Member Me (x@y) removed by a@b. # Member Me (x@y) removed by a@b.
# Member x@y added by a@b # Member x@y added by a@b
@@ -467,7 +467,7 @@ def parse_system_add_remove(text):
# Group left by some one (tmp1@x.org). # Group left by some one (tmp1@x.org).
# Group left by tmp1@x.org. # Group left by tmp1@x.org.
text = text.lower() text = text.lower()
m = re.match(r'member (.+) (removed|added) by (.+)', text) m = re.match(r"member (.+) (removed|added) by (.+)", text)
if m: if m:
affected, action, actor = m.groups() affected, action, actor = m.groups()
return action, extract_addr(affected), extract_addr(actor) return action, extract_addr(affected), extract_addr(actor)

View File

@@ -9,6 +9,7 @@ def with_doc(f):
# https://github.com/devpi/devpi/blob/master/common/devpi_common/types.py # https://github.com/devpi/devpi/blob/master/common/devpi_common/types.py
def cached(f): def cached(f):
"""returns a cached property that is calculated by function f""" """returns a cached property that is calculated by function f"""
def get(self): def get(self):
try: try:
return self._property_cache[f] return self._property_cache[f]

View File

@@ -26,14 +26,12 @@ class Provider(object):
@property @property
def overview_page(self) -> str: def overview_page(self) -> str:
"""URL to the overview page of the provider on providers.delta.chat.""" """URL to the overview page of the provider on providers.delta.chat."""
return from_dc_charpointer( return from_dc_charpointer(lib.dc_provider_get_overview_page(self._provider))
lib.dc_provider_get_overview_page(self._provider))
@property @property
def get_before_login_hints(self) -> str: def get_before_login_hints(self) -> str:
"""Should be shown to the user on login.""" """Should be shown to the user on login."""
return from_dc_charpointer( return from_dc_charpointer(lib.dc_provider_get_before_login_hint(self._provider))
lib.dc_provider_get_before_login_hint(self._provider))
@property @property
def status(self) -> int: def status(self) -> int:

View File

@@ -1,56 +1,62 @@
from __future__ import print_function from __future__ import print_function
import os
import sys
import io
import subprocess
import queue
import threading
import fnmatch import fnmatch
import io
import os
import pathlib
import queue
import subprocess
import sys
import threading
import time import time
import weakref import weakref
from queue import Queue from queue import Queue
from typing import List, Callable from typing import Callable, List
import pytest import pytest
import requests import requests
import pathlib
from . import Account, const, account_hookimpl, get_core_info
from .events import FFIEventLogger, FFIEventTracker
from _pytest._code import Source from _pytest._code import Source
import deltachat import deltachat
from . import Account, account_hookimpl, const, get_core_info
from .events import FFIEventLogger, FFIEventTracker
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("deltachat testplugin options") group = parser.getgroup("deltachat testplugin options")
group.addoption( group.addoption(
"--liveconfig", action="store", default=None, "--liveconfig",
help="a file with >=2 lines where each line " action="store",
"contains NAME=VALUE config settings for one account" default=None,
help="a file with >=2 lines where each line " "contains NAME=VALUE config settings for one account",
) )
group.addoption( group.addoption(
"--ignored", action="store_true", "--ignored",
action="store_true",
help="Also run tests marked with the ignored marker", help="Also run tests marked with the ignored marker",
) )
group.addoption( group.addoption(
"--strict-tls", action="store_true", "--strict-tls",
action="store_true",
help="Never accept invalid TLS certificates for test accounts", help="Never accept invalid TLS certificates for test accounts",
) )
group.addoption( group.addoption(
"--extra-info", action="store_true", "--extra-info",
help="show more info on failures (imap server state, config)" action="store_true",
help="show more info on failures (imap server state, config)",
) )
group.addoption( group.addoption(
"--debug-setup", action="store_true", "--debug-setup",
help="show events during configure and start io phases of online accounts" action="store_true",
help="show events during configure and start io phases of online accounts",
) )
def pytest_configure(config): def pytest_configure(config):
cfg = config.getoption('--liveconfig') cfg = config.getoption("--liveconfig")
if not cfg: if not cfg:
cfg = os.getenv('DCC_NEW_TMP_EMAIL') cfg = os.getenv("DCC_NEW_TMP_EMAIL")
if cfg: if cfg:
config.option.liveconfig = cfg config.option.liveconfig = cfg
@@ -113,19 +119,21 @@ def pytest_configure(config):
def pytest_report_header(config, startdir): def pytest_report_header(config, startdir):
info = get_core_info() info = get_core_info()
summary = ['Deltachat core={} sqlite={} journal_mode={}'.format( summary = [
info['deltachat_core_version'], "Deltachat core={} sqlite={} journal_mode={}".format(
info['sqlite_version'], info["deltachat_core_version"],
info['journal_mode'], info["sqlite_version"],
)] info["journal_mode"],
)
]
cfg = config.option.liveconfig cfg = config.option.liveconfig
if cfg: if cfg:
if "?" in cfg: if "?" in cfg:
url, token = cfg.split("?", 1) url, token = cfg.split("?", 1)
summary.append('Liveconfig provider: {}?<token ommitted>'.format(url)) summary.append("Liveconfig provider: {}?<token ommitted>".format(url))
else: else:
summary.append('Liveconfig file: {}'.format(cfg)) summary.append("Liveconfig file: {}".format(cfg))
return summary return summary
@@ -135,15 +143,15 @@ def testprocess(request):
class TestProcess: class TestProcess:
""" A pytest session-scoped instance to help with managing "live" account configurations. """A pytest session-scoped instance to help with managing "live" account configurations."""
"""
def __init__(self, pytestconfig): def __init__(self, pytestconfig):
self.pytestconfig = pytestconfig self.pytestconfig = pytestconfig
self._addr2files = {} self._addr2files = {}
self._configlist = [] self._configlist = []
def get_liveconfig_producer(self): def get_liveconfig_producer(self):
""" provide live account configs, cached on a per-test-process scope """provide live account configs, cached on a per-test-process scope
so that test functions can re-use already known live configs. so that test functions can re-use already known live configs.
Depending on the --liveconfig option this comes from Depending on the --liveconfig option this comes from
a HTTP provider or a file with a line specifying each accounts config. a HTTP provider or a file with a line specifying each accounts config.
@@ -154,7 +162,7 @@ class TestProcess:
if not liveconfig_opt.startswith("http"): if not liveconfig_opt.startswith("http"):
for line in open(liveconfig_opt): for line in open(liveconfig_opt):
if line.strip() and not line.strip().startswith('#'): if line.strip() and not line.strip().startswith("#"):
d = {} d = {}
for part in line.split(): for part in line.split():
name, value = part.split("=") name, value = part.split("=")
@@ -170,8 +178,7 @@ class TestProcess:
except IndexError: except IndexError:
res = requests.post(liveconfig_opt) res = requests.post(liveconfig_opt)
if res.status_code != 200: if res.status_code != 200:
pytest.fail("newtmpuser count={} code={}: '{}'".format( pytest.fail("newtmpuser count={} code={}: '{}'".format(index, res.status_code, res.text))
index, res.status_code, res.text))
d = res.json() d = res.json()
config = dict(addr=d["email"], mail_pw=d["password"]) config = dict(addr=d["email"], mail_pw=d["password"])
print("newtmpuser {}: addr={}".format(index, config["addr"])) print("newtmpuser {}: addr={}".format(index, config["addr"]))
@@ -230,13 +237,16 @@ def data(request):
# because we are run from a dev-setup with pytest direct, # because we are run from a dev-setup with pytest direct,
# through tox, and then maybe also from deltachat-binding # through tox, and then maybe also from deltachat-binding
# users like "deltabot". # users like "deltabot".
self.paths = [os.path.normpath(x) for x in [ self.paths = [
os.path.join(os.path.dirname(request.fspath.strpath), "data"), os.path.normpath(x)
os.path.join(os.path.dirname(__file__), "..", "..", "..", "test-data") for x in [
]] os.path.join(os.path.dirname(request.fspath.strpath), "data"),
os.path.join(os.path.dirname(__file__), "..", "..", "..", "test-data"),
]
]
def get_path(self, bn): def get_path(self, bn):
""" return path of file or None if it doesn't exist. """ """return path of file or None if it doesn't exist."""
for path in self.paths: for path in self.paths:
fn = os.path.join(path, *bn.split("/")) fn = os.path.join(path, *bn.split("/"))
if os.path.exists(fn): if os.path.exists(fn):
@@ -253,10 +263,11 @@ def data(request):
class ACSetup: class ACSetup:
""" accounts setup helper to deal with multiple configure-process """accounts setup helper to deal with multiple configure-process
and io & imap initialization phases. From tests, use the higher level and io & imap initialization phases. From tests, use the higher level
public ACFactory methods instead of its private helper class. public ACFactory methods instead of its private helper class.
""" """
CONFIGURING = "CONFIGURING" CONFIGURING = "CONFIGURING"
CONFIGURED = "CONFIGURED" CONFIGURED = "CONFIGURED"
IDLEREADY = "IDLEREADY" IDLEREADY = "IDLEREADY"
@@ -272,13 +283,14 @@ class ACSetup:
print("[acsetup]", "{:.3f}".format(time.time() - self.init_time), *args) print("[acsetup]", "{:.3f}".format(time.time() - self.init_time), *args)
def add_configured(self, account): def add_configured(self, account):
""" add an already configured account. """ """add an already configured account."""
assert account.is_configured() assert account.is_configured()
self._account2state[account] = self.CONFIGURED self._account2state[account] = self.CONFIGURED
self.log("added already configured account", account, account.get_config("addr")) self.log("added already configured account", account, account.get_config("addr"))
def start_configure(self, account, reconfigure=False): def start_configure(self, account, reconfigure=False):
""" add an account and start its configure process. """ """add an account and start its configure process."""
class PendingTracker: class PendingTracker:
@account_hookimpl @account_hookimpl
def ac_configure_completed(this, success): def ac_configure_completed(this, success):
@@ -290,7 +302,7 @@ class ACSetup:
self.log("started configure on", account) self.log("started configure on", account)
def wait_one_configured(self, account): def wait_one_configured(self, account):
""" wait until this account has successfully configured. """ """wait until this account has successfully configured."""
if self._account2state[account] == self.CONFIGURING: if self._account2state[account] == self.CONFIGURING:
while 1: while 1:
acc = self._pop_config_success() acc = self._pop_config_success()
@@ -301,7 +313,7 @@ class ACSetup:
acc._evtracker.consume_events() acc._evtracker.consume_events()
def bring_online(self): def bring_online(self):
""" Wait for all accounts to become ready to receive messages. """Wait for all accounts to become ready to receive messages.
This will initialize logging, start IO and the direct_imap attribute This will initialize logging, start IO and the direct_imap attribute
for each account which either is CONFIGURED already or which is CONFIGURING for each account which either is CONFIGURED already or which is CONFIGURING
@@ -336,12 +348,12 @@ class ACSetup:
acc.log("inbox IDLE ready") acc.log("inbox IDLE ready")
def init_logging(self, acc): def init_logging(self, acc):
""" idempotent function for initializing logging (will replace existing logger). """ """idempotent function for initializing logging (will replace existing logger)."""
logger = FFIEventLogger(acc, logid=acc._logid, init_time=self.init_time) logger = FFIEventLogger(acc, logid=acc._logid, init_time=self.init_time)
acc.add_account_plugin(logger, name="logger-" + acc._logid) acc.add_account_plugin(logger, name="logger-" + acc._logid)
def init_imap(self, acc): def init_imap(self, acc):
""" initialize direct_imap and cleanup server state. """ """initialize direct_imap and cleanup server state."""
from deltachat.direct_imap import DirectImap from deltachat.direct_imap import DirectImap
assert acc.is_configured() assert acc.is_configured()
@@ -375,8 +387,7 @@ class ACFactory:
self._finalizers = [] self._finalizers = []
self._accounts = [] self._accounts = []
self._acsetup = ACSetup(testprocess, self.init_time) self._acsetup = ACSetup(testprocess, self.init_time)
self._preconfigured_keys = ["alice", "bob", "charlie", self._preconfigured_keys = ["alice", "bob", "charlie", "dom", "elena", "fiona"]
"dom", "elena", "fiona"]
self.set_logging_default(False) self.set_logging_default(False)
request.addfinalizer(self.finalize) request.addfinalizer(self.finalize)
@@ -399,7 +410,7 @@ class ACFactory:
acc.disable_logging() acc.disable_logging()
def get_next_liveconfig(self): def get_next_liveconfig(self):
""" Base function to get functional online configurations """Base function to get functional online configurations
where we can make valid SMTP and IMAP connections with. where we can make valid SMTP and IMAP connections with.
""" """
configdict = next(self._liveconfig_producer).copy() configdict = next(self._liveconfig_producer).copy()
@@ -461,11 +472,16 @@ class ACFactory:
ac = self.get_unconfigured_account() ac = self.get_unconfigured_account()
acname = ac._logid acname = ac._logid
addr = "{}@offline.org".format(acname) addr = "{}@offline.org".format(acname)
ac.update_config(dict( ac.update_config(
addr=addr, displayname=acname, mail_pw="123", dict(
configured_addr=addr, configured_mail_pw="123", addr=addr,
configured="1", displayname=acname,
)) mail_pw="123",
configured_addr=addr,
configured_mail_pw="123",
configured="1",
)
)
self._preconfigure_key(ac, addr) self._preconfigure_key(ac, addr)
self._acsetup.init_logging(ac) self._acsetup.init_logging(ac)
return ac return ac
@@ -501,7 +517,7 @@ class ACFactory:
return ac return ac
def wait_configured(self, account): def wait_configured(self, account):
""" Wait until the specified account has successfully completed configure. """ """Wait until the specified account has successfully completed configure."""
self._acsetup.wait_one_configured(account) self._acsetup.wait_one_configured(account)
def bring_accounts_online(self): def bring_accounts_online(self):
@@ -531,8 +547,10 @@ class ACFactory:
sys.executable, sys.executable,
"-u", "-u",
fn, fn,
"--email", bot_cfg["addr"], "--email",
"--password", bot_cfg["mail_pw"], bot_cfg["addr"],
"--password",
bot_cfg["mail_pw"],
bot_ac.db_path, bot_ac.db_path,
] ]
if ffi: if ffi:
@@ -543,9 +561,9 @@ class ACFactory:
stdin=subprocess.DEVNULL, stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.STDOUT, # combine stdout/stderr in one stream stderr=subprocess.STDOUT, # combine stdout/stderr in one stream
bufsize=0, # line buffering bufsize=0, # line buffering
close_fds=True, # close all FDs other than 0/1/2 close_fds=True, # close all FDs other than 0/1/2
universal_newlines=True # give back text universal_newlines=True, # give back text
) )
bot = BotProcess(popen, addr=bot_cfg["addr"]) bot = BotProcess(popen, addr=bot_cfg["addr"])
self._finalizers.append(bot.kill) self._finalizers.append(bot.kill)
@@ -565,7 +583,7 @@ class ACFactory:
def introduce_each_other(self, accounts, sending=True): def introduce_each_other(self, accounts, sending=True):
to_wait = [] to_wait = []
for i, acc in enumerate(accounts): for i, acc in enumerate(accounts):
for acc2 in accounts[i + 1:]: for acc2 in accounts[i + 1 :]:
chat = self.get_accepted_chat(acc, acc2) chat = self.get_accepted_chat(acc, acc2)
if sending: if sending:
chat.send_text("hi") chat.send_text("hi")

View File

@@ -1,12 +1,11 @@
from queue import Queue from queue import Queue
from threading import Event from threading import Event
from .hookspec import account_hookimpl, Global from .hookspec import Global, account_hookimpl
class ImexFailed(RuntimeError): class ImexFailed(RuntimeError):
""" Exception for signalling that import/export operations failed.""" """Exception for signalling that import/export operations failed."""
class ImexTracker: class ImexTracker:
@@ -24,14 +23,15 @@ class ImexTracker:
while True: while True:
ev = self._imex_events.get(timeout=progress_timeout) ev = self._imex_events.get(timeout=progress_timeout)
if isinstance(ev, int) and ev >= target_progress: if isinstance(ev, int) and ev >= target_progress:
assert ev <= progress_upper_limit, \ assert ev <= progress_upper_limit, (
str(ev) + " exceeded upper progress limit " + str(progress_upper_limit) str(ev) + " exceeded upper progress limit " + str(progress_upper_limit)
)
return ev return ev
if ev == 0: if ev == 0:
return None return None
def wait_finish(self, progress_timeout=60): def wait_finish(self, progress_timeout=60):
""" Return list of written files, raise ValueError if ExportFailed. """ """Return list of written files, raise ValueError if ExportFailed."""
files_written = [] files_written = []
while True: while True:
ev = self._imex_events.get(timeout=progress_timeout) ev = self._imex_events.get(timeout=progress_timeout)
@@ -44,7 +44,7 @@ class ImexTracker:
class ConfigureFailed(RuntimeError): class ConfigureFailed(RuntimeError):
""" Exception for signalling that configuration failed.""" """Exception for signalling that configuration failed."""
class ConfigureTracker: class ConfigureTracker:
@@ -77,11 +77,11 @@ class ConfigureTracker:
self.account.remove_account_plugin(self) self.account.remove_account_plugin(self)
def wait_smtp_connected(self): def wait_smtp_connected(self):
""" wait until smtp is configured. """ """wait until smtp is configured."""
self._smtp_finished.wait() self._smtp_finished.wait()
def wait_imap_connected(self): def wait_imap_connected(self):
""" wait until smtp is configured. """ """wait until smtp is configured."""
self._imap_finished.wait() self._imap_finished.wait()
def wait_progress(self, data1=None): def wait_progress(self, data1=None):
@@ -91,7 +91,7 @@ class ConfigureTracker:
break break
def wait_finish(self, timeout=None): def wait_finish(self, timeout=None):
""" wait until configure is completed. """wait until configure is completed.
Raise Exception if Configure failed Raise Exception if Configure failed
""" """

View File

@@ -1,10 +1,8 @@
import os import os
import platform import platform
import subprocess import subprocess
import sys import sys
if __name__ == "__main__": if __name__ == "__main__":
assert len(sys.argv) == 2 assert len(sys.argv) == 2
workspacedir = sys.argv[1] workspacedir = sys.argv[1]
@@ -13,5 +11,13 @@ if __name__ == "__main__":
if relpath.startswith("deltachat"): if relpath.startswith("deltachat"):
p = os.path.join(workspacedir, relpath) p = os.path.join(workspacedir, relpath)
subprocess.check_call( subprocess.check_call(
["auditwheel", "repair", p, "-w", workspacedir, [
"--plat", "manylinux2014_" + arch]) "auditwheel",
"repair",
p,
"-w",
workspacedir,
"--plat",
"manylinux2014_" + arch,
]
)

View File

@@ -1,8 +1,6 @@
import os import os
import sys
import subprocess import subprocess
import sys
if __name__ == "__main__": if __name__ == "__main__":
assert len(sys.argv) == 2 assert len(sys.argv) == 2

View File

@@ -1,9 +1,9 @@
import time
import threading
import pytest
import os import os
from queue import Queue, Empty import threading
import time
from queue import Empty, Queue
import pytest
import deltachat import deltachat
@@ -24,7 +24,7 @@ def test_db_busy_error(acfactory, tmpdir):
for acc in accounts: for acc in accounts:
acc.bigfile = os.path.join(acc.get_blobdir(), "bigfile") acc.bigfile = os.path.join(acc.get_blobdir(), "bigfile")
with open(acc.bigfile, "wb") as f: with open(acc.bigfile, "wb") as f:
f.write(b"01234567890"*1000_000) f.write(b"01234567890" * 1000_000)
log("created %s bigfiles" % len(accounts)) log("created %s bigfiles" % len(accounts))
contact_addrs = [acc.get_self_contact().addr for acc in accounts] contact_addrs = [acc.get_self_contact().addr for acc in accounts]
@@ -63,9 +63,7 @@ def test_db_busy_error(acfactory, tmpdir):
elif report_type == ReportType.message_echo: elif report_type == ReportType.message_echo:
continue continue
else: else:
raise ValueError("{} unknown report type {}, args={}".format( raise ValueError("{} unknown report type {}, args={}".format(addr, report_type, report_args))
addr, report_type, report_args
))
alive_count -= 1 alive_count -= 1
replier.log("shutting down") replier.log("shutting down")
replier.account.shutdown() replier.account.shutdown()
@@ -88,10 +86,7 @@ class AutoReplier:
self.current_sent = 0 self.current_sent = 0
self.addr = self.account.get_self_contact().addr self.addr = self.account.get_self_contact().addr
self._thread = threading.Thread( self._thread = threading.Thread(name="Stats{}".format(self.account), target=self.thread_stats)
name="Stats{}".format(self.account),
target=self.thread_stats
)
self._thread.setDaemon(True) self._thread.setDaemon(True)
self._thread.start() self._thread.start()

View File

@@ -1,4 +1,5 @@
import sys import sys
import pytest import pytest
@@ -277,7 +278,7 @@ def test_fetch_existing_msgs_group_and_single(acfactory, lp):
assert len(chats) == 4 # two newly created chats + self-chat + device-chat assert len(chats) == 4 # two newly created chats + self-chat + device-chat
group_chat = [c for c in chats if c.get_name() == "group name"][0] group_chat = [c for c in chats if c.get_name() == "group name"][0]
assert group_chat.is_group() assert group_chat.is_group()
private_chat, = [c for c in chats if c.get_name() == ac1_ac2_chat.get_name()] (private_chat,) = [c for c in chats if c.get_name() == ac1_ac2_chat.get_name()]
assert not private_chat.is_group() assert not private_chat.is_group()
group_messages = group_chat.get_messages() group_messages = group_chat.get_messages()
@@ -378,7 +379,7 @@ def test_ephemeral_timer(acfactory, lp):
lp.sec("ac1: check that ephemeral timer is set for chat") lp.sec("ac1: check that ephemeral timer is set for chat")
assert chat1.get_ephemeral_timer() == 60 assert chat1.get_ephemeral_timer() == 60
chat1_summary = chat1.get_summary() chat1_summary = chat1.get_summary()
assert chat1_summary["ephemeral_timer"] == {'Enabled': {'duration': 60}} assert chat1_summary["ephemeral_timer"] == {"Enabled": {"duration": 60}}
lp.sec("ac2: receive system message about ephemeral timer modification") lp.sec("ac2: receive system message about ephemeral timer modification")
ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED") ac2._evtracker.get_matching("DC_EVENT_CHAT_EPHEMERAL_TIMER_MODIFIED")

View File

@@ -1,10 +1,11 @@
import os import os
import sys
import queue import queue
import sys
from datetime import datetime, timezone from datetime import datetime, timezone
from imap_tools import AND, U
import pytest import pytest
from imap_tools import AND, U
from deltachat import const from deltachat import const
from deltachat.hookspec import account_hookimpl from deltachat.hookspec import account_hookimpl
from deltachat.message import Message from deltachat.message import Message
@@ -78,7 +79,7 @@ def test_export_import_self_keys(acfactory, tmpdir, lp):
assert len(export_files) == 2 assert len(export_files) == 2
for x in export_files: for x in export_files:
assert x.startswith(dir.strpath) assert x.startswith(dir.strpath)
key_id, = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*") (key_id,) = ac1._evtracker.get_info_regex_groups(r".*xporting.*KeyId\((.*)\).*")
ac1._evtracker.consume_events() ac1._evtracker.consume_events()
lp.sec("exported keys (private and public)") lp.sec("exported keys (private and public)")
@@ -86,8 +87,7 @@ def test_export_import_self_keys(acfactory, tmpdir, lp):
lp.indent(dir.strpath + os.sep + name) lp.indent(dir.strpath + os.sep + name)
lp.sec("importing into existing account") lp.sec("importing into existing account")
ac2.import_self_keys(dir.strpath) ac2.import_self_keys(dir.strpath)
key_id2, = ac2._evtracker.get_info_regex_groups( (key_id2,) = ac2._evtracker.get_info_regex_groups(r".*stored.*KeyId\((.*)\).*", check_error=False)
r".*stored.*KeyId\((.*)\).*", check_error=False)
assert key_id2 == key_id assert key_id2 == key_id
@@ -689,7 +689,7 @@ def test_gossip_encryption_preference(acfactory, lp):
msg = ac1._evtracker.wait_next_incoming_message() msg = ac1._evtracker.wait_next_incoming_message()
assert msg.text == "first message" assert msg.text == "first message"
assert not msg.is_encrypted() assert not msg.is_encrypted()
res = "End-to-end encryption preferred:\n{}".format(ac2.get_config('addr')) res = "End-to-end encryption preferred:\n{}".format(ac2.get_config("addr"))
assert msg.chat.get_encryption_info() == res assert msg.chat.get_encryption_info() == res
lp.sec("ac2 learns that ac3 prefers encryption") lp.sec("ac2 learns that ac3 prefers encryption")
ac2.create_chat(ac3) ac2.create_chat(ac3)
@@ -701,7 +701,7 @@ def test_gossip_encryption_preference(acfactory, lp):
lp.sec("ac3 does not know that ac1 prefers encryption") lp.sec("ac3 does not know that ac1 prefers encryption")
ac1.create_chat(ac3) ac1.create_chat(ac3)
chat = ac3.create_chat(ac1) chat = ac3.create_chat(ac1)
res = "No encryption:\n{}".format(ac1.get_config('addr')) res = "No encryption:\n{}".format(ac1.get_config("addr"))
assert chat.get_encryption_info() == res assert chat.get_encryption_info() == res
msg = chat.send_text("not encrypted") msg = chat.send_text("not encrypted")
msg = ac1._evtracker.wait_next_incoming_message() msg = ac1._evtracker.wait_next_incoming_message()
@@ -766,7 +766,7 @@ def test_send_first_message_as_long_unicode_with_cr(acfactory, lp):
def test_no_draft_if_cant_send(acfactory): def test_no_draft_if_cant_send(acfactory):
"""Tests that no quote can be set if the user can't send to this chat""" """Tests that no quote can be set if the user can't send to this chat"""
ac1, = acfactory.get_online_accounts(1) (ac1,) = acfactory.get_online_accounts(1)
device_chat = ac1.get_device_chat() device_chat = ac1.get_device_chat()
msg = Message.new_empty(ac1, "text") msg = Message.new_empty(ac1, "text")
device_chat.set_draft(msg) device_chat.set_draft(msg)
@@ -795,7 +795,9 @@ def test_dont_show_emails(acfactory, lp):
acfactory.bring_accounts_online() acfactory.bring_accounts_online()
ac1.stop_io() ac1.stop_io()
ac1.direct_imap.append("Drafts", """ ac1.direct_imap.append(
"Drafts",
"""
From: ac1 <{}> From: ac1 <{}>
Subject: subj Subject: subj
To: alice@example.org To: alice@example.org
@@ -803,8 +805,13 @@ def test_dont_show_emails(acfactory, lp):
Content-Type: text/plain; charset=utf-8 Content-Type: text/plain; charset=utf-8
message in Drafts that is moved to Sent later message in Drafts that is moved to Sent later
""".format(ac1.get_config("configured_addr"))) """.format(
ac1.direct_imap.append("Sent", """ ac1.get_config("configured_addr")
),
)
ac1.direct_imap.append(
"Sent",
"""
From: ac1 <{}> From: ac1 <{}>
Subject: subj Subject: subj
To: alice@example.org To: alice@example.org
@@ -812,8 +819,13 @@ def test_dont_show_emails(acfactory, lp):
Content-Type: text/plain; charset=utf-8 Content-Type: text/plain; charset=utf-8
message in Sent message in Sent
""".format(ac1.get_config("configured_addr"))) """.format(
ac1.direct_imap.append("Spam", """ ac1.get_config("configured_addr")
),
)
ac1.direct_imap.append(
"Spam",
"""
From: unknown.address@junk.org From: unknown.address@junk.org
Subject: subj Subject: subj
To: {} To: {}
@@ -821,8 +833,13 @@ def test_dont_show_emails(acfactory, lp):
Content-Type: text/plain; charset=utf-8 Content-Type: text/plain; charset=utf-8
Unknown message in Spam Unknown message in Spam
""".format(ac1.get_config("configured_addr"))) """.format(
ac1.direct_imap.append("Junk", """ ac1.get_config("configured_addr")
),
)
ac1.direct_imap.append(
"Junk",
"""
From: unknown.address@junk.org From: unknown.address@junk.org
Subject: subj Subject: subj
To: {} To: {}
@@ -830,7 +847,10 @@ def test_dont_show_emails(acfactory, lp):
Content-Type: text/plain; charset=utf-8 Content-Type: text/plain; charset=utf-8
Unknown message in Junk Unknown message in Junk
""".format(ac1.get_config("configured_addr"))) """.format(
ac1.get_config("configured_addr")
),
)
ac1.set_config("scan_all_folders_debounce_secs", "0") ac1.set_config("scan_all_folders_debounce_secs", "0")
lp.sec("All prepared, now let DC find the message") lp.sec("All prepared, now let DC find the message")
@@ -1154,7 +1174,7 @@ def test_send_and_receive_image(acfactory, lp, data):
def test_import_export_online_all(acfactory, tmpdir, data, lp): def test_import_export_online_all(acfactory, tmpdir, data, lp):
ac1, = acfactory.get_online_accounts(1) (ac1,) = acfactory.get_online_accounts(1)
lp.sec("create some chat content") lp.sec("create some chat content")
chat1 = ac1.create_contact("some1@example.org", name="some1").create_chat() chat1 = ac1.create_contact("some1@example.org", name="some1").create_chat()
@@ -1387,8 +1407,7 @@ def test_add_remove_member_remote_events(acfactory, lp):
ev = in_list.get() ev = in_list.get()
assert ev.action == "chat-modified" assert ev.action == "chat-modified"
assert chat.is_promoted() assert chat.is_promoted()
assert sorted(x.addr for x in chat.get_contacts()) == \ assert sorted(x.addr for x in chat.get_contacts()) == sorted(x.addr for x in ev.chat.get_contacts())
sorted(x.addr for x in ev.chat.get_contacts())
lp.sec("ac1: add address2") lp.sec("ac1: add address2")
# note that if the above create_chat() would not # note that if the above create_chat() would not
@@ -1530,8 +1549,10 @@ def test_connectivity(acfactory, lp):
ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING) ac1._evtracker.wait_for_connectivity(const.DC_CONNECTIVITY_CONNECTING)
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTING, const.DC_CONNECTIVITY_CONNECTED) ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTING, const.DC_CONNECTIVITY_CONNECTED)
lp.sec("Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, " + lp.sec(
"all messages are fetched") "Test that after calling start_io(), maybe_network() and waiting for `all_work_done()`, "
+ "all messages are fetched"
)
ac1.direct_imap.select_config_folder("inbox") ac1.direct_imap.select_config_folder("inbox")
with ac1.direct_imap.idle() as idle1: with ac1.direct_imap.idle() as idle1:
@@ -1594,10 +1615,12 @@ def test_fetch_deleted_msg(acfactory, lp):
See https://github.com/deltachat/deltachat-core-rust/issues/2429. See https://github.com/deltachat/deltachat-core-rust/issues/2429.
""" """
ac1, = acfactory.get_online_accounts(1) (ac1,) = acfactory.get_online_accounts(1)
ac1.stop_io() ac1.stop_io()
ac1.direct_imap.append("INBOX", """ ac1.direct_imap.append(
"INBOX",
"""
From: alice <alice@example.org> From: alice <alice@example.org>
Subject: subj Subject: subj
To: bob@example.com To: bob@example.com
@@ -1606,7 +1629,8 @@ def test_fetch_deleted_msg(acfactory, lp):
Content-Type: text/plain; charset=utf-8 Content-Type: text/plain; charset=utf-8
Deleted message Deleted message
""") """,
)
ac1.direct_imap.delete("1:*", expunge=False) ac1.direct_imap.delete("1:*", expunge=False)
ac1.start_io() ac1.start_io()
@@ -1888,11 +1912,26 @@ def test_group_quote(acfactory, lp):
assert received_reply.quote.id == out_msg.id assert received_reply.quote.id == out_msg.id
@pytest.mark.parametrize("folder,move,expected_destination,", [ @pytest.mark.parametrize(
("xyz", False, "xyz"), # Test that emails are recognized in a random folder but not moved "folder,move,expected_destination,",
("xyz", True, "DeltaChat"), # ...emails are found in a random folder and moved to DeltaChat [
("Spam", False, "INBOX"), # ...emails are moved from the spam folder to the Inbox (
]) "xyz",
False,
"xyz",
), # Test that emails are recognized in a random folder but not moved
(
"xyz",
True,
"DeltaChat",
), # ...emails are found in a random folder and moved to DeltaChat
(
"Spam",
False,
"INBOX",
), # ...emails are moved from the spam folder to the Inbox
],
)
# Testrun.org does not support the CREATE-SPECIAL-USE capability, which means that we can't create a folder with # Testrun.org does not support the CREATE-SPECIAL-USE capability, which means that we can't create a folder with
# the "\Junk" flag (see https://tools.ietf.org/html/rfc6154). So, we can't test spam folder detection by flag. # the "\Junk" flag (see https://tools.ietf.org/html/rfc6154). So, we can't test spam folder detection by flag.
def test_scan_folders(acfactory, lp, folder, move, expected_destination): def test_scan_folders(acfactory, lp, folder, move, expected_destination):

View File

@@ -2,13 +2,13 @@ from __future__ import print_function
import os.path import os.path
import shutil import shutil
from filecmp import cmp
import pytest import pytest
from filecmp import cmp
def wait_msg_delivered(account, msg_list): def wait_msg_delivered(account, msg_list):
""" wait for one or more MSG_DELIVERED events to match msg_list contents. """ """wait for one or more MSG_DELIVERED events to match msg_list contents."""
msg_list = list(msg_list) msg_list = list(msg_list)
while msg_list: while msg_list:
ev = account._evtracker.get_matching("DC_EVENT_MSG_DELIVERED") ev = account._evtracker.get_matching("DC_EVENT_MSG_DELIVERED")
@@ -16,7 +16,7 @@ def wait_msg_delivered(account, msg_list):
def wait_msgs_changed(account, msgs_list): def wait_msgs_changed(account, msgs_list):
""" wait for one or more MSGS_CHANGED events to match msgs_list contents. """ """wait for one or more MSGS_CHANGED events to match msgs_list contents."""
account.log("waiting for msgs_list={}".format(msgs_list)) account.log("waiting for msgs_list={}".format(msgs_list))
msgs_list = list(msgs_list) msgs_list = list(msgs_list)
while msgs_list: while msgs_list:
@@ -38,7 +38,7 @@ class TestOnlineInCreation:
lp.sec("Creating in-creation file outside of blobdir") lp.sec("Creating in-creation file outside of blobdir")
assert tmpdir.strpath != ac1.get_blobdir() assert tmpdir.strpath != ac1.get_blobdir()
src = tmpdir.join('file.txt').ensure(file=1) src = tmpdir.join("file.txt").ensure(file=1)
with pytest.raises(Exception): with pytest.raises(Exception):
chat.prepare_message_file(src.strpath) chat.prepare_message_file(src.strpath)
@@ -48,11 +48,11 @@ class TestOnlineInCreation:
lp.sec("Creating file outside of blobdir") lp.sec("Creating file outside of blobdir")
assert tmpdir.strpath != ac1.get_blobdir() assert tmpdir.strpath != ac1.get_blobdir()
src = tmpdir.join('file.txt') src = tmpdir.join("file.txt")
src.write("hello there\n") src.write("hello there\n")
chat.send_file(src.strpath) chat.send_file(src.strpath)
blob_src = os.path.join(ac1.get_blobdir(), 'file.txt') blob_src = os.path.join(ac1.get_blobdir(), "file.txt")
assert os.path.exists(blob_src), "file.txt not copied to blobdir" assert os.path.exists(blob_src), "file.txt not copied to blobdir"
def test_forward_increation(self, acfactory, data, lp): def test_forward_increation(self, acfactory, data, lp):
@@ -63,7 +63,7 @@ class TestOnlineInCreation:
lp.sec("create a message with a file in creation") lp.sec("create a message with a file in creation")
orig = data.get_path("d.png") orig = data.get_path("d.png")
path = os.path.join(ac1.get_blobdir(), 'd.png') path = os.path.join(ac1.get_blobdir(), "d.png")
with open(path, "x") as fp: with open(path, "x") as fp:
fp.write("preparing") fp.write("preparing")
prepared_original = chat.prepare_message_file(path) prepared_original = chat.prepare_message_file(path)
@@ -94,10 +94,7 @@ class TestOnlineInCreation:
assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered() assert fwd_msg.is_out_pending() or fwd_msg.is_out_delivered()
lp.sec("wait for both messages to be delivered to SMTP") lp.sec("wait for both messages to be delivered to SMTP")
wait_msg_delivered(ac1, [ wait_msg_delivered(ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.id)])
(chat2.id, forwarded_id),
(chat.id, prepared_original.id)
])
lp.sec("wait1 for original or forwarded messages to arrive") lp.sec("wait1 for original or forwarded messages to arrive")
received_original = ac2._evtracker.wait_next_incoming_message() received_original = ac2._evtracker.wait_next_incoming_message()

View File

@@ -1,32 +1,50 @@
from __future__ import print_function from __future__ import print_function
import pytest
import os import os
import time import time
from deltachat import const, Account
from deltachat.message import Message
from deltachat.hookspec import account_hookimpl
from deltachat.capi import ffi, lib
from deltachat.cutil import iter_array
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import pytest
@pytest.mark.parametrize("msgtext,res", [ from deltachat import Account, const
("Member Me (tmp1@x.org) removed by tmp2@x.org.", from deltachat.capi import ffi, lib
("removed", "tmp1@x.org", "tmp2@x.org")), from deltachat.cutil import iter_array
("Member With space (tmp1@x.org) removed by tmp2@x.org.", from deltachat.hookspec import account_hookimpl
("removed", "tmp1@x.org", "tmp2@x.org")), from deltachat.message import Message
("Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
("removed", "tmp1@x.org", "tmp2@x.org")),
("Member With space (tmp1@x.org) removed by me", @pytest.mark.parametrize(
("removed", "tmp1@x.org", "me")), "msgtext,res",
("Group left by some one (tmp1@x.org).", [
("removed", "tmp1@x.org", "tmp1@x.org")), (
("Group left by tmp1@x.org.", "Member Me (tmp1@x.org) removed by tmp2@x.org.",
("removed", "tmp1@x.org", "tmp1@x.org")), ("removed", "tmp1@x.org", "tmp2@x.org"),
("Member tmp1@x.org added by tmp2@x.org.", ("added", "tmp1@x.org", "tmp2@x.org")), ),
("Member nothing bla bla", None), (
("Another unknown system message", None), "Member With space (tmp1@x.org) removed by tmp2@x.org.",
]) ("removed", "tmp1@x.org", "tmp2@x.org"),
),
(
"Member With space (tmp1@x.org) removed by Another member (tmp2@x.org).",
("removed", "tmp1@x.org", "tmp2@x.org"),
),
(
"Member With space (tmp1@x.org) removed by me",
("removed", "tmp1@x.org", "me"),
),
(
"Group left by some one (tmp1@x.org).",
("removed", "tmp1@x.org", "tmp1@x.org"),
),
("Group left by tmp1@x.org.", ("removed", "tmp1@x.org", "tmp1@x.org")),
(
"Member tmp1@x.org added by tmp2@x.org.",
("added", "tmp1@x.org", "tmp2@x.org"),
),
("Member nothing bla bla", None),
("Another unknown system message", None),
],
)
def test_parse_system_add_remove(msgtext, res): def test_parse_system_add_remove(msgtext, res):
from deltachat.message import parse_system_add_remove from deltachat.message import parse_system_add_remove
@@ -424,11 +442,14 @@ class TestOfflineChat:
assert os.path.exists(msg.filename) assert os.path.exists(msg.filename)
assert msg.filemime == "image/png" assert msg.filemime == "image/png"
@pytest.mark.parametrize("typein,typeout", [ @pytest.mark.parametrize(
"typein,typeout",
[
(None, "application/octet-stream"), (None, "application/octet-stream"),
("text/plain", "text/plain"), ("text/plain", "text/plain"),
("image/png", "image/png"), ("image/png", "image/png"),
]) ],
)
def test_message_file(self, ac1, chat1, data, lp, typein, typeout): def test_message_file(self, ac1, chat1, data, lp, typein, typeout):
lp.sec("sending file") lp.sec("sending file")
fn = data.get_path("r.txt") fn = data.get_path("r.txt")
@@ -629,6 +650,6 @@ class TestOfflineChat:
lp.sec("check message count of only system messages (without daymarkers)") lp.sec("check message count of only system messages (without daymarkers)")
dc_array = ffi.gc( dc_array = ffi.gc(
lib.dc_get_chat_msgs(ac1._dc_context, chat.id, const.DC_GCM_INFO_ONLY, 0), lib.dc_get_chat_msgs(ac1._dc_context, chat.id, const.DC_GCM_INFO_ONLY, 0),
lib.dc_array_unref lib.dc_array_unref,
) )
assert len(list(iter_array(dc_array, lambda x: x))) == 2 assert len(list(iter_array(dc_array, lambda x: x))) == 2

View File

@@ -1,24 +1,25 @@
import os import os
from queue import Queue from queue import Queue
from deltachat import capi, cutil, const
from deltachat import register_global_plugin from deltachat import capi, const, cutil, register_global_plugin
from deltachat.capi import ffi, lib
from deltachat.hookspec import global_hookimpl from deltachat.hookspec import global_hookimpl
from deltachat.capi import ffi from deltachat.testplugin import (
from deltachat.capi import lib ACSetup,
from deltachat.testplugin import ACSetup, create_dict_from_files_in_path, write_dict_to_dir create_dict_from_files_in_path,
write_dict_to_dir,
)
# from deltachat.account import EventLogger # from deltachat.account import EventLogger
class TestACSetup: class TestACSetup:
def test_cache_writing(self, tmp_path): def test_cache_writing(self, tmp_path):
base = tmp_path.joinpath("hello") base = tmp_path.joinpath("hello")
base.mkdir() base.mkdir()
d1 = base.joinpath("dir1") d1 = base.joinpath("dir1")
d1.mkdir() d1.mkdir()
d1.joinpath("file1").write_bytes(b'content1') d1.joinpath("file1").write_bytes(b"content1")
d2 = d1.joinpath("dir2") d2 = d1.joinpath("dir2")
d2.mkdir() d2.mkdir()
d2.joinpath("file2").write_bytes(b"123") d2.joinpath("file2").write_bytes(b"123")
@@ -103,6 +104,7 @@ def test_dc_close_events(tmpdir, acfactory):
def dc_account_after_shutdown(self, account): def dc_account_after_shutdown(self, account):
assert account._dc_context is None assert account._dc_context is None
shutdowns.put(account) shutdowns.put(account)
register_global_plugin(ShutdownPlugin()) register_global_plugin(ShutdownPlugin())
assert hasattr(ac1, "_dc_context") assert hasattr(ac1, "_dc_context")
ac1.shutdown() ac1.shutdown()
@@ -178,8 +180,8 @@ def test_get_info_open(tmpdir):
lib.dc_context_unref, lib.dc_context_unref,
) )
info = cutil.from_dc_charpointer(lib.dc_get_info(ctx)) info = cutil.from_dc_charpointer(lib.dc_get_info(ctx))
assert 'deltachat_core_version' in info assert "deltachat_core_version" in info
assert 'database_dir' in info assert "database_dir" in info
def test_logged_hook_failure(acfactory): def test_logged_hook_failure(acfactory):
@@ -187,7 +189,7 @@ def test_logged_hook_failure(acfactory):
cap = [] cap = []
ac1.log = cap.append ac1.log = cap.append
with ac1._event_thread.swallow_and_log_exception("some"): with ac1._event_thread.swallow_and_log_exception("some"):
0/0 0 / 0
assert cap assert cap
assert "some" in str(cap) assert "some" in str(cap)
assert "ZeroDivisionError" in str(cap) assert "ZeroDivisionError" in str(cap)
@@ -202,7 +204,7 @@ def test_logged_ac_process_ffi_failure(acfactory):
class FailPlugin: class FailPlugin:
@account_hookimpl @account_hookimpl
def ac_process_ffi_event(ffi_event): def ac_process_ffi_event(ffi_event):
0/0 0 / 0
cap = Queue() cap = Queue()
ac1.log = cap.put ac1.log = cap.put

View File

@@ -36,10 +36,14 @@ skipsdist = True
skip_install = True skip_install = True
deps = deps =
flake8 flake8
isort
black
# pygments required by rst-lint # pygments required by rst-lint
pygments pygments
restructuredtext_lint restructuredtext_lint
commands = commands =
isort --check --profile black src/deltachat examples/ tests/
black --check src/deltachat examples/ tests/
flake8 src/deltachat flake8 src/deltachat
flake8 tests/ examples/ flake8 tests/ examples/
rst-lint --encoding 'utf-8' README.rst rst-lint --encoding 'utf-8' README.rst
@@ -85,3 +89,4 @@ markers =
[flake8] [flake8]
max-line-length = 120 max-line-length = 120
ignore = E203, E266, E501, W503