diff --git a/python/src/deltachat/account.py b/python/src/deltachat/account.py index ef3928cf..b804855e 100644 --- a/python/src/deltachat/account.py +++ b/python/src/deltachat/account.py @@ -109,6 +109,14 @@ class Account(object): self.check_is_configured() return from_dc_charpointer(lib.dc_get_info(self._dc_context)) + def get_blobdir(self): + """ return the directory for files. + + All sent files are copied to this directory if necessary. + Place files there directly to avoid copying. + """ + return from_dc_charpointer(lib.dc_get_blobdir(self._dc_context)) + def get_self_contact(self): """ return this account's identity as a :class:`deltachat.chatting.Contact`. diff --git a/python/src/deltachat/chatting.py b/python/src/deltachat/chatting.py index 8185c47e..a967fcc3 100644 --- a/python/src/deltachat/chatting.py +++ b/python/src/deltachat/chatting.py @@ -152,6 +152,40 @@ class Chat(object): msg_id = lib.dc_send_msg(self._dc_context, self.id, msg._dc_msg) return Message.from_db(self._dc_context, msg_id) + def prepare_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`. + The file must be inside the blob directory. + + :param path: path to the file. + :param mime_type: the mime-type of this file, defaults to auto-detection. + :param view_type: passed to :meth:`MessageType.new`. + :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) + + def send_prepared(self, message): + """ send a previously prepared message. + + :param message: a :class:`Message` instance previously returned by + :meth:`prepare_file`. + :raises: ValueError if message can not be sent. + :returns: a :class:`deltachat.chatting.Message` instance with updated state + """ + 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) + def get_messages(self): """ return list of messages in this chat. diff --git a/python/src/deltachat/const.py b/python/src/deltachat/const.py index 56e993a9..d94417f7 100644 --- a/python/src/deltachat/const.py +++ b/python/src/deltachat/const.py @@ -31,7 +31,9 @@ DC_STATE_UNDEFINED = 0 DC_STATE_IN_FRESH = 10 DC_STATE_IN_NOTICED = 13 DC_STATE_IN_SEEN = 16 +DC_STATE_OUT_DRAFT = 19 DC_STATE_OUT_PENDING = 20 +DC_STATE_OUT_PREPARING = 21 DC_STATE_OUT_FAILED = 24 DC_STATE_OUT_DELIVERED = 26 DC_STATE_OUT_MDN_RCVD = 28 @@ -52,6 +54,7 @@ DC_EVENT_SMTP_MESSAGE_SENT = 103 DC_EVENT_WARNING = 300 DC_EVENT_ERROR = 400 DC_EVENT_ERROR_NETWORK = 401 +DC_EVENT_ERROR_SELF_NOT_IN_GROUP = 410 DC_EVENT_MSGS_CHANGED = 2000 DC_EVENT_INCOMING_MSG = 2005 DC_EVENT_MSG_DELIVERED = 2010 @@ -66,6 +69,8 @@ DC_EVENT_SECUREJOIN_INVITER_PROGRESS = 2060 DC_EVENT_SECUREJOIN_JOINER_PROGRESS = 2061 DC_EVENT_GET_STRING = 2091 DC_EVENT_HTTP_GET = 2100 +DC_EVENT_FILE_COPIED = 2055 +DC_EVENT_IS_OFFLINE = 2081 # end const generated diff --git a/python/src/deltachat/message.py b/python/src/deltachat/message.py index 9099de2d..c7723758 100644 --- a/python/src/deltachat/message.py +++ b/python/src/deltachat/message.py @@ -232,6 +232,11 @@ class MessageState(object): """ 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). """ diff --git a/python/tests/conftest.py b/python/tests/conftest.py index 31fcb219..6efeac73 100644 --- a/python/tests/conftest.py +++ b/python/tests/conftest.py @@ -130,3 +130,10 @@ def wait_successful_IMAP_SMTP_connection(account): if evt_name == "DC_EVENT_SMTP_CONNECTED": smtp_ok = True print("** IMAP and SMTP logins successful", account) + +def wait_msgs_changed(account, chat_id, msg_id = None): + ev = account._evlogger.get_matching("DC_EVENT_MSGS_CHANGED") + assert ev[1] == chat_id + if msg_id is not None: + assert ev[2] == msg_id + return ev[2] diff --git a/python/tests/test_increation.py b/python/tests/test_increation.py new file mode 100644 index 00000000..3eca2e27 --- /dev/null +++ b/python/tests/test_increation.py @@ -0,0 +1,67 @@ +from __future__ import print_function +import pytest +import os +import shutil +from filecmp import cmp +from deltachat import const +#from datetime import datetime, timedelta +from conftest import wait_configuration_progress, wait_successful_IMAP_SMTP_connection, wait_msgs_changed + +class TestInCreation: + def test_forward_increation(self, acfactory, data, lp): + ac1 = acfactory.get_online_configuring_account() + ac2 = acfactory.get_online_configuring_account() + wait_configuration_progress(ac1, 1000) + wait_configuration_progress(ac2, 1000) + + blobdir = ac1.get_blobdir() + + c2 = ac1.create_contact(email = ac2.get_config("addr")) + chat = ac1.create_chat_by_contact(c2) + assert chat.id >= const.DC_CHAT_ID_LAST_SPECIAL + wait_msgs_changed(ac1, 0, 0) # why no chat id? + + lp.sec("create a message with a file in creation") + path = os.path.join(blobdir, "d.png") + open(path, 'a').close() + prepared_original = chat.prepare_file(path) + assert prepared_original.get_state().is_out_preparing() + + lp.sec("forward the message while still in creation") + chat2 = ac1.create_group_chat("newgroup") + chat2.add_contact(c2) + wait_msgs_changed(ac1, 0, 0) # why not chat id? + ac1.forward_messages([prepared_original], chat2) + forwarded_id = wait_msgs_changed(ac1, chat2.id) + forwarded_msg = ac1.get_message_by_id(forwarded_id) + assert forwarded_msg.get_state().is_out_preparing() + + lp.sec("finish creating the file and send it") + shutil.copy(data.get_path("d.png"), path) + sent_original = chat.send_prepared(prepared_original) + assert sent_original.id == prepared_original.id + assert sent_original.get_state().is_out_pending() + wait_msgs_changed(ac1, chat.id, sent_original.id) + + lp.sec("expect the forwarded message to be sent now too") + wait_msgs_changed(ac1, chat2.id, forwarded_id) + assert ac1.get_message_by_id(forwarded_id).get_state().is_out_pending() + + lp.sec("wait for the messages to be delivered to SMTP") + ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED") + assert ev[1] == chat.id + assert ev[2] == sent_original.id + ev = ac1._evlogger.get_matching("DC_EVENT_MSG_DELIVERED") + assert ev[1] == chat2.id + assert ev[2] == forwarded_id + + lp.sec("wait for both messages to arrive") + ev1 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED") + assert ev1[1] >= const.DC_CHAT_ID_LAST_SPECIAL + received_original = ac2.get_message_by_id(ev1[2]) + assert cmp(received_original.filename, path, False) + ev2 = ac2._evlogger.get_matching("DC_EVENT_MSGS_CHANGED") + assert ev2[1] >= const.DC_CHAT_ID_LAST_SPECIAL + assert ev2[1] != ev1[1] + received_copy = ac2.get_message_by_id(ev2[2]) + assert cmp(received_copy.filename, path, False) diff --git a/src/dc_chat.c b/src/dc_chat.c index 5bc524b3..8b168631 100644 --- a/src/dc_chat.c +++ b/src/dc_chat.c @@ -2476,6 +2476,7 @@ static uint32_t prepare_msg_common(dc_context_t* context, uint32_t chat_id, dc_m chat = dc_chat_new(context); if (dc_chat_load_from_db(chat, chat_id)) { msg->id = prepare_msg_raw(context, chat, msg, dc_create_smeared_timestamp(context), state); + msg->chat_id = chat_id; /* potential error already logged */ } @@ -2509,6 +2510,7 @@ cleanup: * dc_prepare_msg(context, chat_id, msg); * // ... after /file/to/send.mp4 is ready: * dc_send_msg(context, 0, msg); + * ~~~ * * @memberof dc_context_t * @param context The context object as returned from dc_context_new(). @@ -2564,7 +2566,7 @@ uint32_t dc_prepare_msg(dc_context_t* context, uint32_t chat_id, dc_msg_t* msg) * On succcess, msg_id of the object is set up, * The function does not take ownership of the object, * so you have to free it using dc_msg_unref() as usual. - * @return The ID of the message that is about being sent. + * @return The ID of the message that is about to be sent. */ uint32_t dc_send_msg(dc_context_t* context, uint32_t chat_id, dc_msg_t* msg) { @@ -2579,18 +2581,37 @@ uint32_t dc_send_msg(dc_context_t* context, uint32_t chat_id, dc_msg_t* msg) }; } // update message state of separately prepared messages + else if (msg->state==DC_STATE_OUT_PREPARING) { + dc_update_msg_state(context, msg->id, DC_STATE_OUT_PENDING); + } else { - sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, - "UPDATE msgs SET state=" DC_STRINGIFY(DC_STATE_OUT_PENDING) - " WHERE id=?;"); - sqlite3_bind_int(stmt, 1, msg->id); - sqlite3_step(stmt); - sqlite3_finalize(stmt); + return 0; } dc_job_add(context, DC_JOB_SEND_MSG_TO_SMTP, msg->id, NULL, 0); - context->cb(context, DC_EVENT_MSGS_CHANGED, chat_id, msg->id); + context->cb(context, DC_EVENT_MSGS_CHANGED, msg->chat_id, msg->id); + + // recursively send any forwarded copies + if (!chat_id) { + char* forwards = dc_param_get(msg->param, DC_PARAM_PREP_FORWARDS, NULL); + if (forwards) { + char* p = forwards; + while (*p) { + int32_t id = strtol(p, &p, 10); + if (!id) break; // avoid hanging if user tampers with db + dc_msg_t* copy = dc_get_msg(context, id); + if (copy) { + dc_send_msg(context, 0, copy); + } + dc_msg_unref(copy); + } + dc_param_set(msg->param, DC_PARAM_PREP_FORWARDS, NULL); + dc_msg_save_param_to_disk(msg); + } + free(forwards); + } + return msg->id; } @@ -2691,6 +2712,7 @@ void dc_forward_msgs(dc_context_t* context, const uint32_t* msg_ids, int msg_cnt char* q3 = NULL; sqlite3_stmt* stmt = NULL; time_t curr_timestamp = 0; + dc_param_t* original_param = dc_param_new(); if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || msg_ids==NULL || msg_cnt<=0 || chat_id<=DC_CHAT_ID_LAST_SPECIAL) { goto cleanup; @@ -2719,6 +2741,8 @@ void dc_forward_msgs(dc_context_t* context, const uint32_t* msg_ids, int msg_cnt goto cleanup; } + dc_param_set_packed(original_param, msg->param->packed); + // do not mark own messages as being forwarded. // this allows sort of broadcasting // by just forwarding messages to other chats. @@ -2733,10 +2757,23 @@ void dc_forward_msgs(dc_context_t* context, const uint32_t* msg_ids, int msg_cnt uint32_t new_msg_id; // PREPARING messages can't be forwarded immediately if (msg->state==DC_STATE_OUT_PREPARING) { - if (!dc_param_exists(msg->param, DC_PARAM_FWD_ORIGINAL)) { - dc_param_set_int(msg->param, DC_PARAM_FWD_ORIGINAL, src_msg_id); - } new_msg_id = prepare_msg_raw(context, chat, msg, curr_timestamp++, msg->state); + + // to update the original message, perform in-place surgery + // on msg to avoid copying the entire structure, text, etc. + dc_param_t* save_param = msg->param; + msg->param = original_param; + msg->id = src_msg_id; + { + // append new id to the original's param. + char* old_fwd = dc_param_get(msg->param, DC_PARAM_PREP_FORWARDS, ""); + char* new_fwd = dc_mprintf("%s %d", old_fwd, new_msg_id); + dc_param_set(msg->param, DC_PARAM_PREP_FORWARDS, new_fwd); + dc_msg_save_param_to_disk(msg); + free(new_fwd); + free(old_fwd); + } + msg->param = save_param; } else { new_msg_id = prepare_msg_raw(context, chat, msg, curr_timestamp++, msg->state); @@ -2765,4 +2802,5 @@ cleanup: sqlite3_finalize(stmt); free(idsstr); sqlite3_free(q3); + dc_param_unref(original_param); } diff --git a/src/dc_param.h b/src/dc_param.h index 11340926..2b309ad5 100644 --- a/src/dc_param.h +++ b/src/dc_param.h @@ -35,14 +35,14 @@ struct _dc_param #define DC_PARAM_ERRONEOUS_E2EE 'e' /* for msgs: decrypted with validation errors or without mutual set, if neither 'c' nor 'e' are preset, the messages is only transport encrypted */ #define DC_PARAM_FORCE_PLAINTEXT 'u' /* for msgs: force unencrypted message, either DC_FP_ADD_AUTOCRYPT_HEADER (1), DC_FP_NO_AUTOCRYPT_HEADER (2) or 0 */ #define DC_PARAM_WANTS_MDN 'r' /* for msgs: an incoming message which requestes a MDN (aka read receipt) */ -#define DC_PARAM_FORWARDED 'a' /* for msgs: 1 on the forwarded original */ -#define DC_PARAM_FWD_ORIGINAL 'o' /* for msgs: ID of the forwarded original on the forwarded copy */ +#define DC_PARAM_FORWARDED 'a' /* for msgs */ #define DC_PARAM_CMD 'S' /* for msgs */ #define DC_PARAM_CMD_ARG 'E' /* for msgs */ #define DC_PARAM_CMD_ARG2 'F' /* for msgs */ #define DC_PARAM_CMD_ARG3 'G' /* for msgs */ #define DC_PARAM_CMD_ARG4 'H' /* for msgs */ #define DC_PARAM_ERROR 'L' /* for msgs */ +#define DC_PARAM_PREP_FORWARDS 'P' /* for msgs in PREPARING: space-separated list of message IDs of forwarded copies */ #define DC_PARAM_SERVER_FOLDER 'Z' /* for jobs */ #define DC_PARAM_SERVER_UID 'z' /* for jobs */ diff --git a/src/deltachat.h b/src/deltachat.h index cfde65fc..8684cf53 100644 --- a/src/deltachat.h +++ b/src/deltachat.h @@ -269,6 +269,7 @@ uint32_t dc_create_chat_by_msg_id (dc_context_t*, uint32_t msg_id); uint32_t dc_create_chat_by_contact_id (dc_context_t*, uint32_t contact_id); uint32_t dc_get_chat_id_by_contact_id (dc_context_t*, uint32_t contact_id); +uint32_t dc_prepare_msg (dc_context_t*, uint32_t chat_id, dc_msg_t*); uint32_t dc_send_msg (dc_context_t*, uint32_t chat_id, dc_msg_t*); uint32_t dc_send_text_msg (dc_context_t*, uint32_t chat_id, const char* text_to_send); void dc_set_draft (dc_context_t*, uint32_t chat_id, dc_msg_t*);