- properly support prepare-msg API and implement get_message_info()

- remove usage of "attr.s" across the code
- make msg.set_file() copy a file into blobdir if it isn't already
- regroup tests
- add set_draft/get_draft API
This commit is contained in:
holger krekel
2019-07-24 16:37:24 +02:00
parent c7ebf6de09
commit 9836e73683
8 changed files with 268 additions and 152 deletions

View File

@@ -69,6 +69,10 @@ class Account(object):
d[key.lower()] = value
return d
def get_blob_dir(self):
""" return blob directory for this account. """
return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context))
def set_config(self, name, value):
""" set configuration values.
@@ -142,15 +146,6 @@ class Account(object):
self.check_is_configured()
return Contact(self._dc_context, const.DC_CONTACT_ID_SELF)
def create_message(self, view_type):
""" create a new non persistent message.
:param view_type: a string specifying "text", "video",
"image", "audio" or "file".
:returns: :class:`deltachat.message.Message` instance.
"""
return Message.new(self._dc_context, view_type)
def create_contact(self, email, name=None):
""" create a (new) Contact. If there already is a Contact
with that e-mail address, it is unblocked and its name is
@@ -212,7 +207,7 @@ class Account(object):
assert isinstance(contact, int)
contact_id = contact
chat_id = lib.dc_create_chat_by_contact_id(self._dc_context, contact_id)
return Chat(self._dc_context, chat_id)
return Chat(self, chat_id)
def create_chat_by_message(self, message):
""" create or get an existing chat object for the
@@ -229,7 +224,7 @@ class Account(object):
assert isinstance(message, int)
msg_id = message
chat_id = lib.dc_create_chat_by_msg_id(self._dc_context, msg_id)
return Chat(self._dc_context, chat_id)
return Chat(self, chat_id)
def create_group_chat(self, name, verified=False):
""" create a new group chat object.
@@ -241,7 +236,7 @@ class Account(object):
"""
bytes_name = name.encode("utf8")
chat_id = lib.dc_create_group_chat(self._dc_context, verified, bytes_name)
return Chat(self._dc_context, chat_id)
return Chat(self, chat_id)
def get_chats(self):
""" return list of chats.
@@ -257,15 +252,15 @@ class Account(object):
chatlist = []
for i in range(0, lib.dc_chatlist_get_cnt(dc_chatlist)):
chat_id = lib.dc_chatlist_get_chat_id(dc_chatlist, i)
chatlist.append(Chat(self._dc_context, chat_id))
chatlist.append(Chat(self, chat_id))
return chatlist
def get_deaddrop_chat(self):
return Chat(self._dc_context, const.DC_CHAT_ID_DEADDROP)
return Chat(self, const.DC_CHAT_ID_DEADDROP)
def get_message_by_id(self, msg_id):
""" return Message instance. """
return Message.from_db(self._dc_context, msg_id)
return Message.from_db(self, msg_id)
def mark_seen_messages(self, messages):
""" mark the given set of messages as seen.

View File

@@ -1,24 +1,30 @@
""" chatting related objects: Contact, Chat, Message. """
import os
import mimetypes
from . import props
from .cutil import as_dc_charpointer, from_dc_charpointer, iter_array
from .capi import lib, ffi
from . import const
import attr
from attr import validators as v
from .message import Message
@attr.s
class Contact(object):
""" Delta-Chat Contact.
You obtain instances of it through :class:`deltachat.account.Account`.
"""
_dc_context = attr.ib(validator=v.instance_of(ffi.CData))
id = attr.ib(validator=v.instance_of(int))
def __init__(self, dc_context, id):
self._dc_context = dc_context
self.id = id
def __eq__(self, other):
return self._dc_context == other._dc_context and self.id == other.id
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return "<Contact id={} addr={} dc_context={}>".format(self.id, self.addr, self._dc_context)
@property
def _dc_contact(self):
@@ -46,14 +52,26 @@ class Contact(object):
return lib.dc_contact_is_verified(self._dc_contact)
@attr.s
class Chat(object):
""" Chat object which manages members and through which you can send and retrieve messages.
You obtain instances of it through :class:`deltachat.account.Account`.
"""
_dc_context = attr.ib(validator=v.instance_of(ffi.CData))
id = attr.ib(validator=v.instance_of(int))
def __init__(self, account, id):
self.account = account
self._dc_context = account._dc_context
self.id = id
def __eq__(self, other):
return self.id == getattr(other, "id", None) and \
self._dc_context == getattr(other, "_dc_context", None)
def __ne__(self, other):
return not (self == other)
def __repr__(self):
return "<Chat id={} name={} dc_context={}>".format(self.id, self.get_name(), self._dc_context)
@property
def _dc_chat(self):
@@ -126,7 +144,7 @@ class Chat(object):
msg_id = lib.dc_send_text_msg(self._dc_context, self.id, msg)
if msg_id == 0:
raise ValueError("message could not be send, does chat exist?")
return Message.from_db(self._dc_context, msg_id)
return Message.from_db(self.account, msg_id)
def send_file(self, path, mime_type="application/octet-stream"):
""" send a file and return the resulting Message instance.
@@ -136,14 +154,8 @@ class Chat(object):
:raises ValueError: if message can not be send/chat does not exist.
:returns: the resulting :class:`deltachat.message.Message` instance
"""
path = as_dc_charpointer(path)
mtype = as_dc_charpointer(mime_type)
msg = Message.new(self._dc_context, "file")
msg.set_file(path, mtype)
msg_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
if msg_id == 0:
raise ValueError("message could not be send, does chat exist?")
return Message.from_db(self._dc_context, msg_id)
msg = self.prepare_message_file(path=path, mime_type=mime_type)
return self.send_prepared(msg)
def send_image(self, path):
""" send an image message and return the resulting Message instance.
@@ -152,14 +164,22 @@ class Chat(object):
:raises ValueError: if message can not be send/chat does not exist.
:returns: the resulting :class:`deltachat.message.Message` instance
"""
if not os.path.exists(path):
raise ValueError("path does not exist: {!r}".format(path))
msg = Message.new(self._dc_context, "image")
msg.set_file(path)
msg_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg)
return Message.from_db(self._dc_context, msg_id)
mime_type = mimetypes.guess_type(path)[0]
msg = self.prepare_message_file(path=path, mime_type=mime_type, view_type="image")
return self.send_prepared(msg)
def prepare_file(self, path, mime_type=None, view_type="file"):
def prepare_message(self, msg):
""" create a new message.
:param msg: the message to be prepared.
:returns: :class:`deltachat.message.Message` instance.
"""
msg_id = lib.dc_prepare_msg(self._dc_context, self.id, msg._dc_msg)
if msg_id == 0:
raise ValueError("message could not be prepared")
return Message.from_db(self.account, msg_id)
def prepare_message_file(self, path, mime_type=None, view_type="file"):
""" prepare a message for sending and return the resulting Message instance.
To actually send the message, call :meth:`send_prepared`.
@@ -171,14 +191,9 @@ class Chat(object):
:raises ValueError: if message can not be prepared/chat does not exist.
:returns: the resulting :class:`Message` instance
"""
path = as_dc_charpointer(path)
mtype = as_dc_charpointer(mime_type)
msg = Message.new(self._dc_context, view_type)
msg.set_file(path, mtype)
msg_id = lib.dc_prepare_msg(self._dc_context, self.id, msg._dc_msg)
if msg_id == 0:
raise ValueError("message could not be prepared, does chat exist?")
return Message.from_db(self._dc_context, msg_id)
msg = Message.new(self.account, view_type)
msg.set_file(path, mime_type)
return self.prepare_message(msg)
def send_prepared(self, message):
""" send a previously prepared message.
@@ -191,7 +206,30 @@ class Chat(object):
msg_id = lib.dc_send_msg(self._dc_context, 0, message._dc_msg)
if msg_id == 0:
raise ValueError("message could not be sent")
return Message.from_db(self._dc_context, msg_id)
return Message.from_db(self.account, msg_id)
def set_draft(self, message):
""" set message as draft.
:param message: a :class:`Message` instance
:returns: None
"""
if message is None:
lib.dc_set_draft(self._dc_context, self.id, ffi.NULL)
else:
lib.dc_set_draft(self._dc_context, self.id, message._dc_msg)
def get_draft(self):
""" get draft message for this chat.
:param message: a :class:`Message` instance
:returns: Message object or None (if no draft available)
"""
x = lib.dc_get_draft(self._dc_context, self.id)
if x == ffi.NULL:
return None
dc_msg = ffi.gc(x, lib.dc_msg_unref)
return Message.from_dc_msg(self.account, dc_msg)
def get_messages(self):
""" return list of messages in this chat.
@@ -202,7 +240,7 @@ class Chat(object):
lib.dc_get_chat_msgs(self._dc_context, self.id, 0, 0),
lib.dc_array_unref
)
return list(iter_array(dc_array, lambda x: Message.from_db(self._dc_context, x)))
return list(iter_array(dc_array, lambda x: Message.from_db(self.account, x)))
def count_fresh_messages(self):
""" return number of fresh messages in this chat.

View File

@@ -1,46 +1,64 @@
""" chatting related objects: Contact, Chat, Message. """
import os
import shutil
from . import props
from .cutil import from_dc_charpointer, as_dc_charpointer
from .capi import lib, ffi
from . import const
from datetime import datetime
import attr
from attr import validators as v
@attr.s
class Message(object):
""" Message object.
You obtain instances of it through :class:`deltachat.account.Account` or
:class:`deltachat.chatting.Chat`.
"""
_dc_context = attr.ib(validator=v.instance_of(ffi.CData))
try:
id = attr.ib(validator=v.instance_of((int, long)))
except NameError: # py35
id = attr.ib(validator=v.instance_of(int))
def __init__(self, account, id=None, dc_msg=None):
self.account = account
self._dc_context = account._dc_context
if dc_msg is not None:
self._cache_dc_msg = self._dc_msg_volatile = dc_msg
id = lib.dc_msg_get_id(dc_msg)
assert id is not None
self.id = id
assert isinstance(self._dc_context, ffi.CData)
assert int(id) >= 0
def __eq__(self, other):
return self.account == other.account and self.id == other.id
def __repr__(self):
return "<Message id={} dc_context={}>".format(self.id, self._dc_context)
@property
def _dc_msg(self):
if self.id > 0:
return ffi.gc(
lib.dc_get_msg(self._dc_context, self.id),
lib.dc_msg_unref
)
if not hasattr(self, "_cache_dc_msg"):
self._cache_dc_msg = ffi.gc(
lib.dc_get_msg(self._dc_context, self.id),
lib.dc_msg_unref
)
return self._cache_dc_msg
return self._dc_msg_volatile
@classmethod
def from_db(cls, _dc_context, id):
def from_db(cls, account, id):
assert hasattr(account, "_dc_context")
assert id > 0
return cls(_dc_context, id)
return cls(account, id)
@classmethod
def new(cls, dc_context, view_type):
""" create a non-persistent method. """
msg = cls(dc_context, 0)
def from_dc_msg(cls, account, dc_msg):
assert hasattr(account, "_dc_context")
return cls(account, dc_msg=dc_msg)
@classmethod
def new(cls, account, view_type):
""" create a non-persistent message. """
dc_context = account._dc_context
msg = cls(account=account, id=0)
view_type_code = MessageType.get_typecode(view_type)
msg._dc_msg_volatile = ffi.gc(
lib.dc_msg_new(dc_context, view_type_code),
@@ -62,7 +80,9 @@ class Message(object):
def set_text(self, text):
"""set text of this message. """
return lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text))
assert self.id > 0, "message not prepared"
assert self.get_state().is_out_preparing()
lib.dc_msg_set_text(self._dc_msg, as_dc_charpointer(text))
@props.with_doc
def filename(self):
@@ -70,9 +90,23 @@ class Message(object):
return from_dc_charpointer(lib.dc_msg_get_file(self._dc_msg))
def set_file(self, path, mime_type=None):
"""set file for this message. """
mtype = ffi.NULL if mime_type is None else mime_type
assert os.path.exists(path)
"""set file for this message from path and mime_type. """
mtype = ffi.NULL if mime_type is None else as_dc_charpointer(mime_type)
if not os.path.exists(path):
raise ValueError("path does not exist: {!r}".format(path))
blobdir = self.account.get_blob_dir()
if not path.startswith(blobdir):
for i in range(50):
ext = "" if i == 0 else "-" + str(i)
dest = os.path.join(blobdir, os.path.basename(path) + ext)
if os.path.exists(dest):
continue
shutil.copyfile(path, dest)
break
else:
raise ValueError("could not create blobdir-path for {}".format(path))
path = dest
assert path.startswith(blobdir), path
lib.dc_msg_set_file(self._dc_msg, as_dc_charpointer(path), mtype)
@props.with_doc
@@ -91,12 +125,20 @@ class Message(object):
:returns: a :class:`deltachat.message.MessageType` instance.
"""
assert self.id > 0
return MessageType(lib.dc_msg_get_viewtype(self._dc_msg))
def is_setup_message(self):
""" return True if this message is a setup message. """
return lib.dc_msg_is_setupmessage(self._dc_msg)
def get_message_info(self):
""" Return informational text for a single 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._dc_context, self.id))
def continue_key_transfer(self, setup_code):
""" extract key and use it as primary key for this account. """
lib.dc_continue_key_transfer(self._dc_context, self.id, as_dc_charpointer(setup_code))
@@ -144,7 +186,7 @@ class Message(object):
"""
from .chatting import Chat
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
return Chat(self._dc_context, chat_id)
return Chat(self.account, chat_id)
def get_sender_contact(self):
"""return the contact of who wrote the message.
@@ -156,10 +198,8 @@ class Message(object):
return Contact(self._dc_context, contact_id)
@attr.s
class MessageType(object):
""" DeltaChat message type, with is_* methods. """
_type = attr.ib(validator=v.instance_of(int))
_mapping = {
const.DC_MSG_TEXT: 'text',
const.DC_MSG_IMAGE: 'image',
@@ -169,6 +209,12 @@ class MessageType(object):
const.DC_MSG_FILE: 'file'
}
def __init__(self, _type):
self._type = _type
def __eq__(self, other):
return self._type == getattr(other, "_type", None)
@classmethod
def get_typecode(cls, view_type):
for code, value in cls._mapping.items():
@@ -206,15 +252,24 @@ class MessageType(object):
return self._type == const.DC_MSG_FILE
@attr.s
class MessageState(object):
""" Current Message In/Out state, updated on each call of is_* methods.
"""
message = attr.ib(validator=v.instance_of(Message))
def __init__(self, message):
self.message = message
def __eq__(self, other):
return self.message == getattr(other, "message", None)
@property
def _msgstate(self):
return lib.dc_msg_get_state(self.message._dc_msg)
if self.message.id == 0:
return lib.dc_msg_get_state(self.message._dc_msg)
dc_msg = ffi.gc(
lib.dc_get_msg(self.message._dc_context, self.message.id),
lib.dc_msg_unref
)
return lib.dc_msg_get_state(dc_msg)
def is_in_fresh(self):
""" return True if Message is incoming fresh message (un-noticed).