mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 07:32:12 +03:00
refactor docs and ffi/high level event handling to pass all tests again
This commit is contained in:
@@ -1,7 +0,0 @@
|
||||
|
||||
C deltachat interface
|
||||
=====================
|
||||
|
||||
See :doc:`lapi` for accessing many of the below functions
|
||||
through the ``deltachat.capi.lib`` namespace.
|
||||
|
||||
@@ -4,8 +4,8 @@ examples
|
||||
========
|
||||
|
||||
|
||||
Playing around on the commandline
|
||||
----------------------------------
|
||||
Sending a Chat message from the command line
|
||||
---------------------------------------------
|
||||
|
||||
Once you have :doc:`installed deltachat bindings <install>`
|
||||
you can start playing from the python interpreter commandline.
|
||||
@@ -14,10 +14,10 @@ For example you can type ``python`` and then::
|
||||
# instantiate and configure deltachat account
|
||||
import deltachat
|
||||
ac = deltachat.Account("/tmp/db")
|
||||
ac.set_config("addr", "test2@hq5.merlinux.eu")
|
||||
ac.set_config("addr", "address@example.org")
|
||||
ac.set_config("mail_pwd", "some password")
|
||||
|
||||
# start the IO threads and perform configuration
|
||||
# start IO threads and perform configuration
|
||||
ac.start()
|
||||
|
||||
# create a contact and send a message
|
||||
@@ -25,6 +25,23 @@ For example you can type ``python`` and then::
|
||||
chat = ac.create_chat_by_contact(contact)
|
||||
chat.send_text("hi from the python interpreter command line")
|
||||
|
||||
# shutdown IO threads
|
||||
ac.shutdown()
|
||||
|
||||
|
||||
Checkout our :doc:`api` for the various high-level things you can do
|
||||
to send/receive messages, create contacts and chats.
|
||||
|
||||
|
||||
Receiving a Chat message from the command line
|
||||
----------------------------------------------
|
||||
|
||||
Instantiate an account and register a plugin to process
|
||||
incoming messages:
|
||||
|
||||
.. include:: ../examples/echo_and_quit.py
|
||||
:literal:
|
||||
|
||||
Checkout our :doc:`api` for the various high-level things you can do
|
||||
to send/receive messages, create contacts and chats.
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ getting started
|
||||
changelog
|
||||
api
|
||||
lapi
|
||||
plugins
|
||||
|
||||
..
|
||||
Indices and tables
|
||||
|
||||
37
python/examples/echo_and_quit.py
Normal file
37
python/examples/echo_and_quit.py
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
# instantiate and configure deltachat account
|
||||
import deltachat
|
||||
ac = deltachat.Account("/tmp/db")
|
||||
|
||||
# to see low-level events in the console uncomment the following line
|
||||
# ac.add_account_plugin(deltachat.eventlogger.FFIEventLogger(ac, ""))
|
||||
|
||||
if not ac.is_configured():
|
||||
ac.set_config("addr", "tmpy.94mtm@testrun.org")
|
||||
ac.set_config("mail_pw", "5CbD6VnjD/li")
|
||||
ac.set_config("mvbox_watch", "0")
|
||||
ac.set_config("sentbox_watch", "0")
|
||||
|
||||
class MyPlugin:
|
||||
@deltachat.hookspec.account_hookimpl
|
||||
def process_incoming_message(self, message):
|
||||
print("process_incoming message", message)
|
||||
if message.text.strip() == "/quit":
|
||||
print("shutting down")
|
||||
ac.shutdown()
|
||||
else:
|
||||
ch = ac.create_chat_by_contact(message.get_sender_contact())
|
||||
ch.send_text("echoing {}".format(message.text))
|
||||
|
||||
@deltachat.hookspec.account_hookimpl
|
||||
def process_message_delivered(self, message):
|
||||
print("process_message_delivered", message)
|
||||
|
||||
ac.add_account_plugin(MyPlugin())
|
||||
|
||||
# start IO threads and perform configuration
|
||||
ac.start()
|
||||
|
||||
print("waiting for /quit or other message on {}".format(ac.get_config("addr")))
|
||||
|
||||
ac.wait_shutdown()
|
||||
@@ -15,6 +15,7 @@ from .message import Message
|
||||
from .contact import Contact
|
||||
from .tracker import ImexTracker
|
||||
from . import hookspec, iothreads
|
||||
from queue import Queue
|
||||
|
||||
|
||||
class MissingCredentials(ValueError):
|
||||
@@ -48,6 +49,9 @@ class Account(object):
|
||||
hook.account_init(account=self, db_path=db_path)
|
||||
|
||||
self._threads = iothreads.IOThreads(self)
|
||||
self._hook_event_queue = Queue()
|
||||
self._in_use_iter_events = False
|
||||
self._shutdown_event = Event()
|
||||
|
||||
# open database
|
||||
if hasattr(db_path, "encode"):
|
||||
@@ -56,30 +60,13 @@ class Account(object):
|
||||
raise ValueError("Could not dc_open: {}".format(db_path))
|
||||
self._configkeys = self.get_config("sys.config_keys").split()
|
||||
atexit.register(self.shutdown)
|
||||
self._shutdown_event = Event()
|
||||
|
||||
@hookspec.account_hookimpl
|
||||
def process_ffi_event(self, ffi_event):
|
||||
name = ffi_event.name
|
||||
if name == "DC_EVENT_CONFIGURE_PROGRESS":
|
||||
data1 = ffi_event.data1
|
||||
if data1 == 0 or data1 == 1000:
|
||||
success = data1 == 1000
|
||||
self._pm.hook.configure_completed(success=success)
|
||||
elif name == "DC_EVENT_INCOMING_MSG":
|
||||
msg = self.get_message_by_id(ffi_event.data2)
|
||||
self._pm.hook.process_incoming_message(message=msg)
|
||||
elif name == "DC_EVENT_MSGS_CHANGED":
|
||||
if ffi_event.data2 != 0:
|
||||
msg = self.get_message_by_id(ffi_event.data2)
|
||||
self._pm.hook.process_incoming_message(message=msg)
|
||||
elif name == "DC_EVENT_MSG_DELIVERED":
|
||||
msg = self.get_message_by_id(ffi_event.data2)
|
||||
self._pm.hook.process_message_delivered(message=msg)
|
||||
elif name == "DC_EVENT_MEMBER_ADDED":
|
||||
chat = self.get_chat_by_id(ffi_event.data1)
|
||||
contact = self.get_contact_by_id(ffi_event.data2)
|
||||
self._pm.hook.member_added(chat=chat, contact=contact)
|
||||
name, kwargs = self._map_ffi_event(ffi_event)
|
||||
if name is not None:
|
||||
ev = HookEvent(self, name=name, kwargs=kwargs)
|
||||
self._hook_event_queue.put(ev)
|
||||
|
||||
# def __del__(self):
|
||||
# self.shutdown()
|
||||
@@ -547,7 +534,7 @@ class Account(object):
|
||||
""" Stop ongoing securejoin, configuration or other core jobs. """
|
||||
lib.dc_stop_ongoing_process(self._dc_context)
|
||||
|
||||
def start(self):
|
||||
def start(self, callback_thread=True):
|
||||
""" start this account (activate imap/smtp threads etc.)
|
||||
and return immediately.
|
||||
|
||||
@@ -565,7 +552,7 @@ class Account(object):
|
||||
if not self.get_config("addr") or not self.get_config("mail_pw"):
|
||||
raise MissingCredentials("addr or mail_pwd not set in config")
|
||||
lib.dc_configure(self._dc_context)
|
||||
self._threads.start()
|
||||
self._threads.start(callback_thread=callback_thread)
|
||||
|
||||
def wait_shutdown(self):
|
||||
""" wait until shutdown of this account has completed. """
|
||||
@@ -582,12 +569,51 @@ class Account(object):
|
||||
self.stop_ongoing()
|
||||
self._threads.stop(wait=False)
|
||||
lib.dc_close(dc_context)
|
||||
self._hook_event_queue.put(None)
|
||||
self._threads.stop(wait=wait) # to wait for threads
|
||||
self._dc_context = None
|
||||
atexit.unregister(self.shutdown)
|
||||
self._shutdown_event.set()
|
||||
hook = hookspec.Global._get_plugin_manager().hook
|
||||
hook.account_after_shutdown(account=self, dc_context=dc_context)
|
||||
self._shutdown_event.set()
|
||||
|
||||
def iter_events(self, timeout=None):
|
||||
""" yield hook events until shutdown.
|
||||
|
||||
It is not allowed to call iter_events() from multiple threads.
|
||||
"""
|
||||
if self._in_use_iter_events:
|
||||
raise RuntimeError("can only call iter_events() from one thread")
|
||||
self._in_use_iter_events = True
|
||||
while 1:
|
||||
event = self._hook_event_queue.get(timeout=timeout)
|
||||
if event is None:
|
||||
break
|
||||
yield event
|
||||
|
||||
def _map_ffi_event(self, ffi_event):
|
||||
name = ffi_event.name
|
||||
if name == "DC_EVENT_CONFIGURE_PROGRESS":
|
||||
data1 = ffi_event.data1
|
||||
if data1 == 0 or data1 == 1000:
|
||||
success = data1 == 1000
|
||||
return "configure_completed", dict(success=success)
|
||||
elif name == "DC_EVENT_INCOMING_MSG":
|
||||
msg = self.get_message_by_id(ffi_event.data2)
|
||||
return "process_incoming_message", dict(message=msg)
|
||||
elif name == "DC_EVENT_MSGS_CHANGED":
|
||||
if ffi_event.data2 != 0:
|
||||
msg = self.get_message_by_id(ffi_event.data2)
|
||||
if msg.is_in_fresh():
|
||||
return "process_incoming_message", dict(message=msg)
|
||||
elif name == "DC_EVENT_MSG_DELIVERED":
|
||||
msg = self.get_message_by_id(ffi_event.data2)
|
||||
return "process_message_delivered", dict(message=msg)
|
||||
elif name == "DC_EVENT_MEMBER_ADDED":
|
||||
chat = self.get_chat_by_id(ffi_event.data1)
|
||||
contact = self.get_contact_by_id(ffi_event.data2)
|
||||
return "member_added", dict(chat=chat, contact=contact)
|
||||
return None, {}
|
||||
|
||||
|
||||
def _destroy_dc_context(dc_context, dc_context_unref=lib.dc_context_unref):
|
||||
@@ -614,3 +640,17 @@ class ScannedQRCode:
|
||||
@property
|
||||
def contact_id(self):
|
||||
return self._dc_lot.id()
|
||||
|
||||
|
||||
class HookEvent:
|
||||
def __init__(self, account, name, kwargs):
|
||||
assert hasattr(account._pm.hook, name), name
|
||||
self.account = account
|
||||
self.name = name
|
||||
self.kwargs = kwargs
|
||||
|
||||
def call_hook(self):
|
||||
hook = getattr(self.account._pm.hook, self.name, None)
|
||||
if hook is None:
|
||||
raise ValueError("event_name {} unknown".format(self.name))
|
||||
return hook(**self.kwargs)
|
||||
|
||||
@@ -15,7 +15,8 @@ global_hookimpl = pluggy.HookimplMarker(_global_name)
|
||||
class PerAccount:
|
||||
""" per-Account-instance hook specifications.
|
||||
|
||||
If you write a plugin you need to implement one of the following hooks.
|
||||
Except for process_ffi_event all hooks are executed
|
||||
in the thread which calls Account.wait_shutdown().
|
||||
"""
|
||||
@classmethod
|
||||
def _make_plugin_manager(cls):
|
||||
@@ -29,6 +30,10 @@ class PerAccount:
|
||||
|
||||
ffi_event has "name", "data1", "data2" values as specified
|
||||
with `DC_EVENT_* <https://c.delta.chat/group__DC__EVENT.html>`_.
|
||||
|
||||
DANGER: this hook is executed from the callback invoked by core.
|
||||
Hook implementations need to be short running and can typically
|
||||
not call back into core because this would easily cause recursion issues.
|
||||
"""
|
||||
|
||||
@account_hookspec
|
||||
|
||||
@@ -17,13 +17,17 @@ class IOThreads:
|
||||
def is_started(self):
|
||||
return len(self._name2thread) > 0
|
||||
|
||||
def start(self):
|
||||
def start(self, callback_thread):
|
||||
assert not self.is_started()
|
||||
self._start_one_thread("inbox", self.imap_thread_run)
|
||||
self._start_one_thread("smtp", self.smtp_thread_run)
|
||||
|
||||
if callback_thread:
|
||||
self._start_one_thread("cb", self.cb_thread_run)
|
||||
|
||||
if int(self.account.get_config("mvbox_watch")):
|
||||
self._start_one_thread("mvbox", self.mvbox_thread_run)
|
||||
|
||||
if int(self.account.get_config("sentbox_watch")):
|
||||
self._start_one_thread("sentbox", self.sentbox_thread_run)
|
||||
|
||||
@@ -53,7 +57,18 @@ class IOThreads:
|
||||
lib.dc_interrupt_sentbox_idle(self._dc_context)
|
||||
if wait:
|
||||
for name, thread in self._name2thread.items():
|
||||
thread.join()
|
||||
if thread != threading.currentThread():
|
||||
thread.join()
|
||||
|
||||
def cb_thread_run(self):
|
||||
with self.log_execution("CALLBACK THREAD START"):
|
||||
it = self.account.iter_events()
|
||||
while not self._thread_quitflag:
|
||||
try:
|
||||
ev = next(it)
|
||||
except StopIteration:
|
||||
break
|
||||
ev.call_hook()
|
||||
|
||||
def imap_thread_run(self):
|
||||
with self.log_execution("INBOX THREAD START"):
|
||||
|
||||
@@ -3,12 +3,10 @@ import os
|
||||
import sys
|
||||
import pytest
|
||||
import requests
|
||||
from contextlib import contextmanager
|
||||
import time
|
||||
from . import Account, const
|
||||
from .tracker import ConfigureTracker
|
||||
from .capi import lib
|
||||
from .hookspec import account_hookimpl
|
||||
from .eventlogger import FFIEventLogger, FFIEventTracker
|
||||
from _pytest.monkeypatch import MonkeyPatch
|
||||
import tempfile
|
||||
@@ -63,9 +61,9 @@ def pytest_report_header(config, startdir):
|
||||
|
||||
cfg = config.option.liveconfig
|
||||
if cfg:
|
||||
if "#" in cfg:
|
||||
url, token = cfg.split("#", 1)
|
||||
summary.append('Liveconfig provider: {}#<token ommitted>'.format(url))
|
||||
if "?" in cfg:
|
||||
url, token = cfg.split("?", 1)
|
||||
summary.append('Liveconfig provider: {}?<token ommitted>'.format(url))
|
||||
else:
|
||||
summary.append('Liveconfig file: {}'.format(cfg))
|
||||
return summary
|
||||
@@ -263,26 +261,3 @@ def lp():
|
||||
def step(self, msg):
|
||||
print("-" * 5, "step " + msg, "-" * 5)
|
||||
return Printer()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_plugin_recorder():
|
||||
@contextmanager
|
||||
def make_plugin_recorder(account):
|
||||
class HookImpl:
|
||||
def __init__(self):
|
||||
self.calls_member_added = []
|
||||
|
||||
@account_hookimpl
|
||||
def member_added(self, chat, contact):
|
||||
self.calls_member_added.append(dict(chat=chat, contact=contact))
|
||||
|
||||
def get_first(self, name):
|
||||
val = getattr(self, "calls_" + name, None)
|
||||
if val is not None:
|
||||
return val.pop(0)
|
||||
|
||||
with account.temp_plugin(HookImpl()) as plugin:
|
||||
yield plugin
|
||||
|
||||
return make_plugin_recorder
|
||||
|
||||
@@ -170,18 +170,19 @@ class TestOfflineChat:
|
||||
else:
|
||||
pytest.fail("could not find chat")
|
||||
|
||||
def test_add_member_event(self, ac1, make_plugin_recorder):
|
||||
def test_add_member_event(self, ac1):
|
||||
chat = ac1.create_group_chat(name="title1")
|
||||
assert chat.is_group()
|
||||
# promote the chat
|
||||
chat.send_text("hello")
|
||||
contact1 = ac1.create_contact("some1@hello.com", name="some1")
|
||||
|
||||
with make_plugin_recorder(ac1) as rec:
|
||||
chat.add_contact(contact1)
|
||||
kwargs = rec.get_first("member_added")
|
||||
assert kwargs["chat"] == chat
|
||||
assert kwargs["contact"] == contact1
|
||||
chat.add_contact(contact1)
|
||||
for ev in ac1.iter_events(timeout=1):
|
||||
if ev.name == "member_added":
|
||||
assert ev.kwargs["chat"] == chat
|
||||
assert ev.kwargs["contact"] == contact1
|
||||
break
|
||||
|
||||
def test_group_chat_creation(self, ac1):
|
||||
contact1 = ac1.create_contact("some1@hello.com", name="some1")
|
||||
@@ -461,6 +462,11 @@ class TestOnlineAccount:
|
||||
ac2.create_chat_by_contact(ac2.create_contact(email=ac1.get_config("addr")))
|
||||
return chat
|
||||
|
||||
def test_double_iter_events(self, acfactory):
|
||||
ac1 = acfactory.get_one_online_account()
|
||||
with pytest.raises(RuntimeError):
|
||||
next(ac1.iter_events())
|
||||
|
||||
@pytest.mark.ignored
|
||||
def test_configure_generate_key(self, acfactory, lp):
|
||||
# A slow test which will generate new keys.
|
||||
|
||||
Reference in New Issue
Block a user