mirror of
https://github.com/chatmail/core.git
synced 2026-04-06 07:32:12 +03:00
- 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
326 lines
11 KiB
Python
326 lines
11 KiB
Python
""" 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
|
|
|
|
|
|
class Message(object):
|
|
""" Message object.
|
|
|
|
You obtain instances of it through :class:`deltachat.account.Account` or
|
|
:class:`deltachat.chatting.Chat`.
|
|
"""
|
|
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:
|
|
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, account, id):
|
|
assert hasattr(account, "_dc_context")
|
|
assert id > 0
|
|
return cls(account, id)
|
|
|
|
@classmethod
|
|
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),
|
|
lib.dc_msg_unref
|
|
)
|
|
return msg
|
|
|
|
def get_state(self):
|
|
""" get the message in/out state.
|
|
|
|
:returns: :class:`deltachat.message.MessageState`
|
|
"""
|
|
return MessageState(self)
|
|
|
|
@props.with_doc
|
|
def text(self):
|
|
"""unicode text of this messages (might be empty if not a text message). """
|
|
return from_dc_charpointer(lib.dc_msg_get_text(self._dc_msg))
|
|
|
|
def set_text(self, text):
|
|
"""set text of this message. """
|
|
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):
|
|
"""filename if there was an attachment, otherwise empty string. """
|
|
return from_dc_charpointer(lib.dc_msg_get_file(self._dc_msg))
|
|
|
|
def set_file(self, path, mime_type=None):
|
|
"""set file for this message 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
|
|
def basename(self):
|
|
"""basename of the attachment if it exists, otherwise empty string. """
|
|
return from_dc_charpointer(lib.dc_msg_get_filename(self._dc_msg))
|
|
|
|
@props.with_doc
|
|
def filemime(self):
|
|
"""mime type of the file (if it exists)"""
|
|
return from_dc_charpointer(lib.dc_msg_get_filemime(self._dc_msg))
|
|
|
|
@props.with_doc
|
|
def view_type(self):
|
|
"""the view type of this message.
|
|
|
|
:returns: a :class:`deltachat.message.MessageType` instance.
|
|
"""
|
|
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))
|
|
|
|
@props.with_doc
|
|
def time_sent(self):
|
|
"""UTC time when the message was sent.
|
|
|
|
:returns: naive datetime.datetime() object.
|
|
"""
|
|
ts = lib.dc_msg_get_timestamp(self._dc_msg)
|
|
return datetime.utcfromtimestamp(ts)
|
|
|
|
@props.with_doc
|
|
def time_received(self):
|
|
"""UTC time when the message was received.
|
|
|
|
:returns: naive datetime.datetime() object or None if message is an outgoing one.
|
|
"""
|
|
ts = lib.dc_msg_get_received_timestamp(self._dc_msg)
|
|
if ts:
|
|
return datetime.utcfromtimestamp(ts)
|
|
|
|
def get_mime_headers(self):
|
|
""" return mime-header object for an incoming message.
|
|
|
|
This only returns a non-None object if ``save_mime_headers``
|
|
config option was set and the message is incoming.
|
|
|
|
:returns: email-mime message object (with headers only, no body).
|
|
"""
|
|
import email.parser
|
|
mime_headers = lib.dc_get_mime_headers(self._dc_context, self.id)
|
|
if mime_headers:
|
|
s = ffi.string(mime_headers)
|
|
if isinstance(s, bytes):
|
|
s = s.decode("ascii")
|
|
return email.message_from_string(s)
|
|
|
|
@property
|
|
def chat(self):
|
|
"""chat this message was posted in.
|
|
|
|
:returns: :class:`deltachat.chatting.Chat` object
|
|
"""
|
|
from .chatting import Chat
|
|
chat_id = lib.dc_msg_get_chat_id(self._dc_msg)
|
|
return Chat(self.account, chat_id)
|
|
|
|
def get_sender_contact(self):
|
|
"""return the contact of who wrote the message.
|
|
|
|
:returns: :class:`deltachat.chatting.Contact` instance
|
|
"""
|
|
from .chatting import Contact
|
|
contact_id = lib.dc_msg_get_from_id(self._dc_msg)
|
|
return Contact(self._dc_context, contact_id)
|
|
|
|
|
|
class MessageType(object):
|
|
""" DeltaChat message type, with is_* methods. """
|
|
_mapping = {
|
|
const.DC_MSG_TEXT: 'text',
|
|
const.DC_MSG_IMAGE: 'image',
|
|
const.DC_MSG_GIF: 'gif',
|
|
const.DC_MSG_AUDIO: 'audio',
|
|
const.DC_MSG_VIDEO: 'video',
|
|
const.DC_MSG_FILE: 'file'
|
|
}
|
|
|
|
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():
|
|
if value == view_type:
|
|
return code
|
|
raise ValueError("message typecode not found for {!r}".format(view_type))
|
|
|
|
@props.with_doc
|
|
def name(self):
|
|
""" human readable type name. """
|
|
return self._mapping.get(self._type, "")
|
|
|
|
def is_text(self):
|
|
""" return True if it's a text message. """
|
|
return self._type == const.DC_MSG_TEXT
|
|
|
|
def is_image(self):
|
|
""" return True if it's an image message. """
|
|
return self._type == const.DC_MSG_IMAGE
|
|
|
|
def is_gif(self):
|
|
""" return True if it's a gif message. """
|
|
return self._type == const.DC_MSG_GIF
|
|
|
|
def is_audio(self):
|
|
""" return True if it's an audio message. """
|
|
return self._type == const.DC_MSG_AUDIO
|
|
|
|
def is_video(self):
|
|
""" return True if it's a video message. """
|
|
return self._type == const.DC_MSG_VIDEO
|
|
|
|
def is_file(self):
|
|
""" return True if it's a file message. """
|
|
return self._type == const.DC_MSG_FILE
|
|
|
|
|
|
class MessageState(object):
|
|
""" Current Message In/Out state, updated on each call of is_* methods.
|
|
"""
|
|
def __init__(self, message):
|
|
self.message = message
|
|
|
|
def __eq__(self, other):
|
|
return self.message == getattr(other, "message", None)
|
|
|
|
@property
|
|
def _msgstate(self):
|
|
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).
|
|
|
|
Fresh messages are not noticed nor seen and are typically
|
|
shown in notifications.
|
|
"""
|
|
return self._msgstate == const.DC_STATE_IN_FRESH
|
|
|
|
def is_in_noticed(self):
|
|
"""Return True if Message is incoming and noticed.
|
|
|
|
Eg. chat opened but message not yet read - noticed messages
|
|
are not counted as unread but were not marked as read nor resulted in MDNs.
|
|
"""
|
|
return self._msgstate == const.DC_STATE_IN_NOTICED
|
|
|
|
def is_in_seen(self):
|
|
"""Return True if Message is incoming, noticed and has been seen.
|
|
|
|
Eg. chat opened but message not yet read - noticed messages
|
|
are not counted as unread but were not marked as read nor resulted in MDNs.
|
|
"""
|
|
return self._msgstate == const.DC_STATE_IN_SEEN
|
|
|
|
def is_out_preparing(self):
|
|
"""Return True if Message is outgoing, but its file is being prepared.
|
|
"""
|
|
return self._msgstate == const.DC_STATE_OUT_PREPARING
|
|
|
|
def is_out_pending(self):
|
|
"""Return True if Message is outgoing, but is pending (no single checkmark).
|
|
"""
|
|
return self._msgstate == const.DC_STATE_OUT_PENDING
|
|
|
|
def is_out_failed(self):
|
|
"""Return True if Message is unrecoverably failed.
|
|
"""
|
|
return self._msgstate == const.DC_STATE_OUT_FAILED
|
|
|
|
def is_out_delivered(self):
|
|
"""Return True if Message was successfully delivered to the server (one checkmark).
|
|
|
|
Note, that already delivered messages may get into the state is_out_failed().
|
|
"""
|
|
return self._msgstate == const.DC_STATE_OUT_DELIVERED
|
|
|
|
def is_out_mdn_received(self):
|
|
"""Return True if message was marked as read by the recipient(s) (two checkmarks;
|
|
this requires goodwill on the receiver's side). If a sent message changes to this
|
|
state, you'll receive the event DC_EVENT_MSG_READ.
|
|
"""
|
|
return self._msgstate == const.DC_STATE_OUT_MDN_RCVD
|