apply isort and black formatters, add format checking to CI

This commit is contained in:
adbenitez
2022-05-29 21:11:49 -04:00
parent 62b50c87d4
commit 16e0f0e986
26 changed files with 899 additions and 575 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
@@ -15,7 +14,9 @@ class EchoPlugin:
message.create_chat() message.create_chat()
addr = message.get_sender_contact().addr addr = message.get_sender_contact().addr
if message.is_system_message(): if message.is_system_message():
message.chat.send_text("echoing system message from {}:\n{}".format(addr, message)) message.chat.send_text(
"echoing system message from {}:\n{}".format(addr, message)
)
else: else:
text = message.text text = message.text
message.chat.send_text("echoing from {}:\n{}".format(addr, text)) message.chat.send_text("echoing from {}:\n{}".format(addr, text))

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

@@ -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:
@@ -46,12 +46,15 @@ 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
parser = argparse.ArgumentParser(prog=argv[0] if argv else None) parser = argparse.ArgumentParser(prog=argv[0] if argv else None)
parser.add_argument("db", action="store", help="database file") parser.add_argument("db", action="store", help="database file")
parser.add_argument("--show-ffi", action="store_true", help="show low level ffi events") parser.add_argument(
"--show-ffi", action="store_true", help="show low level ffi events"
)
parser.add_argument("--email", action="store", help="email address") parser.add_argument("--email", action="store", help="email address")
parser.add_argument("--password", action="store", help="password") parser.add_argument("--password", action="store", help="password")
@@ -69,9 +72,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,35 @@ 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 +66,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(
source=src_name,
output_file=dst_name, output_file=dst_name,
include_dirs=flags['include_dirs'], include_dirs=flags["include_dirs"],
macros=[('PY_CFFI', '1')]) macros=[("PY_CFFI", "1")],
)
with open(dst_name, "r") as dst_fp: with open(dst_name, "r") as dst_fp:
return dst_fp.read() return dst_fp.read()
finally: finally:
@@ -87,7 +94,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 +104,22 @@ 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(
output_progname="where", objects=[obj_name], output_progname="where", output_dir=tmpdir
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 +136,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 +165,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 +196,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 +210,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,21 +1,28 @@
""" 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):
@@ -28,10 +35,14 @@ def get_core_info():
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_new(
as_dc_charpointer(""), as_dc_charpointer(path.name), ffi.NULL
),
lib.dc_context_unref, lib.dc_context_unref,
)) )
)
def get_dc_info_as_dict(dc_context): def get_dc_info_as_dict(dc_context):
@@ -50,6 +61,7 @@ class Account(object):
by the underlying deltachat core library. All public Account methods are by the underlying deltachat core library. All public Account methods are
meant to be memory-safe and return memory-safe objects. meant to be memory-safe and return memory-safe objects.
""" """
MissingCredentials = MissingCredentials MissingCredentials = MissingCredentials
def __init__(self, db_path, os_name=None, logging=True) -> None: def __init__(self, db_path, os_name=None, logging=True) -> None:
@@ -102,8 +114,11 @@ 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(
name, self._configkeys)) "{!r} not a valid config key, existing keys: {!r}".format(
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."""
@@ -175,10 +190,12 @@ 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(
self._dc_context,
as_dc_charpointer(addr), as_dc_charpointer(addr),
as_dc_charpointer(public), as_dc_charpointer(public),
as_dc_charpointer(secret)) as_dc_charpointer(secret),
)
if res == 0: if res == 0:
raise Exception("Failed to set key") raise Exception("Failed to set key")
@@ -224,8 +241,7 @@ class Account(object):
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)
@@ -319,8 +335,7 @@ class Account(object):
: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_get_blocked_contacts(self._dc_context), lib.dc_array_unref
lib.dc_array_unref
) )
return list(iter_array(dc_array, lambda x: Contact(self, x))) return list(iter_array(dc_array, lambda x: Contact(self, x)))
@@ -345,17 +360,13 @@ class Account(object):
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_get_contacts(self._dc_context, flags, query), lib.dc_array_unref
lib.dc_array_unref
) )
return list(iter_array(dc_array, lambda x: Contact(self, x))) return list(iter_array(dc_array, lambda x: Contact(self, 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:
@@ -390,8 +401,7 @@ class Account(object):
: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_get_chatlist(self._dc_context, 0, ffi.NULL, 0), lib.dc_chatlist_unref
lib.dc_chatlist_unref
) )
assert dc_chatlist != ffi.NULL assert dc_chatlist != ffi.NULL
@@ -529,8 +539,7 @@ class Account(object):
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_check_qr(self._dc_context, as_dc_charpointer(qr)), lib.dc_lot_unref
lib.dc_lot_unref
) )
lot = DCLot(res) lot = DCLot(res)
if lot.state() == const.DC_QR_ERROR: if lot.state() == const.DC_QR_ERROR:

View File

@@ -1,16 +1,22 @@
""" 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.
@@ -20,13 +26,16 @@ class Chat(object):
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.account._dc_context == other.account._dc_context self.id == getattr(other, "id", None)
and 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)
@@ -37,8 +46,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_get_chat(self.account._dc_context, self.id), lib.dc_chat_unref
lib.dc_chat_unref
) )
def delete(self) -> None: def delete(self) -> None:
@@ -132,7 +140,9 @@ class Chat(object):
mute_duration = -1 mute_duration = -1
else: else:
mute_duration = duration mute_duration = duration
ret = lib.dc_set_chat_mute_duration(self.account._dc_context, self.id, mute_duration) ret = lib.dc_set_chat_mute_duration(
self.account._dc_context, self.id, mute_duration
)
if not bool(ret): if not bool(ret):
raise ValueError("Call to dc_set_chat_mute_duration failed") raise ValueError("Call to dc_set_chat_mute_duration failed")
@@ -167,7 +177,9 @@ class Chat(object):
:returns: True on success, False otherwise :returns: True on success, False otherwise
""" """
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.
@@ -343,7 +355,7 @@ class Chat(object):
""" """
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)))
@@ -390,7 +402,9 @@ class Chat(object):
:returns: None :returns: None
""" """
contact = self.account.get_contact(obj) contact = self.account.get_contact(obj)
ret = lib.dc_remove_contact_from_chat(self.account._dc_context, self.id, contact.id) ret = lib.dc_remove_contact_from_chat(
self.account._dc_context, self.id, contact.id
)
if ret != 1: if ret != 1:
raise ValueError("could not remove contact {!r} from chat".format(contact)) raise ValueError("could not remove contact {!r} from chat".format(contact))
@@ -399,19 +413,18 @@ class Chat(object):
:returns: list of :class:`deltachat.contact.Contact` objects for this chat :returns: list of :class:`deltachat.contact.Contact` objects for this chat
""" """
from .contact import Contact from .contact import Contact
dc_array = ffi.gc( dc_array = ffi.gc(
lib.dc_get_chat_contacts(self.account._dc_context, self.id), lib.dc_get_chat_contacts(self.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)
@@ -476,7 +489,10 @@ class Chat(object):
"""return True if this chat is archived. """return True if this chat is archived.
:returns: True if archived. :returns: True if archived.
""" """
return lib.dc_chat_get_visibility(self._dc_chat) == const.DC_CHAT_VISIBILITY_ARCHIVED return (
lib.dc_chat_get_visibility(self._dc_chat)
== const.DC_CHAT_VISIBILITY_ARCHIVED
)
def enable_sending_locations(self, seconds): def enable_sending_locations(self, seconds):
"""enable sending locations for this chat. """enable sending locations for this chat.
@@ -507,17 +523,20 @@ class Chat(object):
else: else:
contact_id = contact.id contact_id = contact.id
dc_array = lib.dc_get_locations(self.account._dc_context, self.id, contact_id, time_from, time_to) dc_array = lib.dc_get_locations(
self.account._dc_context, self.id, contact_id, time_from, time_to
)
return [ return [
Location( Location(
latitude=lib.dc_array_get_latitude(dc_array, i), latitude=lib.dc_array_get_latitude(dc_array, i),
longitude=lib.dc_array_get_longitude(dc_array, i), longitude=lib.dc_array_get_longitude(dc_array, i),
accuracy=lib.dc_array_get_accuracy(dc_array, i), accuracy=lib.dc_array_get_accuracy(dc_array, i),
timestamp=datetime.fromtimestamp( timestamp=datetime.fromtimestamp(
lib.dc_array_get_timestamp(dc_array, i), lib.dc_array_get_timestamp(dc_array, i), timezone.utc
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

@@ -14,26 +14,32 @@ class Contact(object):
You obtain instances of it through :class:`deltachat.account.Account`. You obtain instances of it through :class:`deltachat.account.Account`.
""" """
def __init__(self, account, id): def __init__(self, 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
def __eq__(self, other): def __eq__(self, other):
return self.account._dc_context == other.account._dc_context and self.id == other.id return (
self.account._dc_context == other.account._dc_context
and self.id == other.id
)
def __ne__(self, other): def __ne__(self, other):
return not (self == other) return not (self == other)
def __repr__(self): def __repr__(self):
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self.account._dc_context) return "<Contact id={} addr={} dc_context={}>".format(
self.id, self.addr, self.account._dc_context
)
@property @property
def _dc_contact(self): def _dc_contact(self):
return ffi.gc( return ffi.gc(
lib.dc_get_contact(self.account._dc_context, self.id), lib.dc_get_contact(self.account._dc_context, self.id), lib.dc_contact_unref
lib.dc_contact_unref
) )
@props.with_doc @props.with_doc

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:*"
@@ -88,7 +97,7 @@ class DirectImap:
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)
@@ -163,13 +178,20 @@ 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]
@@ -192,18 +214,17 @@ 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."""

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:
@@ -29,6 +30,7 @@ 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"):
@@ -133,7 +135,12 @@ class FFIEventTracker:
if current == expected_next: if current == expected_next:
return return
elif current != previous: elif current != previous:
raise Exception("Expected connectivity " + str(expected_next) + " but got " + str(current)) raise Exception(
"Expected connectivity "
+ str(expected_next)
+ " but got "
+ str(current)
)
self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED") self.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
@@ -195,6 +202,7 @@ class EventThread(threading.Thread):
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")
@@ -244,8 +252,12 @@ class EventThread(threading.Thread):
lib.dc_event_unref(event) lib.dc_event_unref(event)
ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2) ffi_event = FFIEvent(name=evt_name, data1=data1, data2=data2)
with self.swallow_and_log_exception("ac_process_ffi_event {}".format(ffi_event)): with self.swallow_and_log_exception(
self.account._pm.hook.ac_process_ffi_event(account=self, ffi_event=ffi_event) "ac_process_ffi_event {}".format(ffi_event)
):
self.account._pm.hook.ac_process_ffi_event(
account=self, ffi_event=ffi_event
)
for name, kwargs in self._map_ffi_event(ffi_event): for name, kwargs in self._map_ffi_event(ffi_event):
hook = getattr(self.account._pm.hook, name) hook = getattr(self.account._pm.hook, name)
info = "call {} kwargs={} failed".format(name, kwargs) info = "call {} kwargs={} failed".format(name, kwargs)
@@ -259,8 +271,9 @@ 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(
.format(info, ex, logfile.getvalue())) "{}\nException {}\nTraceback:\n{}".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 +295,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)
@@ -19,6 +18,7 @@ class PerAccount:
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)
@@ -89,6 +89,7 @@ class Global:
plugin manager instance. plugin manager instance.
""" """
_plugin_manager = None _plugin_manager = None
@classmethod @classmethod

View File

@@ -2,13 +2,13 @@
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.
@@ -16,6 +16,7 @@ class 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,16 +33,22 @@ 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(
lib.dc_get_msg(account._dc_context, id), account, ffi.gc(lib.dc_get_msg(account._dc_context, id), lib.dc_msg_unref)
lib.dc_msg_unref )
))
@classmethod @classmethod
def new_empty(cls, account, view_type): def new_empty(cls, account, view_type):
@@ -54,10 +61,12 @@ 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.
@@ -87,8 +96,12 @@ class Message(object):
@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 (
lib.dc_get_msg_html(self.account._dc_context, self.id)) or "" from_optional_dc_charpointer(
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."""
@@ -153,14 +166,14 @@ class Message(object):
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.account._dc_context, self.id, as_dc_charpointer(setup_code)
self.id,
as_dc_charpointer(setup_code)
) )
if res == 0: if res == 0:
raise ValueError("could not decrypt") raise ValueError("could not decrypt")
@@ -238,6 +251,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 +271,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)
@@ -267,12 +282,12 @@ 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 +302,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)
@@ -300,8 +316,7 @@ class Message(object):
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_get_msg(self.account._dc_context, self.id), lib.dc_msg_unref
lib.dc_msg_unref
) )
return lib.dc_msg_get_state(dc_msg) return lib.dc_msg_get_state(dc_msg)
@@ -332,23 +347,23 @@ class Message(object):
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):
@@ -410,13 +425,13 @@ class Message(object):
# 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 +439,17 @@ 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 +466,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(".")
@@ -467,7 +485,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

@@ -16,7 +16,9 @@ class Provider(object):
def __init__(self, account, addr) -> None: def __init__(self, account, addr) -> None:
provider = ffi.gc( provider = ffi.gc(
lib.dc_provider_new_from_email(account._dc_context, as_dc_charpointer(addr)), lib.dc_provider_new_from_email(
account._dc_context, as_dc_charpointer(addr)
),
lib.dc_provider_unref, lib.dc_provider_unref,
) )
if provider == ffi.NULL: if provider == ffi.NULL:
@@ -26,14 +28,14 @@ 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,63 @@
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",
action="store",
default=None,
help="a file with >=2 lines where each line " help="a file with >=2 lines where each line "
"contains NAME=VALUE config settings for one account" "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 +120,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,8 +144,8 @@ 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 = {}
@@ -150,11 +159,13 @@ class TestProcess:
""" """
liveconfig_opt = self.pytestconfig.getoption("--liveconfig") liveconfig_opt = self.pytestconfig.getoption("--liveconfig")
if not liveconfig_opt: if not liveconfig_opt:
pytest.skip("specify DCC_NEW_TMP_EMAIL or --liveconfig to provide live accounts") pytest.skip(
"specify DCC_NEW_TMP_EMAIL or --liveconfig to provide live accounts"
)
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,14 +181,21 @@ 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(
index, res.status_code, res.text)) "newtmpuser count={} code={}: '{}'".format(
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"]))
self._configlist.append(config) self._configlist.append(config)
yield config yield config
pytest.fail("more than {} live accounts requested.".format(MAX_LIVE_CREATED_ACCOUNTS)) pytest.fail(
"more than {} live accounts requested.".format(
MAX_LIVE_CREATED_ACCOUNTS
)
)
def cache_maybe_retrieve_configured_db_files(self, cache_addr, db_target_path): def cache_maybe_retrieve_configured_db_files(self, cache_addr, db_target_path):
db_target_path = pathlib.Path(db_target_path) db_target_path = pathlib.Path(db_target_path)
@@ -230,10 +248,15 @@ 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.normpath(x)
for x in [
os.path.join(os.path.dirname(request.fspath.strpath), "data"), os.path.join(os.path.dirname(request.fspath.strpath), "data"),
os.path.join(os.path.dirname(__file__), "..", "..", "..", "test-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."""
@@ -257,6 +280,7 @@ class ACSetup:
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"
@@ -275,10 +299,13 @@ class ACSetup:
"""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):
@@ -375,8 +402,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)
@@ -426,7 +452,9 @@ class ACFactory:
# we need to use fixed database basename for maybe_cache_* functions to work # we need to use fixed database basename for maybe_cache_* functions to work
path = self.tmpdir.mkdir(logid).join("dc.db") path = self.tmpdir.mkdir(logid).join("dc.db")
if try_cache_addr: if try_cache_addr:
self.testprocess.cache_maybe_retrieve_configured_db_files(try_cache_addr, path) self.testprocess.cache_maybe_retrieve_configured_db_files(
try_cache_addr, path
)
ac = Account(path.strpath, logging=self._logging) ac = Account(path.strpath, logging=self._logging)
ac._logid = logid # later instantiated FFIEventLogger needs this ac._logid = logid # later instantiated FFIEventLogger needs this
ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac)) ac._evtracker = ac.add_account_plugin(FFIEventTracker(ac))
@@ -448,8 +476,12 @@ class ACFactory:
except IndexError: except IndexError:
pass pass
else: else:
fname_pub = self.data.read_path("key/{name}-public.asc".format(name=keyname)) fname_pub = self.data.read_path(
fname_sec = self.data.read_path("key/{name}-secret.asc".format(name=keyname)) "key/{name}-public.asc".format(name=keyname)
)
fname_sec = self.data.read_path(
"key/{name}-secret.asc".format(name=keyname)
)
if fname_pub and fname_sec: if fname_pub and fname_sec:
account._preconfigure_keypair(addr, fname_pub, fname_sec) account._preconfigure_keypair(addr, fname_pub, fname_sec)
return True return True
@@ -461,11 +493,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,
displayname=acname,
mail_pw="123",
configured_addr=addr,
configured_mail_pw="123",
configured="1", 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
@@ -531,8 +568,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:
@@ -545,7 +584,7 @@ class ACFactory:
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)
@@ -599,7 +638,9 @@ class BotProcess:
# we read stdout as quickly as we can in a thread and make # we read stdout as quickly as we can in a thread and make
# the (unicode) lines available for readers through a queue. # the (unicode) lines available for readers through a queue.
self.stdout_queue = queue.Queue() self.stdout_queue = queue.Queue()
self.stdout_thread = t = threading.Thread(target=self._run_stdout_thread, name="bot-stdout-thread") self.stdout_thread = t = threading.Thread(
target=self._run_stdout_thread, name="bot-stdout-thread"
)
t.daemon = True t.daemon = True
t.start() t.start()
@@ -622,7 +663,9 @@ class BotProcess:
self.popen.wait(timeout=timeout) self.popen.wait(timeout=timeout)
def fnmatch_lines(self, pattern_lines): def fnmatch_lines(self, pattern_lines):
patterns = [x.strip() for x in Source(pattern_lines.rstrip()).lines if x.strip()] patterns = [
x.strip() for x in Source(pattern_lines.rstrip()).lines if x.strip()
]
for next_pattern in patterns: for next_pattern in patterns:
print("+++FNMATCH:", next_pattern) print("+++FNMATCH:", next_pattern)
ignored = [] ignored = []

View File

@@ -1,8 +1,7 @@
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):
@@ -20,12 +19,17 @@ class ImexTracker:
elif ffi_event.name == "DC_EVENT_IMEX_FILE_WRITTEN": elif ffi_event.name == "DC_EVENT_IMEX_FILE_WRITTEN":
self._imex_events.put(ffi_event.data2) self._imex_events.put(ffi_event.data2)
def wait_progress(self, target_progress, progress_upper_limit=1000, progress_timeout=60): def wait_progress(
self, target_progress, progress_upper_limit=1000, progress_timeout=60
):
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

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
@@ -10,6 +8,10 @@ if __name__ == "__main__":
# pip wheel will build in an isolated tmp dir that does not have git # pip wheel will build in an isolated tmp dir that does not have git
# history so setuptools_scm can not automatically determine a # history so setuptools_scm can not automatically determine a
# version there. So pass in the version through an env var. # version there. So pass in the version through an env var.
version = subprocess.check_output(["python", "setup.py", "--version"]).strip().split(b"\n")[-1] version = (
subprocess.check_output(["python", "setup.py", "--version"])
.strip()
.split(b"\n")[-1]
)
os.environ["SETUPTOOLS_SCM_PRETEND_VERSION"] = version.decode("ascii") os.environ["SETUPTOOLS_SCM_PRETEND_VERSION"] = version.decode("ascii")
subprocess.check_call(("pip wheel . -w %s" % wheelhousedir).split()) subprocess.check_call(("pip wheel . -w %s" % wheelhousedir).split())

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
@@ -41,7 +41,9 @@ def test_db_busy_error(acfactory, tmpdir):
# each replier receives all events and sends report events to receive_queue # each replier receives all events and sends report events to receive_queue
repliers = [] repliers = []
for acc in accounts: for acc in accounts:
replier = AutoReplier(acc, log=log, num_send=500, num_bigfiles=5, report_func=report_func) replier = AutoReplier(
acc, log=log, num_send=500, num_bigfiles=5, report_func=report_func
)
acc.add_account_plugin(replier) acc.add_account_plugin(replier)
repliers.append(replier) repliers.append(replier)
@@ -63,9 +65,11 @@ 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()
@@ -89,8 +93,7 @@ class AutoReplier:
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), name="Stats{}".format(self.account), target=self.thread_stats
target=self.thread_stats
) )
self._thread.setDaemon(True) self._thread.setDaemon(True)
self._thread.start() self._thread.start()
@@ -116,11 +119,16 @@ class AutoReplier:
self.current_sent += 1 self.current_sent += 1
# we are still alive, let's send a reply # we are still alive, let's send a reply
if self.num_bigfiles and self.current_sent % (self.num_send / self.num_bigfiles) == 0: if (
self.num_bigfiles
and self.current_sent % (self.num_send / self.num_bigfiles) == 0
):
message.chat.send_text("send big file as reply to: {}".format(message.text)) message.chat.send_text("send big file as reply to: {}".format(message.text))
msg = message.chat.send_file(self.account.bigfile) msg = message.chat.send_file(self.account.bigfile)
else: else:
msg = message.chat.send_text("got message id {}, small text reply".format(message.id)) msg = message.chat.send_text(
"got message id {}, small text reply".format(message.id)
)
assert msg.text assert msg.text
self.log("message-sent: {}".format(msg)) self.log("message-sent: {}".format(msg))
self.report_func(self, ReportType.message_echo) self.report_func(self, ReportType.message_echo)

View File

@@ -1,4 +1,5 @@
import sys import sys
import pytest import pytest
@@ -215,7 +216,9 @@ def test_fetch_existing(acfactory, lp, mvbox_move):
chat.send_text("message text") chat.send_text("message text")
assert_folders_configured(ac1) assert_folders_configured(ac1)
lp.sec("wait until the bcc_self message arrives in correct folder and is marked seen") lp.sec(
"wait until the bcc_self message arrives in correct folder and is marked seen"
)
assert idle1.wait_for_seen() assert idle1.wait_for_seen()
assert_folders_configured(ac1) assert_folders_configured(ac1)
@@ -254,7 +257,9 @@ def test_fetch_existing_msgs_group_and_single(acfactory, lp):
acfactory.bring_accounts_online() acfactory.bring_accounts_online()
lp.sec("receive a message") lp.sec("receive a message")
ac2.create_group_chat("group name", contacts=[ac1]).send_text("incoming, unencrypted group message") ac2.create_group_chat("group name", contacts=[ac1]).send_text(
"incoming, unencrypted group message"
)
ac1._evtracker.wait_next_incoming_message() ac1._evtracker.wait_next_incoming_message()
lp.sec("send out message with bcc to ourselves") lp.sec("send out message with bcc to ourselves")
@@ -277,7 +282,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 +383,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
@@ -34,8 +35,12 @@ def test_basic_imap_api(acfactory, tmpdir):
def test_configure_generate_key(acfactory, lp): def test_configure_generate_key(acfactory, lp):
# A slow test which will generate new keys. # A slow test which will generate new keys.
acfactory.remove_preconfigured_keys() acfactory.remove_preconfigured_keys()
ac1 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_RSA2048)) ac1 = acfactory.new_online_configuring_account(
ac2 = acfactory.new_online_configuring_account(key_gen_type=str(const.DC_KEY_GEN_ED25519)) key_gen_type=str(const.DC_KEY_GEN_RSA2048)
)
ac2 = acfactory.new_online_configuring_account(
key_gen_type=str(const.DC_KEY_GEN_ED25519)
)
acfactory.bring_accounts_online() acfactory.bring_accounts_online()
chat = acfactory.get_accepted_chat(ac1, ac2) chat = acfactory.get_accepted_chat(ac1, ac2)
@@ -78,7 +83,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 +91,9 @@ 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
@@ -243,7 +249,9 @@ def test_mvbox_sentbox_threads(acfactory, lp):
ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=True) ac1 = acfactory.new_online_configuring_account(mvbox_move=True, sentbox_watch=True)
lp.sec("ac2: start without mvbox/sentbox threads") lp.sec("ac2: start without mvbox/sentbox threads")
ac2 = acfactory.new_online_configuring_account(mvbox_move=False, sentbox_watch=False) ac2 = acfactory.new_online_configuring_account(
mvbox_move=False, sentbox_watch=False
)
lp.sec("ac2 and ac1: waiting for configuration") lp.sec("ac2 and ac1: waiting for configuration")
acfactory.bring_accounts_online() acfactory.bring_accounts_online()
@@ -465,7 +473,10 @@ def test_moved_markseen(acfactory, lp):
ac2.mark_seen_messages([msg]) ac2.mark_seen_messages([msg])
uid = idle2.wait_for_seen() uid = idle2.wait_for_seen()
assert len([a for a in ac2.direct_imap.conn.fetch(AND(seen=True, uid=U(uid, "*")))]) == 1 assert (
len([a for a in ac2.direct_imap.conn.fetch(AND(seen=True, uid=U(uid, "*")))])
== 1
)
def test_message_override_sender_name(acfactory, lp): def test_message_override_sender_name(acfactory, lp):
@@ -689,7 +700,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 +712,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 +777,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 +806,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 +816,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 +830,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 +844,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 +858,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")
@@ -849,7 +880,9 @@ def test_dont_show_emails(acfactory, lp):
assert ac1.direct_imap.get_uid_by_message_id("spam.message@junk.org") assert ac1.direct_imap.get_uid_by_message_id("spam.message@junk.org")
ac1.stop_io() ac1.stop_io()
lp.sec("'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time") lp.sec(
"'Send out' the draft, i.e. move it to the Sent folder, and wait for DC to display it this time"
)
ac1.direct_imap.select_folder("Drafts") ac1.direct_imap.select_folder("Drafts")
uid = ac1.direct_imap.get_uid_by_message_id("aepiors@example.org") uid = ac1.direct_imap.get_uid_by_message_id("aepiors@example.org")
ac1.direct_imap.conn.move(uid, "Sent") ac1.direct_imap.conn.move(uid, "Sent")
@@ -884,7 +917,9 @@ def test_no_old_msg_is_fresh(acfactory, lp):
assert ac1.create_chat(ac2).count_fresh_messages() == 1 assert ac1.create_chat(ac2).count_fresh_messages() == 1
assert len(list(ac1.get_fresh_messages())) == 1 assert len(list(ac1.get_fresh_messages())) == 1
lp.sec("Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'") lp.sec(
"Send a message from ac1_clone to ac2 and check that ac1 marks the first message as 'noticed'"
)
ac1_clone.create_chat(ac2).send_text("Hi back") ac1_clone.create_chat(ac2).send_text("Hi back")
ev = ac1._evtracker.get_matching("DC_EVENT_MSGS_NOTICED") ev = ac1._evtracker.get_matching("DC_EVENT_MSGS_NOTICED")
@@ -1154,7 +1189,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()
@@ -1180,7 +1215,10 @@ def test_import_export_online_all(acfactory, tmpdir, data, lp):
assert len(messages) == 3 assert len(messages) == 3
assert messages[0].text == "msg1" assert messages[0].text == "msg1"
assert messages[1].filemime == "image/png" assert messages[1].filemime == "image/png"
assert os.stat(messages[1].filename).st_size == os.stat(original_image_path).st_size assert (
os.stat(messages[1].filename).st_size
== os.stat(original_image_path).st_size
)
ac.set_config("displayname", "new displayname") ac.set_config("displayname", "new displayname")
assert ac.get_config("displayname") == "new displayname" assert ac.get_config("displayname") == "new displayname"
@@ -1326,7 +1364,9 @@ def test_set_get_contact_avatar(acfactory, data, lp):
assert open(received_path, "rb").read() == open(p, "rb").read() assert open(received_path, "rb").read() == open(p, "rb").read()
lp.sec("ac2: send back message") lp.sec("ac2: send back message")
msg3 = msg2.create_chat().send_text("yes, i received your avatar -- how do you like mine?") msg3 = msg2.create_chat().send_text(
"yes, i received your avatar -- how do you like mine?"
)
assert msg3.is_encrypted() assert msg3.is_encrypted()
lp.sec("ac1: wait for receiving message and avatar from ac2") lp.sec("ac1: wait for receiving message and avatar from ac2")
@@ -1371,11 +1411,17 @@ def test_add_remove_member_remote_events(acfactory, lp):
@account_hookimpl @account_hookimpl
def ac_member_added(self, chat, contact, message): def ac_member_added(self, chat, contact, message):
in_list.put(EventHolder(action="added", chat=chat, contact=contact, message=message)) in_list.put(
EventHolder(action="added", chat=chat, contact=contact, message=message)
)
@account_hookimpl @account_hookimpl
def ac_member_removed(self, chat, contact, message): def ac_member_removed(self, chat, contact, message):
in_list.put(EventHolder(action="removed", chat=chat, contact=contact, message=message)) in_list.put(
EventHolder(
action="removed", chat=chat, contact=contact, message=message
)
)
ac2.add_account_plugin(InPlugin()) ac2.add_account_plugin(InPlugin())
@@ -1387,8 +1433,9 @@ 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(
sorted(x.addr for x in ev.chat.get_contacts()) 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
@@ -1528,10 +1575,14 @@ def test_connectivity(acfactory, lp):
ac1.start_io() ac1.start_io()
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:
@@ -1543,18 +1594,26 @@ def test_connectivity(acfactory, lp):
assert len(msgs) == 1 assert len(msgs) == 1
assert msgs[0].text == "Hi" assert msgs[0].text == "Hi"
lp.sec("Test that the connectivity changes to WORKING while new messages are fetched") lp.sec(
"Test that the connectivity changes to WORKING while new messages are fetched"
)
ac2.create_chat(ac1).send_text("Hi 2") ac2.create_chat(ac1).send_text("Hi 2")
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_CONNECTED, const.DC_CONNECTIVITY_WORKING) ac1._evtracker.wait_for_connectivity_change(
ac1._evtracker.wait_for_connectivity_change(const.DC_CONNECTIVITY_WORKING, const.DC_CONNECTIVITY_CONNECTED) const.DC_CONNECTIVITY_CONNECTED, const.DC_CONNECTIVITY_WORKING
)
ac1._evtracker.wait_for_connectivity_change(
const.DC_CONNECTIVITY_WORKING, const.DC_CONNECTIVITY_CONNECTED
)
msgs = ac1.create_chat(ac2).get_messages() msgs = ac1.create_chat(ac2).get_messages()
assert len(msgs) == 2 assert len(msgs) == 2
assert msgs[1].text == "Hi 2" assert msgs[1].text == "Hi 2"
lp.sec("Test that the connectivity doesn't flicker to WORKING if there are no new messages") lp.sec(
"Test that the connectivity doesn't flicker to WORKING if there are no new messages"
)
ac1.maybe_network() ac1.maybe_network()
while 1: while 1:
@@ -1563,7 +1622,9 @@ def test_connectivity(acfactory, lp):
break break
ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED") ac1._evtracker.get_matching("DC_EVENT_CONNECTIVITY_CHANGED")
lp.sec("Test that the connectivity doesn't flicker to WORKING if the sender of the message is blocked") lp.sec(
"Test that the connectivity doesn't flicker to WORKING if the sender of the message is blocked"
)
ac1.create_contact(ac2).block() ac1.create_contact(ac2).block()
ac1.direct_imap.select_config_folder("inbox") ac1.direct_imap.select_config_folder("inbox")
@@ -1594,10 +1655,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 +1669,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()
@@ -1626,7 +1690,10 @@ def test_fetch_deleted_msg(acfactory, lp):
if ev.name == "DC_EVENT_MSGS_CHANGED": if ev.name == "DC_EVENT_MSGS_CHANGED":
pytest.fail("A deleted message was shown to the user") pytest.fail("A deleted message was shown to the user")
if ev.name == "DC_EVENT_INFO" and "INBOX: Idle entering wait-on-remote state" in ev.data2: if (
ev.name == "DC_EVENT_INFO"
and "INBOX: Idle entering wait-on-remote state" in ev.data2
):
break # DC is done with reading messages break # DC is done with reading messages
@@ -1888,11 +1955,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,9 +2,9 @@ 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):
@@ -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)
@@ -85,19 +85,22 @@ class TestOnlineInCreation:
assert prepared_original.is_out_preparing() assert prepared_original.is_out_preparing()
shutil.copyfile(orig, path) shutil.copyfile(orig, path)
chat.send_prepared(prepared_original) chat.send_prepared(prepared_original)
assert prepared_original.is_out_pending() or prepared_original.is_out_delivered() assert (
prepared_original.is_out_pending() or prepared_original.is_out_delivered()
)
lp.sec("check that both forwarded and original message are proper.") lp.sec("check that both forwarded and original message are proper.")
wait_msgs_changed(ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.id)]) wait_msgs_changed(
ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.id)]
)
fwd_msg = ac1.get_message_by_id(forwarded_id) fwd_msg = ac1.get_message_by_id(forwarded_id)
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(
(chat2.id, forwarded_id), ac1, [(chat2.id, forwarded_id), (chat.id, prepared_original.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 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), ("Member nothing bla bla", None),
("Another unknown system message", 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
@@ -274,7 +292,11 @@ class TestOfflineChat:
assert d["archived"] == chat.is_archived() assert d["archived"] == chat.is_archived()
# assert d["param"] == chat.param # assert d["param"] == chat.param
assert d["color"] == chat.get_color() assert d["color"] == chat.get_color()
assert d["profile_image"] == "" if chat.get_profile_image() is None else chat.get_profile_image() assert (
d["profile_image"] == ""
if chat.get_profile_image() is None
else chat.get_profile_image()
)
assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft() assert d["draft"] == "" if chat.get_draft() is None else chat.get_draft()
def test_group_chat_creation_with_translation(self, ac1): def test_group_chat_creation_with_translation(self, ac1):
@@ -424,11 +446,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 +654,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")
@@ -42,7 +43,9 @@ class TestACSetup:
pc.bring_online() pc.bring_online()
assert pc._account2state[acc] == pc.IDLEREADY assert pc._account2state[acc] == pc.IDLEREADY
def test_two_accounts_one_waited_all_started(self, monkeypatch, acfactory, testprocess): def test_two_accounts_one_waited_all_started(
self, monkeypatch, acfactory, testprocess
):
pc = ACSetup(init_time=0.0, testprocess=testprocess) pc = ACSetup(init_time=0.0, testprocess=testprocess)
monkeypatch.setattr(pc, "init_imap", lambda *args, **kwargs: None) monkeypatch.setattr(pc, "init_imap", lambda *args, **kwargs: None)
monkeypatch.setattr(pc, "_onconfigure_start_io", lambda *args, **kwargs: None) monkeypatch.setattr(pc, "_onconfigure_start_io", lambda *args, **kwargs: None)
@@ -103,6 +106,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()
@@ -168,7 +172,12 @@ def test_provider_info_none():
lib.dc_context_new(ffi.NULL, ffi.NULL, ffi.NULL), lib.dc_context_new(ffi.NULL, ffi.NULL, ffi.NULL),
lib.dc_context_unref, lib.dc_context_unref,
) )
assert lib.dc_provider_new_from_email(ctx, cutil.as_dc_charpointer("email@unexistent.no")) == ffi.NULL assert (
lib.dc_provider_new_from_email(
ctx, cutil.as_dc_charpointer("email@unexistent.no")
)
== ffi.NULL
)
def test_get_info_open(tmpdir): def test_get_info_open(tmpdir):
@@ -178,8 +187,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):

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