From f7f8f9297a3efff28aa45a007517d15a1abce8da Mon Sep 17 00:00:00 2001 From: "B. Petersen" Date: Fri, 6 Jul 2018 17:19:38 +0200 Subject: [PATCH] file context functions affecting chats/msgs/contacts in the corresponing files --- cmdline/cmdline.c | 4 +- cmdline/stress.c | 10 +- src/dc_chat.c | 2100 ++++++++++++++++++++++++- src/dc_chat.h | 23 +- src/dc_chatlist.c | 95 +- src/dc_chatlist.h | 5 +- src/dc_configure.c | 6 +- src/dc_contact.c | 889 ++++++++++- src/dc_contact.h | 16 +- src/dc_context.c | 3578 +----------------------------------------- src/dc_context.h | 39 - src/dc_e2ee.c | 4 +- src/dc_hash.h | 2 +- src/dc_imex.c | 28 +- src/dc_mimeparser.c | 4 +- src/dc_msg.c | 625 +++++++- src/dc_msg.h | 20 +- src/dc_pgp.c | 4 +- src/dc_receive_imf.c | 4 +- src/dc_securejoin.c | 2 +- src/deltachat.h | 6 +- src/mrmailbox.h | 6 +- 22 files changed, 3723 insertions(+), 3747 deletions(-) diff --git a/cmdline/cmdline.c b/cmdline/cmdline.c index b8ec5333..79878433 100644 --- a/cmdline/cmdline.c +++ b/cmdline/cmdline.c @@ -687,7 +687,7 @@ char* dc_cmdline(dc_context_t* context, const char* cmdline) char* temp_name = dc_chat_get_name(chat); dc_log_info(context, 0, "%s#%i: %s [%s] [%i fresh]", chat_prefix(chat), - (int)dc_chat_get_id(chat), temp_name, temp_subtitle, (int)dc_get_fresh_msg_count(context, dc_chat_get_id(chat))); + (int)dc_chat_get_id(chat), temp_name, temp_subtitle, (int)dc_get_fresh_msg_cnt(context, dc_chat_get_id(chat))); free(temp_subtitle); free(temp_name); @@ -762,7 +762,7 @@ char* dc_cmdline(dc_context_t* context, const char* cmdline) free(drafttext); free(timestr); } - ret = dc_mprintf("%i messages.", dc_get_total_msg_count(context, dc_chat_get_id(sel_chat))); + ret = dc_mprintf("%i messages.", dc_get_msg_cnt(context, dc_chat_get_id(sel_chat))); dc_marknoticed_chat(context, dc_chat_get_id(sel_chat)); } else { diff --git a/cmdline/stress.c b/cmdline/stress.c index dcac4609..75140f4f 100644 --- a/cmdline/stress.c +++ b/cmdline/stress.c @@ -829,21 +829,21 @@ void stress_functions(dc_context_t* context) ok = dc_pgp_pk_decrypt(context, ctext_signed, ctext_signed_bytes, keyring, public_keyring/*for validate*/, 1, &plain, &plain_bytes, &valid_signatures); assert( ok && plain && plain_bytes>0 ); assert( strncmp((char*)plain, original_text, strlen(original_text))==0 ); - assert( dc_hash_count(&valid_signatures) == 1 ); + assert( dc_hash_cnt(&valid_signatures) == 1 ); free(plain); plain = NULL; dc_hash_clear(&valid_signatures); ok = dc_pgp_pk_decrypt(context, ctext_signed, ctext_signed_bytes, keyring, NULL/*for validate*/, 1, &plain, &plain_bytes, &valid_signatures); assert( ok && plain && plain_bytes>0 ); assert( strncmp((char*)plain, original_text, strlen(original_text))==0 ); - assert( dc_hash_count(&valid_signatures) == 0 ); + assert( dc_hash_cnt(&valid_signatures) == 0 ); free(plain); plain = NULL; dc_hash_clear(&valid_signatures); ok = dc_pgp_pk_decrypt(context, ctext_signed, ctext_signed_bytes, keyring, public_keyring2/*for validate*/, 1, &plain, &plain_bytes, &valid_signatures); assert( ok && plain && plain_bytes>0 ); assert( strncmp((char*)plain, original_text, strlen(original_text))==0 ); - assert( dc_hash_count(&valid_signatures) == 0 ); + assert( dc_hash_cnt(&valid_signatures) == 0 ); free(plain); plain = NULL; dc_hash_clear(&valid_signatures); @@ -851,14 +851,14 @@ void stress_functions(dc_context_t* context) ok = dc_pgp_pk_decrypt(context, ctext_signed, ctext_signed_bytes, keyring, public_keyring2/*for validate*/, 1, &plain, &plain_bytes, &valid_signatures); assert( ok && plain && plain_bytes>0 ); assert( strncmp((char*)plain, original_text, strlen(original_text))==0 ); - assert( dc_hash_count(&valid_signatures) == 1 ); + assert( dc_hash_cnt(&valid_signatures) == 1 ); free(plain); plain = NULL; dc_hash_clear(&valid_signatures); ok = dc_pgp_pk_decrypt(context, ctext_unsigned, ctext_unsigned_bytes, keyring, public_keyring/*for validate*/, 1, &plain, &plain_bytes, &valid_signatures); assert( ok && plain && plain_bytes>0 ); assert( strncmp((char*)plain, original_text, strlen(original_text))==0 ); - assert( dc_hash_count(&valid_signatures) == 0 ); + assert( dc_hash_cnt(&valid_signatures) == 0 ); free(plain); plain = NULL; dc_hash_clear(&valid_signatures); diff --git a/src/dc_chat.c b/src/dc_chat.c index 85a3069e..26c7f208 100644 --- a/src/dc_chat.c +++ b/src/dc_chat.c @@ -20,11 +20,13 @@ ******************************************************************************/ +#include #include "dc_context.h" #include "dc_job.h" #include "dc_smtp.h" #include "dc_imap.h" #include "dc_mimefactory.h" +#include "dc_apeerstate.h" #define DC_CHAT_MAGIC 0xc4a7c4a7 @@ -113,11 +115,6 @@ void dc_chat_empty(dc_chat_t* chat) } -/******************************************************************************* - * Getters - ******************************************************************************/ - - /** * Get chat ID. The chat ID is the ID under which the chat is filed in the database. * @@ -249,7 +246,7 @@ char* dc_chat_get_subtitle(const dc_chat_t* chat) } else { - cnt = dc_get_chat_contact_count(chat->context, chat->id); + cnt = dc_get_chat_contact_cnt(chat->context, chat->id); ret = dc_stock_str_repl_pl(chat->context, DC_STR_MEMBER, cnt /*SELF is included in group chats (if not removed)*/); } } @@ -412,11 +409,6 @@ int dc_chat_is_self_talk(const dc_chat_t* chat) } -/******************************************************************************* - * Misc. - ******************************************************************************/ - - int dc_chat_update_param(dc_chat_t* chat) { int success = 0; @@ -469,7 +461,7 @@ static int dc_chat_set_from_stmt(dc_chat_t* chat, sqlite3_stmt* row) else if (chat->id==DC_CHAT_ID_ARCHIVED_LINK) { free(chat->name); char* tempname = dc_stock_str(chat->context, DC_STR_ARCHIVEDCHATS); - chat->name = dc_mprintf("%s (%i)", tempname, dc_get_archived_count(chat->context)); + chat->name = dc_mprintf("%s (%i)", tempname, dc_get_archived_cnt(chat->context)); free(tempname); } else if (chat->id==DC_CHAT_ID_STARRED) { @@ -529,3 +521,2087 @@ cleanup: return success; } + +/******************************************************************************* + * Context functions to work with chats + ******************************************************************************/ + + +size_t dc_get_chat_cnt(dc_context_t* context) +{ + size_t ret = 0; + sqlite3_stmt* stmt = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || context->sql->cobj==NULL) { + goto cleanup; /* no database, no chats - this is no error (needed eg. for information) */ + } + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT COUNT(*) FROM chats WHERE id>" DC_STRINGIFY(DC_CHAT_ID_LAST_SPECIAL) " AND blocked=0;"); + if (sqlite3_step(stmt)!=SQLITE_ROW) { + goto cleanup; + } + + ret = sqlite3_column_int(stmt, 0); + +cleanup: + sqlite3_finalize(stmt); + return ret; +} + + +int dc_add_to_chat_contacts_table(dc_context_t* context, uint32_t chat_id, uint32_t contact_id) +{ + /* add a contact to a chat; the function does not check the type or if any of the record exist or are already added to the chat! */ + int ret = 0; + sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, + "INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)"); + sqlite3_bind_int(stmt, 1, chat_id); + sqlite3_bind_int(stmt, 2, contact_id); + ret = (sqlite3_step(stmt)==SQLITE_DONE)? 1 : 0; + sqlite3_finalize(stmt); + return ret; +} + + +/** + * Get chat object by a chat ID. + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id The ID of the chat to get the chat object for. + * @return A chat object of the type dc_chat_t, must be freed using dc_chat_unref() when done. + */ +dc_chat_t* dc_get_chat(dc_context_t* context, uint32_t chat_id) +{ + int success = 0; + dc_chat_t* obj = dc_chat_new(context); + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + goto cleanup; + } + + if (!dc_chat_load_from_db(obj, chat_id)) { + goto cleanup; + } + + success = 1; + +cleanup: + if (success) { + return obj; + } + else { + dc_chat_unref(obj); + return NULL; + } +} + + +/** + * Mark all messages in a chat as _noticed_. + * _Noticed_ messages are no longer _fresh_ and do not count as being unseen. + * IMAP/MDNs is not done for noticed messages. See also dc_marknoticed_contact() + * and dc_markseen_msgs() + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id The chat ID of which all messages should be marked as being noticed. + * @return None. + */ +void dc_marknoticed_chat(dc_context_t* context, uint32_t chat_id) +{ + /* marking a chat as "seen" is done by marking all fresh chat messages as "noticed" - + "noticed" messages are not counted as being unread but are still waiting for being marked as "seen" using dc_markseen_msgs() */ + sqlite3_stmt* stmt; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + return; + } + + stmt = dc_sqlite3_prepare(context->sql, + "UPDATE msgs SET state=" DC_STRINGIFY(DC_STATE_IN_NOTICED) " WHERE chat_id=? AND state=" DC_STRINGIFY(DC_STATE_IN_FRESH) ";"); + sqlite3_bind_int(stmt, 1, chat_id); + sqlite3_step(stmt); + sqlite3_finalize(stmt); +} + + +/** + * Check, if there is a normal chat with a given contact. + * To get the chat messages, use dc_get_chat_msgs(). + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param contact_id The contact ID to check. + * @return If there is a normal chat with the given contact_id, this chat_id is + * returned. If there is no normal chat with the contact_id, the function + * returns 0. + */ +uint32_t dc_get_chat_id_by_contact_id(dc_context_t* context, uint32_t contact_id) +{ + uint32_t chat_id = 0; + int chat_id_blocked = 0; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + return 0; + } + + dc_lookup_real_nchat_by_contact_id(context, contact_id, &chat_id, &chat_id_blocked); + + return chat_id_blocked? 0 : chat_id; /* from outside view, chats only existing in the deaddrop do not exist */ +} + + +uint32_t dc_get_chat_id_by_grpid(dc_context_t* context, const char* grpid, int* ret_blocked, int* ret_verified) +{ + uint32_t chat_id = 0; + sqlite3_stmt* stmt = NULL; + + if(ret_blocked) { *ret_blocked = 0; } + if(ret_verified) { *ret_verified = 0; } + + if (context==NULL || grpid==NULL) { + goto cleanup; + } + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT id, blocked, type FROM chats WHERE grpid=?;"); + sqlite3_bind_text (stmt, 1, grpid, -1, SQLITE_STATIC); + if (sqlite3_step(stmt)==SQLITE_ROW) { + chat_id = sqlite3_column_int(stmt, 0); + if(ret_blocked) { *ret_blocked = sqlite3_column_int(stmt, 1); } + if(ret_verified) { *ret_verified = (sqlite3_column_int(stmt, 2)==DC_CHAT_TYPE_VERIFIED_GROUP); } + } + +cleanup: + sqlite3_finalize(stmt); + return chat_id; +} + + +/** + * Create a normal chat with a single user. To create group chats, + * see dc_create_group_chat(). + * + * If there is already an exitant chat, this ID is returned and no new chat is + * crated. If there is no existant chat with the user, a new chat is created; + * this new chat may already contain messages, eg. from the deaddrop, to get the + * chat messages, use dc_get_chat_msgs(). + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param contact_id The contact ID to create the chat for. If there is already + * a chat with this contact, the already existing ID is returned. + * @return The created or reused chat ID on success. 0 on errors. + */ +uint32_t dc_create_chat_by_contact_id(dc_context_t* context, uint32_t contact_id) +{ + uint32_t chat_id = 0; + int chat_blocked = 0; + int send_event = 0; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + return 0; + } + + dc_lookup_real_nchat_by_contact_id(context, contact_id, &chat_id, &chat_blocked); + if (chat_id) { + if (chat_blocked) { + dc_unblock_chat(context, chat_id); /* unblock chat (typically move it from the deaddrop to view) */ + send_event = 1; + } + goto cleanup; /* success */ + } + + if (0==dc_real_contact_exists(context, contact_id) && contact_id!=DC_CONTACT_ID_SELF) { + dc_log_warning(context, 0, "Cannot create chat, contact %i does not exist.", (int)contact_id); + goto cleanup; + } + + dc_create_or_lookup_nchat_by_contact_id(context, contact_id, DC_CHAT_NOT_BLOCKED, &chat_id, NULL); + if (chat_id) { + send_event = 1; + } + + dc_scaleup_contact_origin(context, contact_id, DC_ORIGIN_CREATE_CHAT); + +cleanup: + if (send_event) { + context->cb(context, DC_EVENT_MSGS_CHANGED, 0, 0); + } + + return chat_id; +} + + +/** + * Create a normal chat or a group chat by a messages ID that comes typically + * from the deaddrop, DC_CHAT_ID_DEADDROP (1). + * + * If the given message ID already belongs to a normal chat or to a group chat, + * the chat ID of this chat is returned and no new chat is created. + * If a new chat is created, the given message ID is moved to this chat, however, + * there may be more messages moved to the chat from the deaddrop. To get the + * chat messages, use dc_get_chat_msgs(). + * + * If the user is asked before creation, he should be + * asked whether he wants to chat with the _contact_ belonging to the message; + * the group names may be really weired when take from the subject of implicit + * groups and this may look confusing. + * + * Moreover, this function also scales up the origin of the contact belonging + * to the message and, depending on the contacts origin, messages from the + * same group may be shown or not - so, all in all, it is fine to show the + * contact name only. + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param msg_id The message ID to create the chat for. + * @return The created or reused chat ID on success. 0 on errors. + */ +uint32_t dc_create_chat_by_msg_id(dc_context_t* context, uint32_t msg_id) +{ + uint32_t chat_id = 0; + int send_event = 0; + dc_msg_t* msg = dc_msg_new(); + dc_chat_t* chat = dc_chat_new(context); + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + goto cleanup; + } + + if (!dc_msg_load_from_db(msg, context, msg_id) + || !dc_chat_load_from_db(chat, msg->chat_id) + || chat->id<=DC_CHAT_ID_LAST_SPECIAL) { + goto cleanup; + } + + chat_id = chat->id; + + if (chat->blocked) { + dc_unblock_chat(context, chat->id); + send_event = 1; + } + + dc_scaleup_contact_origin(context, msg->from_id, DC_ORIGIN_CREATE_CHAT); + +cleanup: + dc_msg_unref(msg); + dc_chat_unref(chat); + if (send_event) { + context->cb(context, DC_EVENT_MSGS_CHANGED, 0, 0); + } + return chat_id; +} + + +/** + * Returns all message IDs of the given types in a chat. Typically used to show + * a gallery. The result must be dc_array_unref()'d + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id The chat ID to get all messages with media from. + * @param msg_type Specify a message type to query here, one of the DC_MSG_* constats. + * @param or_msg_type Another message type to return, one of the DC_MSG_* constats. + * The function will return both types then. 0 if you need only one. + * @return An array with messages from the given chat ID that have the wanted message types. + */ +dc_array_t* dc_get_chat_media(dc_context_t* context, uint32_t chat_id, int msg_type, int or_msg_type) +{ + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + return NULL; + } + + dc_array_t* ret = dc_array_new(context, 100); + + sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, + "SELECT id FROM msgs WHERE chat_id=? AND (type=? OR type=?) ORDER BY timestamp, id;"); + sqlite3_bind_int(stmt, 1, chat_id); + sqlite3_bind_int(stmt, 2, msg_type); + sqlite3_bind_int(stmt, 3, or_msg_type>0? or_msg_type : msg_type); + while (sqlite3_step(stmt)==SQLITE_ROW) { + dc_array_add_id(ret, sqlite3_column_int(stmt, 0)); + } + sqlite3_finalize(stmt); + + return ret; +} + + +/** + * Get next/previous message of the same type. + * Typically used to implement the "next" and "previous" buttons on a media + * player playing eg. voice messages. + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param curr_msg_id This is the current (image) message displayed. + * @param dir 1=get the next (image) message, -1=get the previous one. + * @return Returns the message ID that should be played next. The + * returned message is in the same chat as the given one and has the same type. + * Typically, this result is passed again to dc_get_next_media() + * later on the next swipe. If there is not next/previous message, the function returns 0. + */ +uint32_t dc_get_next_media(dc_context_t* context, uint32_t curr_msg_id, int dir) +{ + uint32_t ret_msg_id = 0; + dc_msg_t* msg = dc_msg_new(); + dc_array_t* list = NULL; + int i = 0; + int cnt = 0; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + goto cleanup; + } + + if (!dc_msg_load_from_db(msg, context, curr_msg_id)) { + goto cleanup; + } + + if ((list=dc_get_chat_media(context, msg->chat_id, msg->type, 0))==NULL) { + goto cleanup; + } + + cnt = dc_array_get_cnt(list); + for (i = 0; i < cnt; i++) { + if (curr_msg_id==dc_array_get_id(list, i)) + { + if (dir > 0) { + /* get the next message from the current position */ + if (i+1 < cnt) { + ret_msg_id = dc_array_get_id(list, i+1); + } + } + else if (dir < 0) { + /* get the previous message from the current position */ + if (i-1 >= 0) { + ret_msg_id = dc_array_get_id(list, i-1); + } + } + break; + } + } + + +cleanup: + dc_array_unref(list); + dc_msg_unref(msg); + return ret_msg_id; +} + + +/** + * Get contact IDs belonging to a chat. + * + * - for normal chats, the function always returns exactly one contact, + * DC_CONTACT_ID_SELF is _not_ returned. + * + * - for group chats all members are returned, DC_CONTACT_ID_SELF is returned + * explicitly as it may happen that oneself gets removed from a still existing + * group + * + * - for the deaddrop, all contacts are returned, DC_CONTACT_ID_SELF is not + * added + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id Chat ID to get the belonging contact IDs for. + * @return an array of contact IDs belonging to the chat; must be freed using dc_array_unref() when done. + */ +dc_array_t* dc_get_chat_contacts(dc_context_t* context, uint32_t chat_id) +{ + /* Normal chats do not include SELF. Group chats do (as it may happen that one is deleted from a + groupchat but the chats stays visible, moreover, this makes displaying lists easier) */ + dc_array_t* ret = dc_array_new(context, 100); + sqlite3_stmt* stmt = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + goto cleanup; + } + + if (chat_id==DC_CHAT_ID_DEADDROP) { + goto cleanup; /* we could also create a list for all contacts in the deaddrop by searching contacts belonging to chats with chats.blocked=2, however, currently this is not needed */ + } + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT cc.contact_id FROM chats_contacts cc" + " LEFT JOIN contacts c ON c.id=cc.contact_id" + " WHERE cc.chat_id=?" + " ORDER BY c.id=1, LOWER(c.name||c.addr), c.id;"); + sqlite3_bind_int(stmt, 1, chat_id); + while (sqlite3_step(stmt)==SQLITE_ROW) { + dc_array_add_id(ret, sqlite3_column_int(stmt, 0)); + } + +cleanup: + sqlite3_finalize(stmt); + return ret; +} + + +/** + * Get all message IDs belonging to a chat. + * Optionally, some special markers added to the ID-array may help to + * implement virtual lists. + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id The chat ID of which the messages IDs should be queried. + * @param flags If set to DC_GCM_ADD_DAY_MARKER, the marker DC_MSG_ID_DAYMARKER will + * be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour. + * @param marker1before An optional message ID. If set, the id DC_MSG_ID_MARKER1 will be added just + * before the given ID in the returned array. Set this to 0 if you do not want this behaviour. + * @return Array of message IDs, must be dc_array_unref()'d when no longer used. + */ +dc_array_t* dc_get_chat_msgs(dc_context_t* context, uint32_t chat_id, uint32_t flags, uint32_t marker1before) +{ + //clock_t start = clock(); + + int success = 0; + dc_array_t* ret = dc_array_new(context, 512); + sqlite3_stmt* stmt = NULL; + + uint32_t curr_id; + time_t curr_local_timestamp; + int curr_day, last_day = 0; + long cnv_to_local = dc_gm2local_offset(); + #define SECONDS_PER_DAY 86400 + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || ret==NULL) { + goto cleanup; + } + + if (chat_id==DC_CHAT_ID_DEADDROP) + { + stmt = dc_sqlite3_prepare(context->sql, + "SELECT m.id, m.timestamp" + " FROM msgs m" + " LEFT JOIN chats ON m.chat_id=chats.id" + " LEFT JOIN contacts ON m.from_id=contacts.id" + " WHERE m.from_id!=" DC_STRINGIFY(DC_CONTACT_ID_SELF) + " AND m.hidden=0 " + " AND chats.blocked=" DC_STRINGIFY(DC_CHAT_DEADDROP_BLOCKED) + " AND contacts.blocked=0" + " ORDER BY m.timestamp,m.id;"); /* the list starts with the oldest message*/ + } + else if (chat_id==DC_CHAT_ID_STARRED) + { + stmt = dc_sqlite3_prepare(context->sql, + "SELECT m.id, m.timestamp" + " FROM msgs m" + " LEFT JOIN contacts ct ON m.from_id=ct.id" + " WHERE m.starred=1 " + " AND m.hidden=0 " + " AND ct.blocked=0" + " ORDER BY m.timestamp,m.id;"); /* the list starts with the oldest message*/ + } + else + { + stmt = dc_sqlite3_prepare(context->sql, + "SELECT m.id, m.timestamp" + " FROM msgs m" + //" LEFT JOIN contacts ct ON m.from_id=ct.id" + " WHERE m.chat_id=? " + " AND m.hidden=0 " + //" AND ct.blocked=0" -- we hide blocked-contacts from starred and deaddrop, but we have to show them in groups (otherwise it may be hard to follow conversation, wa and tg do the same. however, maybe this needs discussion some time :) + " ORDER BY m.timestamp,m.id;"); /* the list starts with the oldest message*/ + sqlite3_bind_int(stmt, 1, chat_id); + } + + while (sqlite3_step(stmt)==SQLITE_ROW) + { + curr_id = sqlite3_column_int(stmt, 0); + + /* add user marker */ + if (curr_id==marker1before) { + dc_array_add_id(ret, DC_MSG_ID_MARKER1); + } + + /* add daymarker, if needed */ + if (flags&DC_GCM_ADDDAYMARKER) { + curr_local_timestamp = (time_t)sqlite3_column_int64(stmt, 1) + cnv_to_local; + curr_day = curr_local_timestamp/SECONDS_PER_DAY; + if (curr_day!=last_day) { + dc_array_add_id(ret, DC_MSG_ID_DAYMARKER); + last_day = curr_day; + } + } + + dc_array_add_id(ret, curr_id); + } + + success = 1; + +cleanup: + sqlite3_finalize(stmt); + + //dc_log_info(context, 0, "Message list for chat #%i created in %.3f ms.", chat_id, (double)(clock()-start)*1000.0/CLOCKS_PER_SEC); + + if (success) { + return ret; + } + else { + if (ret) { + dc_array_unref(ret); + } + return NULL; + } +} + + +/** + * Save a draft for a chat. + * + * To get the draft for a given chat ID, use dc_chat_get_draft(). + * + * @memberof dc_context_t + * @param context The context as created by dc_context_new(). + * @param chat_id The chat ID to save the draft for. + * @param msg The message text to save as a draft. + * @return None. + */ +void dc_set_draft(dc_context_t* context, uint32_t chat_id, const char* msg) +{ + sqlite3_stmt* stmt = NULL; + dc_chat_t* chat = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + goto cleanup; + } + + if ((chat=dc_get_chat(context, chat_id))==NULL) { + goto cleanup; + } + + if (msg && msg[0]==0) { + msg = NULL; // an empty draft is no draft + } + + if (chat->draft_text==NULL && msg==NULL + && chat->draft_timestamp==0) { + goto cleanup; // nothing to do - there is no old and no new draft + } + + if (chat->draft_timestamp && chat->draft_text && msg && strcmp(chat->draft_text, msg)==0) { + goto cleanup; // for equal texts, we do not update the timestamp + } + + // save draft in object - NULL or empty: clear draft + free(chat->draft_text); + chat->draft_text = msg? dc_strdup(msg) : NULL; + chat->draft_timestamp = msg? time(NULL) : 0; + + // save draft in database + stmt = dc_sqlite3_prepare(context->sql, + "UPDATE chats SET draft_timestamp=?, draft_txt=? WHERE id=?;"); + sqlite3_bind_int64(stmt, 1, chat->draft_timestamp); + sqlite3_bind_text (stmt, 2, chat->draft_text? chat->draft_text : "", -1, SQLITE_STATIC); + sqlite3_bind_int (stmt, 3, chat->id); + sqlite3_step(stmt); + + context->cb(context, DC_EVENT_MSGS_CHANGED, 0, 0); + +cleanup: + sqlite3_finalize(stmt); + dc_chat_unref(chat); +} + + +void dc_lookup_real_nchat_by_contact_id(dc_context_t* context, uint32_t contact_id, uint32_t* ret_chat_id, int* ret_chat_blocked) +{ + /* checks for "real" chats or self-chat */ + sqlite3_stmt* stmt = NULL; + + if (ret_chat_id) { *ret_chat_id = 0; } + if (ret_chat_blocked) { *ret_chat_blocked = 0; } + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || context->sql->cobj==NULL) { + return; /* no database, no chats - this is no error (needed eg. for information) */ + } + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT c.id, c.blocked" + " FROM chats c" + " INNER JOIN chats_contacts j ON c.id=j.chat_id" + " WHERE c.type=" DC_STRINGIFY(DC_CHAT_TYPE_SINGLE) " AND c.id>" DC_STRINGIFY(DC_CHAT_ID_LAST_SPECIAL) " AND j.contact_id=?;"); + sqlite3_bind_int(stmt, 1, contact_id); + if (sqlite3_step(stmt)==SQLITE_ROW) { + if (ret_chat_id) { *ret_chat_id = sqlite3_column_int(stmt, 0); } + if (ret_chat_blocked) { *ret_chat_blocked = sqlite3_column_int(stmt, 1); } + } + sqlite3_finalize(stmt); +} + + +void dc_create_or_lookup_nchat_by_contact_id(dc_context_t* context, uint32_t contact_id, int create_blocked, uint32_t* ret_chat_id, int* ret_chat_blocked) +{ + uint32_t chat_id = 0; + int chat_blocked = 0; + dc_contact_t* contact = NULL; + char* chat_name = NULL; + char* q = NULL; + sqlite3_stmt* stmt = NULL; + + if (ret_chat_id) { *ret_chat_id = 0; } + if (ret_chat_blocked) { *ret_chat_blocked = 0; } + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || context->sql->cobj==NULL) { + return; /* database not opened - error */ + } + + if (contact_id==0) { + return; + } + + dc_lookup_real_nchat_by_contact_id(context, contact_id, &chat_id, &chat_blocked); + if (chat_id!=0) { + if (ret_chat_id) { *ret_chat_id = chat_id; } + if (ret_chat_blocked) { *ret_chat_blocked = chat_blocked; } + return; /* soon success */ + } + + /* get fine chat name */ + contact = dc_contact_new(context); + if (!dc_contact_load_from_db(contact, context->sql, contact_id)) { + goto cleanup; + } + + chat_name = (contact->name&&contact->name[0])? contact->name : contact->addr; + + /* create chat record; the grpid is only used to make dc_sqlite3_get_rowid() work (we cannot use last_insert_id() due multi-threading) */ + q = sqlite3_mprintf("INSERT INTO chats (type, name, param, blocked, grpid) VALUES(%i, %Q, %Q, %i, %Q)", DC_CHAT_TYPE_SINGLE, chat_name, + contact_id==DC_CONTACT_ID_SELF? "K=1" : "", create_blocked, contact->addr); + assert( DC_PARAM_SELFTALK=='K'); + stmt = dc_sqlite3_prepare(context->sql, q); + if (stmt==NULL) { + goto cleanup; + } + + if (sqlite3_step(stmt)!=SQLITE_DONE) { + goto cleanup; + } + + chat_id = dc_sqlite3_get_rowid(context->sql, "chats", "grpid", contact->addr); + + sqlite3_free(q); + q = NULL; + sqlite3_finalize(stmt); + stmt = NULL; + + /* add contact IDs to the new chat record (may be replaced by dc_add_to_chat_contacts_table()) */ + q = sqlite3_mprintf("INSERT INTO chats_contacts (chat_id, contact_id) VALUES(%i, %i)", chat_id, contact_id); + stmt = dc_sqlite3_prepare(context->sql, q); + + if (sqlite3_step(stmt)!=SQLITE_DONE) { + goto cleanup; + } + + sqlite3_free(q); + q = NULL; + sqlite3_finalize(stmt); + stmt = NULL; + +cleanup: + sqlite3_free(q); + sqlite3_finalize(stmt); + dc_contact_unref(contact); + + if (ret_chat_id) { *ret_chat_id = chat_id; } + if (ret_chat_blocked) { *ret_chat_blocked = create_blocked; } +} + + +/** + * Get the total number of messages in a chat. + * + * @memberof dc_context_t + * + * @param context The context object as returned from dc_context_new(). + * @param chat_id The ID of the chat to count the messages for. + * @return Number of total messages in the given chat. 0 for errors or empty chats. + */ +int dc_get_msg_cnt(dc_context_t* context, uint32_t chat_id) +{ + int ret = 0; + sqlite3_stmt* stmt = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + goto cleanup; + } + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT COUNT(*) FROM msgs WHERE chat_id=?;"); + sqlite3_bind_int(stmt, 1, chat_id); + if (sqlite3_step(stmt)!=SQLITE_ROW) { + goto cleanup; + } + + ret = sqlite3_column_int(stmt, 0); + +cleanup: + sqlite3_finalize(stmt); + return ret; +} + + +void dc_unarchive_chat(dc_context_t* context, uint32_t chat_id) +{ + sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, + "UPDATE chats SET archived=0 WHERE id=?"); + sqlite3_bind_int (stmt, 1, chat_id); + sqlite3_step(stmt); + sqlite3_finalize(stmt); +} + + +/** + * Get the number of _fresh_ messages in a chat. Typically used to implement + * a badge with a number in the chatlist. + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id The ID of the chat to count the messages for. + * @return Number of fresh messages in the given chat. 0 for errors or if there are no fresh messages. + */ +int dc_get_fresh_msg_cnt(dc_context_t* context, uint32_t chat_id) +{ + int ret = 0; + sqlite3_stmt* stmt = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + goto cleanup; + } + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT COUNT(*) FROM msgs " + " WHERE state=" DC_STRINGIFY(DC_STATE_IN_FRESH) + " AND hidden=0 " + " AND chat_id=?;"); /* we have an index over the state-column, this should be sufficient as there are typically only few fresh messages */ + sqlite3_bind_int(stmt, 1, chat_id); + + if (sqlite3_step(stmt)!=SQLITE_ROW) { + goto cleanup; + } + + ret = sqlite3_column_int(stmt, 0); + +cleanup: + sqlite3_finalize(stmt); + return ret; +} + + +/** + * Archive or unarchive a chat. + * + * Archived chats are not included in the default chatlist returned + * by dc_get_chatlist(). Instead, if there are _any_ archived chats, + * the pseudo-chat with the chat_id DC_CHAT_ID_ARCHIVED_LINK will be added the the + * end of the chatlist. + * + * - To get a list of archived chats, use dc_get_chatlist() with the flag DC_GCL_ARCHIVED_ONLY. + * - To find out the archived state of a given chat, use dc_chat_get_archived() + * - Calling this function usually results in the event #DC_EVENT_MSGS_CHANGED + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id The ID of the chat to archive or unarchive. + * @param archive 1=archive chat, 0=unarchive chat, all other values are reserved for future use + * @return None + */ +void dc_archive_chat(dc_context_t* context, uint32_t chat_id, int archive) +{ + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL || (archive!=0 && archive!=1)) { + return; + } + + sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, + "UPDATE chats SET archived=? WHERE id=?;"); + sqlite3_bind_int (stmt, 1, archive); + sqlite3_bind_int (stmt, 2, chat_id); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + + context->cb(context, DC_EVENT_MSGS_CHANGED, 0, 0); +} + + +void dc_block_chat(dc_context_t* context, uint32_t chat_id, int new_blocking) +{ + sqlite3_stmt* stmt = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + return; + } + + stmt = dc_sqlite3_prepare(context->sql, + "UPDATE chats SET blocked=? WHERE id=?;"); + sqlite3_bind_int(stmt, 1, new_blocking); + sqlite3_bind_int(stmt, 2, chat_id); + sqlite3_step(stmt); + sqlite3_finalize(stmt); +} + + +void dc_unblock_chat(dc_context_t* context, uint32_t chat_id) +{ + dc_block_chat(context, chat_id, DC_CHAT_NOT_BLOCKED); +} + + +/** + * Delete a chat. + * + * Messages are deleted from the device and the chat database entry is deleted. + * After that, the event #DC_EVENT_MSGS_CHANGED is posted. + * + * Things that are _not_ done implicitly: + * + * - Messages are **not deleted from the server**. + * - The chat or the contact is **not blocked**, so new messages from the user/the group may appear + * and the user may create the chat again. + * - **Groups are not left** - this would + * be unexpected as (1) deleting a normal chat also does not prevent new mails + * from arriving, (2) leaving a group requires sending a message to + * all group members - esp. for groups not used for a longer time, this is + * really unexpected when deletion results in contacting all members again, + * (3) only leaving groups is also a valid usecase. + * + * To leave a chat explicitly, use dc_remove_contact_from_chat() with + * chat_id=DC_CONTACT_ID_SELF) + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id The ID of the chat to delete. + * @return None + */ +void dc_delete_chat(dc_context_t* context, uint32_t chat_id) +{ + /* Up to 2017-11-02 deleting a group also implied leaving it, see above why we have changed this. */ + int pending_transaction = 0; + dc_chat_t* obj = dc_chat_new(context); + char* q3 = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL) { + goto cleanup; + } + + if (!dc_chat_load_from_db(obj, chat_id)) { + goto cleanup; + } + + dc_sqlite3_begin_transaction(context->sql); + pending_transaction = 1; + + q3 = sqlite3_mprintf("DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=%i);", chat_id); + if (!dc_sqlite3_execute(context->sql, q3)) { + goto cleanup; + } + sqlite3_free(q3); + q3 = NULL; + + q3 = sqlite3_mprintf("DELETE FROM msgs WHERE chat_id=%i;", chat_id); + if (!dc_sqlite3_execute(context->sql, q3)) { + goto cleanup; + } + sqlite3_free(q3); + q3 = NULL; + + q3 = sqlite3_mprintf("DELETE FROM chats_contacts WHERE chat_id=%i;", chat_id); + if (!dc_sqlite3_execute(context->sql, q3)) { + goto cleanup; + } + sqlite3_free(q3); + q3 = NULL; + + q3 = sqlite3_mprintf("DELETE FROM chats WHERE id=%i;", chat_id); + if (!dc_sqlite3_execute(context->sql, q3)) { + goto cleanup; + } + sqlite3_free(q3); + q3 = NULL; + + dc_sqlite3_commit(context->sql); + pending_transaction = 0; + + context->cb(context, DC_EVENT_MSGS_CHANGED, 0, 0); + +cleanup: + if (pending_transaction) { dc_sqlite3_rollback(context->sql); } + dc_chat_unref(obj); + sqlite3_free(q3); +} + + +/******************************************************************************* + * Handle Group Chats + ******************************************************************************/ + + +#define IS_SELF_IN_GROUP (dc_is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF)==1) +#define DO_SEND_STATUS_MAILS (dc_param_get_int(chat->param, DC_PARAM_UNPROMOTED, 0)==0) + + +int dc_is_group_explicitly_left(dc_context_t* context, const char* grpid) +{ + sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, "SELECT id FROM leftgrps WHERE grpid=?;"); + sqlite3_bind_text (stmt, 1, grpid, -1, SQLITE_STATIC); + int ret = (sqlite3_step(stmt)==SQLITE_ROW); + sqlite3_finalize(stmt); + return ret; +} + + +void dc_set_group_explicitly_left(dc_context_t* context, const char* grpid) +{ + if (!dc_is_group_explicitly_left(context, grpid)) + { + sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, "INSERT INTO leftgrps (grpid) VALUES(?);"); + sqlite3_bind_text (stmt, 1, grpid, -1, SQLITE_STATIC); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + } +} + + +static int dc_real_group_exists(dc_context_t* context, uint32_t chat_id) +{ + // check if a group or a verified group exists under the given ID + sqlite3_stmt* stmt = NULL; + int ret = 0; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || context->sql->cobj==NULL + || chat_id<=DC_CHAT_ID_LAST_SPECIAL) { + return 0; + } + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT id FROM chats " + " WHERE id=? " + " AND (type=" DC_STRINGIFY(DC_CHAT_TYPE_GROUP) " OR type=" DC_STRINGIFY(DC_CHAT_TYPE_VERIFIED_GROUP) ");"); + sqlite3_bind_int(stmt, 1, chat_id); + if (sqlite3_step(stmt)==SQLITE_ROW) { + ret = 1; + } + sqlite3_finalize(stmt); + + return ret; +} + + +/** + * Create a new group chat. + * + * After creation, the group has one member with the + * ID DC_CONTACT_ID_SELF and is in _unpromoted_ state. This means, you can + * add or remove members, change the name, the group image and so on without + * messages being sent to all group members. + * + * This changes as soon as the first message is sent to the group members and + * the group becomes _promoted_. After that, all changes are synced with all + * group members by sending status message. + * + * To check, if a chat is still unpromoted, you dc_chat_is_unpromoted() + * + * @memberof dc_context_t + * @param context The context as created by dc_context_new(). + * @param verified If set to 1 the function creates a secure verfied group. + * Only secure-verified members are allowd in these groups and end-to-end-encryption is always enabled. + * @param chat_name The name of the group chat to create. + * The name may be changed later using dc_set_chat_name(). + * To find out the name of a group later, see dc_chat_get_name() + * @return The chat ID of the new group chat, 0 on errors. + */ +uint32_t dc_create_group_chat(dc_context_t* context, int verified, const char* chat_name) +{ + uint32_t chat_id = 0; + char* draft_txt = NULL; + char* grpid = NULL; + sqlite3_stmt* stmt = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_name==NULL || chat_name[0]==0) { + return 0; + } + + draft_txt = dc_stock_str_repl_string(context, DC_STR_NEWGROUPDRAFT, chat_name); + grpid = dc_create_id(); + + stmt = dc_sqlite3_prepare(context->sql, + "INSERT INTO chats (type, name, draft_timestamp, draft_txt, grpid, param) VALUES(?, ?, ?, ?, ?, 'U=1');" /*U=DC_PARAM_UNPROMOTED*/); + sqlite3_bind_int (stmt, 1, verified? DC_CHAT_TYPE_VERIFIED_GROUP : DC_CHAT_TYPE_GROUP); + sqlite3_bind_text (stmt, 2, chat_name, -1, SQLITE_STATIC); + sqlite3_bind_int64(stmt, 3, time(NULL)); + sqlite3_bind_text (stmt, 4, draft_txt, -1, SQLITE_STATIC); + sqlite3_bind_text (stmt, 5, grpid, -1, SQLITE_STATIC); + if ( sqlite3_step(stmt)!=SQLITE_DONE) { + goto cleanup; + } + + if ((chat_id=dc_sqlite3_get_rowid(context->sql, "chats", "grpid", grpid))==0) { + goto cleanup; + } + + if (dc_add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF)) { + goto cleanup; + } + +cleanup: + sqlite3_finalize(stmt); + free(draft_txt); + free(grpid); + + if (chat_id) { + context->cb(context, DC_EVENT_MSGS_CHANGED, 0, 0); + } + + return chat_id; +} + + +/** + * Set group name. + * + * If the group is already _promoted_ (any message was sent to the group), + * all group members are informed by a special status message that is sent automatically by this function. + * + * Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent. + * + * @memberof dc_context_t + * @param chat_id The chat ID to set the name for. Must be a group chat. + * @param new_name New name of the group. + * @param context The context as created by dc_context_new(). + * @return 1=success, 0=error + */ +int dc_set_chat_name(dc_context_t* context, uint32_t chat_id, const char* new_name) +{ + /* the function only sets the names of group chats; normal chats get their names from the contacts */ + int success = 0; + dc_chat_t* chat = dc_chat_new(context); + dc_msg_t* msg = dc_msg_new(); + char* q3 = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || new_name==NULL || new_name[0]==0 || chat_id<=DC_CHAT_ID_LAST_SPECIAL) { + goto cleanup; + } + + if (0==dc_real_group_exists(context, chat_id) + || 0==dc_chat_load_from_db(chat, chat_id)) { + goto cleanup; + } + + if (strcmp(chat->name, new_name)==0) { + success = 1; + goto cleanup; /* name not modified */ + } + + if (!IS_SELF_IN_GROUP) { + dc_log_error(context, DC_ERROR_SELF_NOT_IN_GROUP, NULL); + goto cleanup; /* we shoud respect this - whatever we send to the group, it gets discarded anyway! */ + } + + q3 = sqlite3_mprintf("UPDATE chats SET name=%Q WHERE id=%i;", new_name, chat_id); + if (!dc_sqlite3_execute(context->sql, q3)) { + goto cleanup; + } + + /* send a status mail to all group members, also needed for outself to allow multi-client */ + if (DO_SEND_STATUS_MAILS) + { + msg->type = DC_MSG_TEXT; + msg->text = dc_stock_str_repl_string2(context, DC_STR_MSGGRPNAME, chat->name, new_name); + dc_param_set_int(msg->param, DC_PARAM_CMD, DC_CMD_GROUPNAME_CHANGED); + msg->id = dc_send_msg_object(context, chat_id, msg); + context->cb(context, DC_EVENT_MSGS_CHANGED, chat_id, msg->id); + } + context->cb(context, DC_EVENT_CHAT_MODIFIED, chat_id, 0); + + success = 1; + +cleanup: + sqlite3_free(q3); + dc_chat_unref(chat); + dc_msg_unref(msg); + return success; +} + + +/** + * Set group profile image. + * + * If the group is already _promoted_ (any message was sent to the group), + * all group members are informed by a special status message that is sent automatically by this function. + * + * Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent. + * + * To find out the profile image of a chat, use dc_chat_get_profile_image() + * + * @memberof dc_context_t + * @param context The context as created by dc_context_new(). + * @param chat_id The chat ID to set the image for. + * @param new_image Full path of the image to use as the group image. If you pass NULL here, + * the group image is deleted (for promoted groups, all members are informed about this change anyway). + * @return 1=success, 0=error + */ +int dc_set_chat_profile_image(dc_context_t* context, uint32_t chat_id, const char* new_image /*NULL=remove image*/) +{ + int success = 0; + dc_chat_t* chat = dc_chat_new(context); + dc_msg_t* msg = dc_msg_new(); + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL) { + goto cleanup; + } + + if (0==dc_real_group_exists(context, chat_id) + || 0==dc_chat_load_from_db(chat, chat_id)) { + goto cleanup; + } + + if (!IS_SELF_IN_GROUP) { + dc_log_error(context, DC_ERROR_SELF_NOT_IN_GROUP, NULL); + goto cleanup; /* we shoud respect this - whatever we send to the group, it gets discarded anyway! */ + } + + dc_param_set(chat->param, DC_PARAM_PROFILE_IMAGE, new_image/*may be NULL*/); + if (!dc_chat_update_param(chat)) { + goto cleanup; + } + + /* send a status mail to all group members, also needed for outself to allow multi-client */ + if (DO_SEND_STATUS_MAILS) + { + dc_param_set_int(msg->param, DC_PARAM_CMD, DC_CMD_GROUPIMAGE_CHANGED); + dc_param_set (msg->param, DC_PARAM_CMD_ARG, new_image); + msg->type = DC_MSG_TEXT; + msg->text = dc_stock_str(context, new_image? DC_STR_MSGGRPIMGCHANGED : DC_STR_MSGGRPIMGDELETED); + msg->id = dc_send_msg_object(context, chat_id, msg); + context->cb(context, DC_EVENT_MSGS_CHANGED, chat_id, msg->id); + } + context->cb(context, DC_EVENT_CHAT_MODIFIED, chat_id, 0); + + success = 1; + +cleanup: + dc_chat_unref(chat); + dc_msg_unref(msg); + return success; +} + + +int dc_get_chat_contact_cnt(dc_context_t* context, uint32_t chat_id) +{ + int ret = 0; + sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, + "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=?;"); + sqlite3_bind_int(stmt, 1, chat_id); + if (sqlite3_step(stmt)==SQLITE_ROW) { + ret = sqlite3_column_int(stmt, 0); + } + sqlite3_finalize(stmt); + return ret; +} + + +/** + * Check if a given contact ID is a member of a group chat. + * + * @memberof dc_context_t + * @param context The context as created by dc_context_new(). + * @param chat_id The chat ID to check. + * @param contact_id The contact ID to check. To check if yourself is member + * of the chat, pass DC_CONTACT_ID_SELF (1) here. + * @return 1=contact ID is member of chat ID, 0=contact is not in chat + */ +int dc_is_contact_in_chat(dc_context_t* context, uint32_t chat_id, uint32_t contact_id) +{ + /* this function works for group and for normal chats, however, it is more useful for group chats. + DC_CONTACT_ID_SELF may be used to check, if the user itself is in a group chat (DC_CONTACT_ID_SELF is not added to normal chats) */ + int ret = 0; + sqlite3_stmt* stmt = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + goto cleanup; + } + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT contact_id FROM chats_contacts WHERE chat_id=? AND contact_id=?;"); + sqlite3_bind_int(stmt, 1, chat_id); + sqlite3_bind_int(stmt, 2, contact_id); + ret = (sqlite3_step(stmt)==SQLITE_ROW)? 1 : 0; + +cleanup: + sqlite3_finalize(stmt); + return ret; +} + + +int dc_add_contact_to_chat_ex(dc_context_t* context, uint32_t chat_id, uint32_t contact_id, int flags) +{ + int success = 0; + dc_contact_t* contact = dc_get_contact(context, contact_id); + dc_apeerstate_t* peerstate = dc_apeerstate_new(context); + dc_chat_t* chat = dc_chat_new(context); + dc_msg_t* msg = dc_msg_new(); + char* self_addr = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || contact==NULL || chat_id<=DC_CHAT_ID_LAST_SPECIAL) { + goto cleanup; + } + + if (0==dc_real_group_exists(context, chat_id) /*this also makes sure, not contacts are added to special or normal chats*/ + || (0==dc_real_contact_exists(context, contact_id) && contact_id!=DC_CONTACT_ID_SELF) + || 0==dc_chat_load_from_db(chat, chat_id)) { + goto cleanup; + } + + if (!IS_SELF_IN_GROUP) { + dc_log_error(context, DC_ERROR_SELF_NOT_IN_GROUP, NULL); + goto cleanup; /* we shoud respect this - whatever we send to the group, it gets discarded anyway! */ + } + + if ((flags&DC_FROM_HANDSHAKE) && dc_param_get_int(chat->param, DC_PARAM_UNPROMOTED, 0)==1) { + // after a handshake, force sending the `Chat-Group-Member-Added` message + dc_param_set(chat->param, DC_PARAM_UNPROMOTED, NULL); + dc_chat_update_param(chat); + } + + self_addr = dc_sqlite3_get_config(context->sql, "configured_addr", ""); + if (strcasecmp(contact->addr, self_addr)==0) { + goto cleanup; /* ourself is added using DC_CONTACT_ID_SELF, do not add it explicitly. if SELF is not in the group, members cannot be added at all. */ + } + + if (dc_is_contact_in_chat(context, chat_id, contact_id)) + { + if (!(flags&DC_FROM_HANDSHAKE)) { + success = 1; + goto cleanup; + } + // else continue and send status mail + } + else + { + if (chat->type==DC_CHAT_TYPE_VERIFIED_GROUP) + { + if (!dc_apeerstate_load_by_addr(peerstate, context->sql, contact->addr) + || dc_contact_n_peerstate_are_verified(contact, peerstate)!=DC_BIDIRECT_VERIFIED) { + dc_log_error(context, 0, "Only bidirectional verified contacts can be added to verfied groups."); + goto cleanup; + } + } + + if (0==dc_add_to_chat_contacts_table(context, chat_id, contact_id)) { + goto cleanup; + } + } + + /* send a status mail to all group members */ + if (DO_SEND_STATUS_MAILS) + { + msg->type = DC_MSG_TEXT; + msg->text = dc_stock_str_repl_string(context, DC_STR_MSGADDMEMBER, (contact->authname&&contact->authname[0])? contact->authname : contact->addr); + dc_param_set_int(msg->param, DC_PARAM_CMD, DC_CMD_MEMBER_ADDED_TO_GROUP); + dc_param_set (msg->param, DC_PARAM_CMD_ARG, contact->addr); + dc_param_set_int(msg->param, DC_PARAM_CMD_ARG2, flags); // combine the Secure-Join protocol headers with the Chat-Group-Member-Added header + msg->id = dc_send_msg_object(context, chat_id, msg); + context->cb(context, DC_EVENT_MSGS_CHANGED, chat_id, msg->id); + } + context->cb(context, DC_EVENT_CHAT_MODIFIED, chat_id, 0); + + success = 1; + +cleanup: + dc_chat_unref(chat); + dc_contact_unref(contact); + dc_apeerstate_unref(peerstate); + dc_msg_unref(msg); + free(self_addr); + return success; +} + + +/** + * Add a member to a group. + * + * If the group is already _promoted_ (any message was sent to the group), + * all group members are informed by a special status message that is sent automatically by this function. + * + * If the group is a verified group, only verified contacts can be added to the group. + * + * Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent. + * + * @memberof dc_context_t + * @param context The context as created by dc_context_new(). + * @param chat_id The chat ID to add the contact to. Must be a group chat. + * @param contact_id The contact ID to add to the chat. + * @return 1=member added to group, 0=error + */ +int dc_add_contact_to_chat(dc_context_t* context, uint32_t chat_id, uint32_t contact_id /*may be DC_CONTACT_ID_SELF*/) +{ + return dc_add_contact_to_chat_ex(context, chat_id, contact_id, 0); +} + + +/** + * Remove a member from a group. + * + * If the group is already _promoted_ (any message was sent to the group), + * all group members are informed by a special status message that is sent automatically by this function. + * + * Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent. + * + * @memberof dc_context_t + * @param context The context as created by dc_context_new(). + * @param chat_id The chat ID to remove the contact from. Must be a group chat. + * @param contact_id The contact ID to remove from the chat. + * @return 1=member removed from group, 0=error + */ +int dc_remove_contact_from_chat(dc_context_t* context, uint32_t chat_id, uint32_t contact_id /*may be DC_CONTACT_ID_SELF*/) +{ + int success = 0; + dc_contact_t* contact = dc_get_contact(context, contact_id); + dc_chat_t* chat = dc_chat_new(context); + dc_msg_t* msg = dc_msg_new(); + char* q3 = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL || (contact_id<=DC_CONTACT_ID_LAST_SPECIAL && contact_id!=DC_CONTACT_ID_SELF)) { + goto cleanup; /* we do not check if "contact_id" exists but just delete all records with the id from chats_contacts */ + } /* this allows to delete pending references to deleted contacts. Of course, this should _not_ happen. */ + + if (0==dc_real_group_exists(context, chat_id) + || 0==dc_chat_load_from_db(chat, chat_id)) { + goto cleanup; + } + + if (!IS_SELF_IN_GROUP) { + dc_log_error(context, DC_ERROR_SELF_NOT_IN_GROUP, NULL); + goto cleanup; /* we shoud respect this - whatever we send to the group, it gets discarded anyway! */ + } + + /* send a status mail to all group members - we need to do this before we update the database - + otherwise the !IS_SELF_IN_GROUP__-check in dc_chat_send_msg() will fail. */ + if (contact) + { + if (DO_SEND_STATUS_MAILS) + { + msg->type = DC_MSG_TEXT; + if (contact->id==DC_CONTACT_ID_SELF) { + dc_set_group_explicitly_left(context, chat->grpid); + msg->text = dc_stock_str(context, DC_STR_MSGGROUPLEFT); + } + else { + msg->text = dc_stock_str_repl_string(context, DC_STR_MSGDELMEMBER, (contact->authname&&contact->authname[0])? contact->authname : contact->addr); + } + dc_param_set_int(msg->param, DC_PARAM_CMD, DC_CMD_MEMBER_REMOVED_FROM_GROUP); + dc_param_set (msg->param, DC_PARAM_CMD_ARG, contact->addr); + msg->id = dc_send_msg_object(context, chat_id, msg); + context->cb(context, DC_EVENT_MSGS_CHANGED, chat_id, msg->id); + } + } + + q3 = sqlite3_mprintf("DELETE FROM chats_contacts WHERE chat_id=%i AND contact_id=%i;", chat_id, contact_id); + if (!dc_sqlite3_execute(context->sql, q3)) { + goto cleanup; + } + + context->cb(context, DC_EVENT_CHAT_MODIFIED, chat_id, 0); + + success = 1; + +cleanup: + sqlite3_free(q3); + dc_chat_unref(chat); + dc_contact_unref(contact); + dc_msg_unref(msg); + return success; +} + + +/******************************************************************************* + * Sending messages + ******************************************************************************/ + + +static int last_msg_in_chat_encrypted(dc_sqlite3_t* sql, uint32_t chat_id) +{ + int last_is_encrypted = 0; + sqlite3_stmt* stmt = dc_sqlite3_prepare(sql, + "SELECT param " + " FROM msgs " + " WHERE timestamp=(SELECT MAX(timestamp) FROM msgs WHERE chat_id=?) " + " ORDER BY id DESC;"); + sqlite3_bind_int(stmt, 1, chat_id); + if (sqlite3_step(stmt)==SQLITE_ROW) { + dc_param_t* msg_param = dc_param_new(); + dc_param_set_packed(msg_param, (char*)sqlite3_column_text(stmt, 0)); + if (dc_param_exists(msg_param, DC_PARAM_GUARANTEE_E2EE)) { + last_is_encrypted = 1; + } + dc_param_unref(msg_param); + } + sqlite3_finalize(stmt); + return last_is_encrypted; +} + + +static uint32_t dc_send_msg_raw(dc_context_t* context, dc_chat_t* chat, const dc_msg_t* msg, time_t timestamp) +{ + char* rfc724_mid = NULL; + sqlite3_stmt* stmt = NULL; + uint32_t msg_id = 0; + uint32_t to_id = 0; + + if (!DC_CHAT_TYPE_CAN_SEND(chat->type)) { + dc_log_error(context, 0, "Cannot send to chat type #%i.", chat->type); + goto cleanup; + } + + if (DC_CHAT_TYPE_IS_MULTI(chat->type) && !dc_is_contact_in_chat(context, chat->id, DC_CONTACT_ID_SELF)) { + dc_log_error(context, DC_ERROR_SELF_NOT_IN_GROUP, NULL); + goto cleanup; + } + + { + char* from = dc_sqlite3_get_config(context->sql, "configured_addr", NULL); + if (from==NULL) { + dc_log_error(context, 0, "Cannot send message, not configured."); + goto cleanup; + } + rfc724_mid = dc_create_outgoing_rfc724_mid(DC_CHAT_TYPE_IS_MULTI(chat->type)? chat->grpid : NULL, from); + free(from); + } + + if (chat->type==DC_CHAT_TYPE_SINGLE) + { + stmt = dc_sqlite3_prepare(context->sql, + "SELECT contact_id FROM chats_contacts WHERE chat_id=?;"); + sqlite3_bind_int(stmt, 1, chat->id); + if (sqlite3_step(stmt)!=SQLITE_ROW) { + dc_log_error(context, 0, "Cannot send message, contact for chat #%i not found.", chat->id); + goto cleanup; + } + to_id = sqlite3_column_int(stmt, 0); + sqlite3_finalize(stmt); + stmt = NULL; + } + else if (DC_CHAT_TYPE_IS_MULTI(chat->type)) + { + if (dc_param_get_int(chat->param, DC_PARAM_UNPROMOTED, 0)==1) { + /* mark group as being no longer unpromoted */ + dc_param_set(chat->param, DC_PARAM_UNPROMOTED, NULL); + dc_chat_update_param(chat); + } + } + + /* check if we can guarantee E2EE for this message. If we can, we won't send the message without E2EE later (because of a reset, changed settings etc. - messages may be delayed significally if there is no network present) */ + int do_guarantee_e2ee = 0; + if (context->e2ee_enabled && dc_param_get_int(msg->param, DC_PARAM_FORCE_PLAINTEXT, 0)==0) + { + int can_encrypt = 1, all_mutual = 1; /* be optimistic */ + stmt = dc_sqlite3_prepare(context->sql, + "SELECT ps.prefer_encrypted " + " FROM chats_contacts cc " + " LEFT JOIN contacts c ON cc.contact_id=c.id " + " LEFT JOIN acpeerstates ps ON c.addr=ps.addr " + " WHERE cc.chat_id=? " /* take care that this statement returns NULL rows if there is no peerstates for a chat member! */ + " AND cc.contact_id>" DC_STRINGIFY(DC_CONTACT_ID_LAST_SPECIAL) ";"); /* for DC_PARAM_SELFTALK this statement does not return any row */ + sqlite3_bind_int(stmt, 1, chat->id); + while (sqlite3_step(stmt)==SQLITE_ROW) + { + if (sqlite3_column_type(stmt, 0)==SQLITE_NULL) { + can_encrypt = 0; + all_mutual = 0; + } + else { + /* the peerstate exist, so we have either public_key or gossip_key and can encrypt potentially */ + int prefer_encrypted = sqlite3_column_int(stmt, 0); + if (prefer_encrypted!=DC_PE_MUTUAL) { + all_mutual = 0; + } + } + } + sqlite3_finalize(stmt); + stmt = NULL; + + if (can_encrypt) + { + if (all_mutual) { + do_guarantee_e2ee = 1; + } + else { + if (last_msg_in_chat_encrypted(context->sql, chat->id)) { + do_guarantee_e2ee = 1; + } + } + } + } + + if (do_guarantee_e2ee) { + dc_param_set_int(msg->param, DC_PARAM_GUARANTEE_E2EE, 1); + } + dc_param_set(msg->param, DC_PARAM_ERRONEOUS_E2EE, NULL); /* reset eg. on forwarding */ + + /* add message to the database */ + stmt = dc_sqlite3_prepare(context->sql, + "INSERT INTO msgs (rfc724_mid,chat_id,from_id,to_id, timestamp,type,state, txt,param,hidden) VALUES (?,?,?,?, ?,?,?, ?,?,?);"); + sqlite3_bind_text (stmt, 1, rfc724_mid, -1, SQLITE_STATIC); + sqlite3_bind_int (stmt, 2, chat->id); + sqlite3_bind_int (stmt, 3, DC_CONTACT_ID_SELF); + sqlite3_bind_int (stmt, 4, to_id); + sqlite3_bind_int64(stmt, 5, timestamp); + sqlite3_bind_int (stmt, 6, msg->type); + sqlite3_bind_int (stmt, 7, DC_STATE_OUT_PENDING); + sqlite3_bind_text (stmt, 8, msg->text? msg->text : "", -1, SQLITE_STATIC); + sqlite3_bind_text (stmt, 9, msg->param->packed, -1, SQLITE_STATIC); + sqlite3_bind_int (stmt, 10, msg->hidden); + if (sqlite3_step(stmt)!=SQLITE_DONE) { + dc_log_error(context, 0, "Cannot send message, cannot insert to database.", chat->id); + goto cleanup; + } + + msg_id = dc_sqlite3_get_rowid(context->sql, "msgs", "rfc724_mid", rfc724_mid); + dc_job_add(context, DC_JOB_SEND_MSG_TO_SMTP, msg_id, NULL, 0); + +cleanup: + free(rfc724_mid); + sqlite3_finalize(stmt); + return msg_id; +} + + +/** + * Send a message of any type to a chat. The given message object is not unref'd + * by the function but some fields are set up. + * + * Sends the event #DC_EVENT_MSGS_CHANGED on succcess. + * However, this does not imply, the message really reached the recipient - + * sending may be delayed eg. due to network problems. However, from your + * view, you're done with the message. Sooner or later it will find its way. + * + * To send a simple text message, you can also use dc_send_text_msg() + * which is easier to use. + * + * @private @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id Chat ID to send the message to. + * @param msg Message object to send to the chat defined by the chat ID. + * 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. + */ +uint32_t dc_send_msg_object(dc_context_t* context, uint32_t chat_id, dc_msg_t* msg) +{ + char* pathNfilename = NULL; + dc_chat_t* chat = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || msg==NULL || chat_id<=DC_CHAT_ID_LAST_SPECIAL) { + return 0; + } + + msg->id = 0; + msg->context = context; + + if (msg->type==DC_MSG_TEXT) + { + ; /* the caller should check if the message text is empty */ + } + else if (DC_MSG_NEEDS_ATTACHMENT(msg->type)) + { + pathNfilename = dc_param_get(msg->param, DC_PARAM_FILE, NULL); + if (pathNfilename) + { + /* Got an attachment. Take care, the file may not be ready in this moment! + This is useful eg. if a video should be sent and already shown as "being processed" in the chat. + In this case, the user should create an `.increation`; when the file is deleted later on, the message is sent. + (we do not use a state in the database as this would make eg. forwarding such messages much more complicated) */ + + if (msg->type==DC_MSG_FILE || msg->type==DC_MSG_IMAGE) + { + /* Correct the type, take care not to correct already very special formats as GIF or VOICE. + Typical conversions: + - from FILE to AUDIO/VIDEO/IMAGE + - from FILE/IMAGE to GIF */ + int better_type = 0; + char* better_mime = NULL; + dc_msg_guess_msgtype_from_suffix(pathNfilename, &better_type, &better_mime); + if (better_type) { + msg->type = better_type; + dc_param_set(msg->param, DC_PARAM_MIMETYPE, better_mime); + } + free(better_mime); + } + + if ((msg->type==DC_MSG_IMAGE || msg->type==DC_MSG_GIF) + && (dc_param_get_int(msg->param, DC_PARAM_WIDTH, 0)<=0 || dc_param_get_int(msg->param, DC_PARAM_HEIGHT, 0)<=0)) { + /* set width/height of images, if not yet done */ + unsigned char* buf = NULL; size_t buf_bytes; uint32_t w, h; + if (dc_read_file(pathNfilename, (void**)&buf, &buf_bytes, msg->context)) { + if (dc_get_filemeta(buf, buf_bytes, &w, &h)) { + dc_param_set_int(msg->param, DC_PARAM_WIDTH, w); + dc_param_set_int(msg->param, DC_PARAM_HEIGHT, h); + } + } + free(buf); + } + + dc_log_info(context, 0, "Attaching \"%s\" for message type #%i.", pathNfilename, (int)msg->type); + + if (msg->text) { free(msg->text); } + if (msg->type==DC_MSG_AUDIO) { + char* filename = dc_get_filename(pathNfilename); + char* author = dc_param_get(msg->param, DC_PARAM_AUTHORNAME, ""); + char* title = dc_param_get(msg->param, DC_PARAM_TRACKNAME, ""); + msg->text = dc_mprintf("%s %s %s", filename, author, title); /* for outgoing messages, also add the mediainfo. For incoming messages, this is not needed as the filename is build from these information */ + free(filename); + free(author); + free(title); + } + else if (DC_MSG_MAKE_FILENAME_SEARCHABLE(msg->type)) { + msg->text = dc_get_filename(pathNfilename); + } + else if (DC_MSG_MAKE_SUFFIX_SEARCHABLE(msg->type)) { + msg->text = dc_get_filesuffix_lc(pathNfilename); + } + } + else + { + dc_log_error(context, 0, "Attachment missing for message of type #%i.", (int)msg->type); /* should not happen */ + goto cleanup; + } + } + else + { + dc_log_error(context, 0, "Cannot send messages of type #%i.", (int)msg->type); /* should not happen */ + goto cleanup; + } + + dc_unarchive_chat(context, chat_id); + + context->smtp->log_connect_errors = 1; + + chat = dc_chat_new(context); + if (dc_chat_load_from_db(chat, chat_id)) { + msg->id = dc_send_msg_raw(context, chat, msg, dc_create_smeared_timestamp(context)); + if (msg ->id==0) { + goto cleanup; /* error already logged */ + } + } + + context->cb(context, DC_EVENT_MSGS_CHANGED, chat_id, msg->id); + +cleanup: + dc_chat_unref(chat); + free(pathNfilename); + return msg->id; +} + + +/** + * Send a simple text message a given chat. + * + * Sends the event #DC_EVENT_MSGS_CHANGED on succcess. + * However, this does not imply, the message really reached the recipient - + * sending may be delayed eg. due to network problems. However, from your + * view, you're done with the message. Sooner or later it will find its way. + * + * See also dc_send_image_msg(). + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id Chat ID to send the text message to. + * @param text_to_send Text to send to the chat defined by the chat ID. + * @return The ID of the message that is about being sent. + */ +uint32_t dc_send_text_msg(dc_context_t* context, uint32_t chat_id, const char* text_to_send) +{ + dc_msg_t* msg = dc_msg_new(); + uint32_t ret = 0; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL || text_to_send==NULL) { + goto cleanup; + } + + msg->type = DC_MSG_TEXT; + msg->text = dc_strdup(text_to_send); + + ret = dc_send_msg_object(context, chat_id, msg); + +cleanup: + dc_msg_unref(msg); + return ret; +} + + +/** + * Send an image to a chat. + * + * Sends the event #DC_EVENT_MSGS_CHANGED on succcess. + * However, this does not imply, the message really reached the recipient - + * sending may be delayed eg. due to network problems. However, from your + * view, you're done with the message. Sooner or later it will find its way. + * + * See also dc_send_text_msg(). + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id Chat ID to send the image to. + * @param file Full path of the image file to send. The core may make a copy of the file. + * @param filemime Mime type of the file to send. NULL if you don't know or don't care. + * @param width Width in pixel of the file. 0 if you don't know or don't care. + * @param height Width in pixel of the file. 0 if you don't know or don't care. + * @return The ID of the message that is about being sent. + */ +uint32_t dc_send_image_msg(dc_context_t* context, uint32_t chat_id, const char* file, const char* filemime, int width, int height) +{ + dc_msg_t* msg = dc_msg_new(); + uint32_t ret = 0; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL || file==NULL) { + goto cleanup; + } + + msg->type = DC_MSG_IMAGE; + dc_param_set (msg->param, DC_PARAM_FILE, file); + dc_param_set_int(msg->param, DC_PARAM_WIDTH, width); /* set in sending job, if 0 */ + dc_param_set_int(msg->param, DC_PARAM_HEIGHT, height); /* set in sending job, if 0 */ + + ret = dc_send_msg_object(context, chat_id, msg); + +cleanup: + dc_msg_unref(msg); + return ret; + +} + + +/** + * Send a video to a chat. + * + * Sends the event #DC_EVENT_MSGS_CHANGED on succcess. + * However, this does not imply, the message really reached the recipient - + * sending may be delayed eg. due to network problems. However, from your + * view, you're done with the message. Sooner or later it will find its way. + * + * See also dc_send_image_msg(). + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id Chat ID to send the video to. + * @param file Full path of the video file to send. The core may make a copy of the file. + * @param filemime Mime type of the file to send. NULL if you don't know or don't care. + * @param width Width in video of the file, if known. 0 if you don't know or don't care. + * @param height Width in video of the file, if known. 0 if you don't know or don't care. + * @param duration Length of the video in milliseconds. 0 if you don't know or don't care. + * @return The ID of the message that is about being sent. + */ +uint32_t dc_send_video_msg(dc_context_t* context, uint32_t chat_id, const char* file, const char* filemime, int width, int height, int duration) +{ + dc_msg_t* msg = dc_msg_new(); + uint32_t ret = 0; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL || file==NULL) { + goto cleanup; + } + + msg->type = DC_MSG_VIDEO; + dc_param_set (msg->param, DC_PARAM_FILE, file); + dc_param_set (msg->param, DC_PARAM_MIMETYPE, filemime); + dc_param_set_int(msg->param, DC_PARAM_WIDTH, width); + dc_param_set_int(msg->param, DC_PARAM_HEIGHT, height); + dc_param_set_int(msg->param, DC_PARAM_DURATION, duration); + + ret = dc_send_msg_object(context, chat_id, msg); + +cleanup: + dc_msg_unref(msg); + return ret; + +} + + +/** + * Send a voice message to a chat. Voice messages are messages just recorded though the device microphone. + * For sending music or other audio data, use dc_send_audio_msg(). + * + * Sends the event #DC_EVENT_MSGS_CHANGED on succcess. + * However, this does not imply, the message really reached the recipient - + * sending may be delayed eg. due to network problems. However, from your + * view, you're done with the message. Sooner or later it will find its way. + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id Chat ID to send the voice message to. + * @param file Full path of the file to send. The core may make a copy of the file. + * @param filemime Mime type of the file to send. NULL if you don't know or don't care. + * @param duration Length of the voice message in milliseconds. 0 if you don't know or don't care. + * @return The ID of the message that is about being sent. + */ +uint32_t dc_send_voice_msg(dc_context_t* context, uint32_t chat_id, const char* file, const char* filemime, int duration) +{ + dc_msg_t* msg = dc_msg_new(); + uint32_t ret = 0; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL || file==NULL) { + goto cleanup; + } + + msg->type = DC_MSG_VOICE; + dc_param_set (msg->param, DC_PARAM_FILE, file); + dc_param_set (msg->param, DC_PARAM_MIMETYPE, filemime); + dc_param_set_int(msg->param, DC_PARAM_DURATION, duration); + + ret = dc_send_msg_object(context, chat_id, msg); + +cleanup: + dc_msg_unref(msg); + return ret; +} + + +/** + * Send an audio file to a chat. Audio messages are eg. music tracks. + * For voice messages just recorded though the device microphone, use dc_send_voice_msg(). + * + * Sends the event #DC_EVENT_MSGS_CHANGED on succcess. + * However, this does not imply, the message really reached the recipient - + * sending may be delayed eg. due to network problems. However, from your + * view, you're done with the message. Sooner or later it will find its way. + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id Chat ID to send the audio to. + * @param file Full path of the file to send. The core may make a copy of the file. + * @param filemime Mime type of the file to send. NULL if you don't know or don't care. + * @param duration Length of the audio in milliseconds. 0 if you don't know or don't care. + * @param author Author or artist of the file. NULL if you don't know or don't care. + * @param trackname Trackname or title of the file. NULL if you don't know or don't care. + * @return The ID of the message that is about being sent. + */ +uint32_t dc_send_audio_msg(dc_context_t* context, uint32_t chat_id, const char* file, const char* filemime, int duration, const char* author, const char* trackname) +{ + dc_msg_t* msg = dc_msg_new(); + uint32_t ret = 0; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL || file==NULL) { + goto cleanup; + } + + msg->type = DC_MSG_AUDIO; + dc_param_set (msg->param, DC_PARAM_FILE, file); + dc_param_set (msg->param, DC_PARAM_MIMETYPE, filemime); + dc_param_set_int(msg->param, DC_PARAM_DURATION, duration); + dc_param_set (msg->param, DC_PARAM_AUTHORNAME, author); + dc_param_set (msg->param, DC_PARAM_TRACKNAME, trackname); + + ret = dc_send_msg_object(context, chat_id, msg); + +cleanup: + dc_msg_unref(msg); + return ret; +} + + +/** + * Send a document to a chat. Use this function to send any document or file to + * a chat. + * + * Sends the event #DC_EVENT_MSGS_CHANGED on succcess. + * However, this does not imply, the message really reached the recipient - + * sending may be delayed eg. due to network problems. However, from your + * view, you're done with the message. Sooner or later it will find its way. + * + * @memberof dc_context_t + * @param context The context object as returned from dc_context_new(). + * @param chat_id Chat ID to send the document to. + * @param file Full path of the file to send. The core may make a copy of the file. + * @param filemime Mime type of the file to send. NULL if you don't know or don't care. + * @return The ID of the message that is about being sent. + */ +uint32_t dc_send_file_msg(dc_context_t* context, uint32_t chat_id, const char* file, const char* filemime) +{ + dc_msg_t* msg = dc_msg_new(); + uint32_t ret = 0; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL || file==NULL) { + goto cleanup; + } + + msg->type = DC_MSG_FILE; + dc_param_set(msg->param, DC_PARAM_FILE, file); + dc_param_set(msg->param, DC_PARAM_MIMETYPE, filemime); + + ret = dc_send_msg_object(context, chat_id, msg); + +cleanup: + dc_msg_unref(msg); + return ret; +} + + +/** + * Send foreign contact data to a chat. + * + * Sends the name and the email address of another contact to a chat. + * The contact this may or may not be a member of the chat. + * + * Typically used to share a contact to another member or to a group of members. + * + * Internally, the function just creates an appropriate text message and sends it + * using dc_send_text_msg(). + * + * NB: The "vcard" in the function name is just an abbreviation of "visiting card" and + * is not related to the VCARD data format. + * + * @memberof dc_context_t + * @param context The context object. + * @param chat_id The chat to send the message to. + * @param contact_id The contact whichs data should be shared to the chat. + * @return Returns the ID of the message sent. + */ +uint32_t dc_send_vcard_msg(dc_context_t* context, uint32_t chat_id, uint32_t contact_id) +{ + uint32_t ret = 0; + dc_msg_t* msg = dc_msg_new(); + dc_contact_t* contact = NULL; + char* text_to_send = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL) { + goto cleanup; + } + + if ((contact=dc_get_contact(context, contact_id))==NULL) { + goto cleanup; + } + + if (contact->authname && contact->authname[0]) { + text_to_send = dc_mprintf("%s: %s", contact->authname, contact->addr); + } + else { + text_to_send = dc_strdup(contact->addr); + } + + ret = dc_send_text_msg(context, chat_id, text_to_send); + +cleanup: + dc_msg_unref(msg); + dc_contact_unref(contact); + free(text_to_send); + return ret; +} + + +/* + * Log a device message. + * Such a message is typically shown in the "middle" of the chat, the user can check this using dc_msg_is_info(). + * Texts are typically "Alice has added Bob to the group" or "Alice fingerprint verified." + */ +void dc_add_device_msg(dc_context_t* context, uint32_t chat_id, const char* text) +{ + uint32_t msg_id = 0; + sqlite3_stmt* stmt = NULL; + char* rfc724_mid = dc_create_outgoing_rfc724_mid(NULL, "@device"); + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || text==NULL) { + goto cleanup; + } + + stmt = dc_sqlite3_prepare(context->sql, + "INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,rfc724_mid) VALUES (?,?,?, ?,?,?, ?,?);"); + sqlite3_bind_int (stmt, 1, chat_id); + sqlite3_bind_int (stmt, 2, DC_CONTACT_ID_DEVICE); + sqlite3_bind_int (stmt, 3, DC_CONTACT_ID_DEVICE); + sqlite3_bind_int64(stmt, 4, dc_create_smeared_timestamp(context)); + sqlite3_bind_int (stmt, 5, DC_MSG_TEXT); + sqlite3_bind_int (stmt, 6, DC_STATE_IN_NOTICED); + sqlite3_bind_text (stmt, 7, text, -1, SQLITE_STATIC); + sqlite3_bind_text (stmt, 8, rfc724_mid, -1, SQLITE_STATIC); + if (sqlite3_step(stmt)!=SQLITE_DONE) { + goto cleanup; + } + msg_id = dc_sqlite3_get_rowid(context->sql, "msgs", "rfc724_mid", rfc724_mid); + context->cb(context, DC_EVENT_MSGS_CHANGED, chat_id, msg_id); + +cleanup: + free(rfc724_mid); + sqlite3_finalize(stmt); +} + + +/** + * Forward messages to another chat. + * + * @memberof dc_context_t + * @param context the context object as created by dc_context_new() + * @param msg_ids an array of uint32_t containing all message IDs that should be forwarded + * @param msg_cnt the number of messages IDs in the msg_ids array + * @param chat_id The destination chat ID. + * @return none + */ +void dc_forward_msgs(dc_context_t* context, const uint32_t* msg_ids, int msg_cnt, uint32_t chat_id) +{ + dc_msg_t* msg = dc_msg_new(); + dc_chat_t* chat = dc_chat_new(context); + dc_contact_t* contact = dc_contact_new(context); + int transaction_pending = 0; + carray* created_db_entries = carray_new(16); + char* idsstr = NULL; + char* q3 = NULL; + sqlite3_stmt* stmt = NULL; + time_t curr_timestamp = 0; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || msg_ids==NULL || msg_cnt<=0 || chat_id<=DC_CHAT_ID_LAST_SPECIAL) { + goto cleanup; + } + + dc_sqlite3_begin_transaction(context->sql); + transaction_pending = 1; + + dc_unarchive_chat(context, chat_id); + + context->smtp->log_connect_errors = 1; + + if (!dc_chat_load_from_db(chat, chat_id)) { + goto cleanup; + } + + curr_timestamp = dc_create_smeared_timestamps(context, msg_cnt); + + idsstr = dc_arr_to_string(msg_ids, msg_cnt); + q3 = sqlite3_mprintf("SELECT id FROM msgs WHERE id IN(%s) ORDER BY timestamp,id", idsstr); + stmt = dc_sqlite3_prepare(context->sql, q3); + while (sqlite3_step(stmt)==SQLITE_ROW) + { + int src_msg_id = sqlite3_column_int(stmt, 0); + if (!dc_msg_load_from_db(msg, context, src_msg_id)) { + goto cleanup; + } + + dc_param_set_int(msg->param, DC_PARAM_FORWARDED, 1); + dc_param_set (msg->param, DC_PARAM_GUARANTEE_E2EE, NULL); + dc_param_set (msg->param, DC_PARAM_FORCE_PLAINTEXT, NULL); + + uint32_t new_msg_id = dc_send_msg_raw(context, chat, msg, curr_timestamp++); + carray_add(created_db_entries, (void*)(uintptr_t)chat_id, NULL); + carray_add(created_db_entries, (void*)(uintptr_t)new_msg_id, NULL); + } + + dc_sqlite3_commit(context->sql); + transaction_pending = 0; + +cleanup: + if (transaction_pending) { dc_sqlite3_rollback(context->sql); } + if (created_db_entries) { + size_t i, icnt = carray_count(created_db_entries); + for (i = 0; i < icnt; i += 2) { + context->cb(context, DC_EVENT_MSGS_CHANGED, (uintptr_t)carray_get(created_db_entries, i), (uintptr_t)carray_get(created_db_entries, i+1)); + } + carray_free(created_db_entries); + } + dc_contact_unref(contact); + dc_msg_unref(msg); + dc_chat_unref(chat); + sqlite3_finalize(stmt); + free(idsstr); + sqlite3_free(q3); +} diff --git a/src/dc_chat.h b/src/dc_chat.h index df8b6a04..7be3277d 100644 --- a/src/dc_chat.h +++ b/src/dc_chat.h @@ -50,19 +50,36 @@ struct _dc_chat dc_param_t* param; /**< Additional parameters for a chat. Should not be used directly. */ }; - int dc_chat_load_from_db (dc_chat_t*, uint32_t id); int dc_chat_update_param (dc_chat_t*); - #define DC_CHAT_TYPE_IS_MULTI(a) ((a)==DC_CHAT_TYPE_GROUP || (a)==DC_CHAT_TYPE_VERIFIED_GROUP) #define DC_CHAT_TYPE_CAN_SEND(a) ((a)==DC_CHAT_TYPE_SINGLE || (a)==DC_CHAT_TYPE_GROUP || (a)==DC_CHAT_TYPE_VERIFIED_GROUP) - #define DC_CHAT_PREFIX "Chat:" /* you MUST NOT modify this or the following strings */ #define DC_CHATS_FOLDER "DeltaChat" // make sure not to use reserved words here, eg. "Chats" or "Chat" are reserved in gmail +// Context functions to work with chats +int dc_add_to_chat_contacts_table (dc_context_t*, uint32_t chat_id, uint32_t contact_id); +int dc_is_contact_in_chat (dc_context_t*, uint32_t chat_id, uint32_t contact_id); +size_t dc_get_chat_cnt (dc_context_t*); +uint32_t dc_get_chat_id_by_grpid (dc_context_t*, const char* grpid, int* ret_blocked, int* ret_verified); +void dc_create_or_lookup_nchat_by_contact_id (dc_context_t*, uint32_t contact_id, int create_blocked, uint32_t* ret_chat_id, int* ret_chat_blocked); +void dc_lookup_real_nchat_by_contact_id (dc_context_t*, uint32_t contact_id, uint32_t* ret_chat_id, int* ret_chat_blocked); +void dc_unarchive_chat (dc_context_t*, uint32_t chat_id); +void dc_block_chat (dc_context_t*, uint32_t chat_id, int new_blocking); +void dc_unblock_chat (dc_context_t*, uint32_t chat_id); +uint32_t dc_send_msg_object (dc_context_t*, uint32_t chat_id, dc_msg_t*); +void dc_add_device_msg (dc_context_t*, uint32_t chat_id, const char* text); +int dc_get_chat_contact_cnt (dc_context_t*, uint32_t chat_id); +int dc_is_group_explicitly_left (dc_context_t*, const char* grpid); +void dc_set_group_explicitly_left (dc_context_t*, const char* grpid); + +#define DC_FROM_HANDSHAKE 0x01 +int dc_add_contact_to_chat_ex (dc_context_t*, uint32_t chat_id, uint32_t contact_id, int flags); + + #ifdef __cplusplus } /* /extern "C" */ #endif diff --git a/src/dc_chatlist.c b/src/dc_chatlist.c index 063cf14a..ae0d5c3d 100644 --- a/src/dc_chatlist.c +++ b/src/dc_chatlist.c @@ -287,6 +287,32 @@ dc_context_t* dc_chatlist_get_context(dc_chatlist_t* chatlist) } +static uint32_t get_last_deaddrop_fresh_msg(dc_context_t* context) +{ + uint32_t ret = 0; + sqlite3_stmt* stmt = NULL; + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT m.id " + " FROM msgs m " + " LEFT JOIN chats c ON c.id=m.chat_id " + " WHERE m.state=" DC_STRINGIFY(DC_STATE_IN_FRESH) + " AND m.hidden=0 " + " AND c.blocked=" DC_STRINGIFY(DC_CHAT_DEADDROP_BLOCKED) + " ORDER BY m.timestamp DESC, m.id DESC;"); /* we have an index over the state-column, this should be sufficient as there are typically only few fresh messages */ + + if (sqlite3_step(stmt)!=SQLITE_ROW) { + goto cleanup; + } + + ret = sqlite3_column_int(stmt, 0); + +cleanup: + sqlite3_finalize(stmt); + return ret; +} + + /** * Library-internal. * @@ -340,7 +366,7 @@ int dc_chatlist_load_from_db(dc_chatlist_t* chatlist, int listflags, const char* { /* show normal chatlist */ if (!(listflags & DC_GCL_NO_SPECIALS)) { - uint32_t last_deaddrop_fresh_msg_id = dc_get_last_deaddrop_fresh_msg(chatlist->context); + uint32_t last_deaddrop_fresh_msg_id = get_last_deaddrop_fresh_msg(chatlist->context); if (last_deaddrop_fresh_msg_id > 0) { dc_array_add_id(chatlist->chatNlastmsg_ids, DC_CHAT_ID_DEADDROP); /* show deaddrop with the last fresh message */ dc_array_add_id(chatlist->chatNlastmsg_ids, last_deaddrop_fresh_msg_id); @@ -372,7 +398,7 @@ int dc_chatlist_load_from_db(dc_chatlist_t* chatlist, int listflags, const char* dc_array_add_id(chatlist->chatNlastmsg_ids, sqlite3_column_int(stmt, 1)); } - if (add_archived_link_item && dc_get_archived_count(chatlist->context)>0) + if (add_archived_link_item && dc_get_archived_cnt(chatlist->context)>0) { dc_array_add_id(chatlist->chatNlastmsg_ids, DC_CHAT_ID_ARCHIVED_LINK); dc_array_add_id(chatlist->chatNlastmsg_ids, 0); @@ -388,3 +414,68 @@ cleanup: free(strLikeCmd); return success; } + + +/******************************************************************************* + * Context functions to work with chatlists + ******************************************************************************/ + + +int dc_get_archived_cnt(dc_context_t* context) +{ + int ret = 0; + sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, + "SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;"); + if (sqlite3_step(stmt)==SQLITE_ROW) { + ret = sqlite3_column_int(stmt, 0); + } + sqlite3_finalize(stmt); + return ret; +} + + +/** + * Get a list of chats. The list can be filtered by query parameters. + * To get the chat messages, use dc_get_chat_msgs(). + * + * @memberof dc_context_t + * @param context The context object as returned by dc_context_new() + * @param listflags A combination of flags: + * - if the flag DC_GCL_ARCHIVED_ONLY is set, only archived chats are returned. + * if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and + * the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are _any_ archived + * chats + * - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added + * to the list (may be used eg. for selecting chats on forwarding, the flag is + * not needed when DC_GCL_ARCHIVED_ONLY is already set) + * @param query_str An optional query for filtering the list. Only chats matching this query + * are returned. Give NULL for no filtering. + * @param query_id An optional contact ID for filtering the list. Only chats including this contact ID + * are returned. Give 0 for no filtering. + * @return A chatlist as an dc_chatlist_t object. Must be freed using + * dc_chatlist_unref() when no longer used + */ +dc_chatlist_t* dc_get_chatlist(dc_context_t* context, int listflags, const char* query_str, uint32_t query_id) +{ + int success = 0; + dc_chatlist_t* obj = dc_chatlist_new(context); + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + goto cleanup; + } + + if (!dc_chatlist_load_from_db(obj, listflags, query_str, query_id)) { + goto cleanup; + } + + success = 1; + +cleanup: + if (success) { + return obj; + } + else { + dc_chatlist_unref(obj); + return NULL; + } +} \ No newline at end of file diff --git a/src/dc_chatlist.h b/src/dc_chatlist.h index 6e362e7c..59858d65 100644 --- a/src/dc_chatlist.h +++ b/src/dc_chatlist.h @@ -38,10 +38,13 @@ struct _dc_chatlist dc_array_t* chatNlastmsg_ids; }; - int dc_chatlist_load_from_db (dc_chatlist_t*, int listflags, const char* query, uint32_t query_contact_id); +// Context functions to work with chatlist +int dc_get_archived_cnt (dc_context_t*); + + #ifdef __cplusplus } /* /extern "C" */ #endif diff --git a/src/dc_configure.c b/src/dc_configure.c index b0a64d10..1b71d41c 100644 --- a/src/dc_configure.c +++ b/src/dc_configure.c @@ -261,10 +261,10 @@ typedef struct outlk_autodiscover_t #define OUTLK_PORT 3 #define OUTLK_SSL 4 #define OUTLK_REDIRECTURL 5 - #define _OUTLK_COUNT_ 6 + #define _OUTLK_CNT_ 6 int tag_config; - char* config[_OUTLK_COUNT_]; + char* config[_OUTLK_CNT_]; char* redirect; } outlk_autodiscover_t; @@ -273,7 +273,7 @@ typedef struct outlk_autodiscover_t static void outlk_clean_config(outlk_autodiscover_t* outlk_ad) { int i; - for (i = 0; i < _OUTLK_COUNT_; i++) { + for (i = 0; i < _OUTLK_CNT_; i++) { free(outlk_ad->config[i]); outlk_ad->config[i] = NULL; } diff --git a/src/dc_contact.c b/src/dc_contact.c index dd9f887d..68d22e62 100644 --- a/src/dc_contact.c +++ b/src/dc_contact.c @@ -23,6 +23,8 @@ #include "dc_context.h" #include "dc_contact.h" #include "dc_apeerstate.h" +#include "dc_loginparam.h" +#include "dc_pgp.h" #define DC_CONTACT_MAGIC 0x0c047ac7 @@ -101,11 +103,6 @@ void dc_contact_empty(dc_contact_t* contact) } -/******************************************************************************* - * Getters - ******************************************************************************/ - - /** * Get the ID of the contact. * @@ -310,6 +307,62 @@ cleanup: } +/** + * Library-internal. + * + * Calling this function is not thread-safe, locking is up to the caller. + * + * @private @memberof dc_contact_t + */ +int dc_contact_load_from_db(dc_contact_t* contact, dc_sqlite3_t* sql, uint32_t contact_id) +{ + int success = 0; + sqlite3_stmt* stmt = NULL; + + if (contact == NULL || contact->magic != DC_CONTACT_MAGIC || sql == NULL) { + goto cleanup; + } + + dc_contact_empty(contact); + + if (contact_id == DC_CONTACT_ID_SELF) + { + contact->id = contact_id; + contact->name = dc_stock_str(contact->context, DC_STR_SELF); + contact->addr = dc_sqlite3_get_config(sql, "configured_addr", ""); + } + else + { + stmt = dc_sqlite3_prepare(sql, + "SELECT c.name, c.addr, c.origin, c.blocked, c.authname " + " FROM contacts c " + " WHERE c.id=?;"); + sqlite3_bind_int(stmt, 1, contact_id); + if (sqlite3_step(stmt) != SQLITE_ROW) { + goto cleanup; + } + + contact->id = contact_id; + contact->name = dc_strdup((char*)sqlite3_column_text (stmt, 0)); + contact->addr = dc_strdup((char*)sqlite3_column_text (stmt, 1)); + contact->origin = sqlite3_column_int (stmt, 2); + contact->blocked = sqlite3_column_int (stmt, 3); + contact->authname = dc_strdup((char*)sqlite3_column_text (stmt, 4)); + } + + success = 1; + +cleanup: + sqlite3_finalize(stmt); + return success; +} + + +/******************************************************************************* + * Working with names + ******************************************************************************/ + + /** * Get the first name. * @@ -337,11 +390,6 @@ char* dc_get_first_name(const char* full_name) } -/******************************************************************************* - * Misc. - ******************************************************************************/ - - /** * Normalize a name in-place. * @@ -393,57 +441,6 @@ void dc_normalize_name(char* full_name) } -/** - * Library-internal. - * - * Calling this function is not thread-safe, locking is up to the caller. - * - * @private @memberof dc_contact_t - */ -int dc_contact_load_from_db(dc_contact_t* contact, dc_sqlite3_t* sql, uint32_t contact_id) -{ - int success = 0; - sqlite3_stmt* stmt = NULL; - - if (contact == NULL || contact->magic != DC_CONTACT_MAGIC || sql == NULL) { - goto cleanup; - } - - dc_contact_empty(contact); - - if (contact_id == DC_CONTACT_ID_SELF) - { - contact->id = contact_id; - contact->name = dc_stock_str(contact->context, DC_STR_SELF); - contact->addr = dc_sqlite3_get_config(sql, "configured_addr", ""); - } - else - { - stmt = dc_sqlite3_prepare(sql, - "SELECT c.name, c.addr, c.origin, c.blocked, c.authname " - " FROM contacts c " - " WHERE c.id=?;"); - sqlite3_bind_int(stmt, 1, contact_id); - if (sqlite3_step(stmt) != SQLITE_ROW) { - goto cleanup; - } - - contact->id = contact_id; - contact->name = dc_strdup((char*)sqlite3_column_text (stmt, 0)); - contact->addr = dc_strdup((char*)sqlite3_column_text (stmt, 1)); - contact->origin = sqlite3_column_int (stmt, 2); - contact->blocked = sqlite3_column_int (stmt, 3); - contact->authname = dc_strdup((char*)sqlite3_column_text (stmt, 4)); - } - - success = 1; - -cleanup: - sqlite3_finalize(stmt); - return success; -} - - /******************************************************************************* * Working with e-mail-addresses ******************************************************************************/ @@ -543,3 +540,773 @@ int dc_addr_equals_contact(dc_context_t* context, const char* addr, uint32_t con } return addr_are_equal; } + + +/******************************************************************************* + * Context functions to work with contacts + ******************************************************************************/ + + +int dc_real_contact_exists(dc_context_t* context, uint32_t contact_id) +{ + sqlite3_stmt* stmt = NULL; + int ret = 0; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || context->sql->cobj==NULL + || contact_id<=DC_CONTACT_ID_LAST_SPECIAL) { + goto cleanup; + } + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT id FROM contacts WHERE id=?;"); + sqlite3_bind_int(stmt, 1, contact_id); + + if (sqlite3_step(stmt)==SQLITE_ROW) { + ret = 1; + } + +cleanup: + sqlite3_finalize(stmt); + return ret; +} + + +size_t dc_get_real_contact_cnt(dc_context_t* context) +{ + size_t ret = 0; + sqlite3_stmt* stmt = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || context->sql->cobj==NULL) { + goto cleanup; + } + + stmt = dc_sqlite3_prepare(context->sql, "SELECT COUNT(*) FROM contacts WHERE id>?;"); + sqlite3_bind_int(stmt, 1, DC_CONTACT_ID_LAST_SPECIAL); + if (sqlite3_step(stmt)!=SQLITE_ROW) { + goto cleanup; + } + + ret = sqlite3_column_int(stmt, 0); + +cleanup: + sqlite3_finalize(stmt); + return ret; +} + + +uint32_t dc_add_or_lookup_contact( dc_context_t* context, + const char* name /*can be NULL, the caller may use dc_normalize_name() before*/, + const char* addr__, + int origin, + int* sth_modified ) +{ + #define CONTACT_MODIFIED 1 + #define CONTACT_CREATED 2 + sqlite3_stmt* stmt = NULL; + uint32_t row_id = 0; + int dummy = 0; + char* addr = NULL; + char* row_name = NULL; + char* row_addr = NULL; + char* row_authname = NULL; + + if (sth_modified==NULL) { + sth_modified = &dummy; + } + + *sth_modified = 0; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || addr__==NULL || origin<=0) { + goto cleanup; + } + + /* normalize the email-address: + - remove leading `mailto:` */ + addr = dc_addr_normalize(addr__); + + /* rough check if email-address is valid */ + if (strlen(addr) < 3 || strchr(addr, '@')==NULL || strchr(addr, '.')==NULL) { + dc_log_warning(context, 0, "Bad address \"%s\" for contact \"%s\".", addr, name?name:""); + goto cleanup; + } + + /* insert email-address to database or modify the record with the given email-address. + we treat all email-addresses case-insensitive. */ + stmt = dc_sqlite3_prepare(context->sql, + "SELECT id, name, addr, origin, authname FROM contacts WHERE addr=? COLLATE NOCASE;"); + sqlite3_bind_text(stmt, 1, (const char*)addr, -1, SQLITE_STATIC); + if (sqlite3_step(stmt)==SQLITE_ROW) + { + + int row_origin, update_addr = 0, update_name = 0, update_authname = 0; + + row_id = sqlite3_column_int(stmt, 0); + row_name = dc_strdup((char*)sqlite3_column_text(stmt, 1)); + row_addr = dc_strdup((char*)sqlite3_column_text(stmt, 2)); + row_origin = sqlite3_column_int(stmt, 3); + row_authname = dc_strdup((char*)sqlite3_column_text(stmt, 4)); + sqlite3_finalize (stmt); + stmt = NULL; + + if (name && name[0]) { + if (row_name[0]) { + if (origin>=row_origin && strcmp(name, row_name)!=0) { + update_name = 1; + } + } + else { + update_name = 1; + } + + if (origin==DC_ORIGIN_INCOMING_UNKNOWN_FROM && strcmp(name, row_authname)!=0) { + update_authname = 1; + } + } + + if (origin>=row_origin && strcmp(addr, row_addr)!=0 /*really compare case-sensitive here*/) { + update_addr = 1; + } + + if (update_name || update_authname || update_addr || origin>row_origin) + { + stmt = dc_sqlite3_prepare(context->sql, + "UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;"); + sqlite3_bind_text(stmt, 1, update_name? name : row_name, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, update_addr? addr : row_addr, -1, SQLITE_STATIC); + sqlite3_bind_int (stmt, 3, origin>row_origin? origin : row_origin); + sqlite3_bind_text(stmt, 4, update_authname? name : row_authname, -1, SQLITE_STATIC); + sqlite3_bind_int (stmt, 5, row_id); + sqlite3_step (stmt); + sqlite3_finalize (stmt); + stmt = NULL; + + if (update_name) + { + /* Update the contact name also if it is used as a group name. + This is one of the few duplicated data, however, getting the chat list is easier this way.*/ + stmt = dc_sqlite3_prepare(context->sql, + "UPDATE chats SET name=? WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?);"); + sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC); + sqlite3_bind_int (stmt, 2, DC_CHAT_TYPE_SINGLE); + sqlite3_bind_int (stmt, 3, row_id); + sqlite3_step (stmt); + } + + *sth_modified = CONTACT_MODIFIED; + } + } + else + { + sqlite3_finalize (stmt); + stmt = NULL; + + stmt = dc_sqlite3_prepare(context->sql, + "INSERT INTO contacts (name, addr, origin) VALUES(?, ?, ?);"); + sqlite3_bind_text(stmt, 1, name? name : "", -1, SQLITE_STATIC); /* avoid NULL-fields in column */ + sqlite3_bind_text(stmt, 2, addr, -1, SQLITE_STATIC); + sqlite3_bind_int (stmt, 3, origin); + if (sqlite3_step(stmt)==SQLITE_DONE) + { + row_id = dc_sqlite3_get_rowid(context->sql, "contacts", "addr", addr); + *sth_modified = CONTACT_CREATED; + } + else + { + dc_log_error(context, 0, "Cannot add contact."); /* should not happen */ + } + } + +cleanup: + free(addr); + free(row_addr); + free(row_name); + free(row_authname); + sqlite3_finalize(stmt); + return row_id; +} + + +void dc_scaleup_contact_origin(dc_context_t* context, uint32_t contact_id, int origin) +{ + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + return; + } + + sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, + "UPDATE contacts SET origin=? WHERE id=? AND originsql, contact_id)) { + if (contact->blocked) { + is_blocked = 1; + } + } + + dc_contact_unref(contact); + return is_blocked; +} + + +int dc_get_contact_origin(dc_context_t* context, uint32_t contact_id, int* ret_blocked) +{ + int ret = 0; + int dummy = 0; if (ret_blocked==NULL) { ret_blocked = &dummy; } + dc_contact_t* contact = dc_contact_new(context); + + *ret_blocked = 0; + + if (!dc_contact_load_from_db(contact, context->sql, contact_id)) { /* we could optimize this by loading only the needed fields */ + goto cleanup; + } + + if (contact->blocked) { + *ret_blocked = 1; + goto cleanup; + } + + ret = contact->origin; + +cleanup: + dc_contact_unref(contact); + return ret; +} + + +/** + * Add a single contact as a result of an _explicit_ user action. + * + * We assume, the contact name, if any, is entered by the user and is used "as is" therefore, + * normalize() is _not_ called for the name. If the contact is blocked, it is unblocked. + * + * To add a number of contacts, see dc_add_address_book() which is much faster for adding + * a bunch of addresses. + * + * May result in a #DC_EVENT_CONTACTS_CHANGED event. + * + * @memberof dc_context_t + * @param context The context object as created by dc_context_new(). + * @param name Name of the contact to add. If you do not know the name belonging + * to the address, you can give NULL here. + * @param addr E-mail-address of the contact to add. If the email address + * already exists, the name is updated and the origin is increased to + * "manually created". + * @return Contact ID of the created or reused contact. + */ +uint32_t dc_create_contact(dc_context_t* context, const char* name, const char* addr) +{ + uint32_t contact_id = 0; + int sth_modified = 0; + int blocked = 0; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || addr==NULL || addr[0]==0) { + goto cleanup; + } + + contact_id = dc_add_or_lookup_contact(context, name, addr, DC_ORIGIN_MANUALLY_CREATED, &sth_modified); + + blocked = dc_is_contact_blocked(context, contact_id); + + context->cb(context, DC_EVENT_CONTACTS_CHANGED, sth_modified==CONTACT_CREATED? contact_id : 0, 0); + + if (blocked) { + dc_block_contact(context, contact_id, 0); + } + +cleanup: + return contact_id; +} + + +/** + * Add a number of contacts. + * + * Typically used to add the whole address book from the OS. As names here are typically not + * well formatted, we call normalize() for each name given. + * + * To add a single contact entered by the user, you should prefer dc_create_contact(), + * however, for adding a bunch of addresses, this function is _much_ faster. + * + * The function takes are of not overwriting names manually added or edited by dc_create_contact(). + * + * @memberof dc_context_t + * @param context the context object as created by dc_context_new(). + * @param adr_book A multi-line string in the format in the format + * `Name one\nAddress one\nName two\Address two`. If an email address + * already exists, the name is updated and the origin is increased to + * "manually created". + * @return The number of modified or added contacts. + */ +int dc_add_address_book(dc_context_t* context, const char* adr_book) /* format: Name one\nAddress one\nName two\Address two */ +{ + carray* lines = NULL; + size_t i = 0; + size_t iCnt = 0; + int sth_modified = 0; + int modify_cnt = 0; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || adr_book==NULL) { + goto cleanup; + } + + if ((lines=dc_split_into_lines(adr_book))==NULL) { + goto cleanup; + } + + dc_sqlite3_begin_transaction(context->sql); + + iCnt = carray_count(lines); + for (i = 0; i+1 < iCnt; i += 2) { + char* name = (char*)carray_get(lines, i); + char* addr = (char*)carray_get(lines, i+1); + dc_normalize_name(name); + dc_add_or_lookup_contact(context, name, addr, DC_ORIGIN_ADRESS_BOOK, &sth_modified); + if (sth_modified) { + modify_cnt++; + } + } + + dc_sqlite3_commit(context->sql); + +cleanup: + dc_free_splitted_lines(lines); + + return modify_cnt; +} + + +/** + * Returns known and unblocked contacts. + * + * To get information about a single contact, see dc_get_contact(). + * + * @memberof dc_context_t + * @param context The context object as created by dc_context_new(). + * @param listflags A combination of flags: + * - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters + * - if the flag DC_GCL_VERIFIED_ONLY is set, only verified contacts are returned. + * if DC_GCL_VERIFIED_ONLY is not set, verified and unverified contacts are returned. + * @param query A string to filter the list. Typically used to implement an + * incremental search. NULL for no filtering. + * @return An array containing all contact IDs. Must be dc_array_unref()'d + * after usage. + */ +dc_array_t* dc_get_contacts(dc_context_t* context, uint32_t listflags, const char* query) +{ + char* self_addr = NULL; + char* self_name = NULL; + char* self_name2 = NULL; + int add_self = 0; + dc_array_t* ret = dc_array_new(context, 100); + char* s3strLikeCmd = NULL; + sqlite3_stmt* stmt = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + goto cleanup; + } + + self_addr = dc_sqlite3_get_config(context->sql, "configured_addr", ""); /* we add DC_CONTACT_ID_SELF explicitly; so avoid doubles if the address is present as a normal entry for some case */ + + if ((listflags&DC_GCL_VERIFIED_ONLY) || query) + { + if ((s3strLikeCmd=sqlite3_mprintf("%%%s%%", query? query : ""))==NULL) { + goto cleanup; + } + stmt = dc_sqlite3_prepare(context->sql, + "SELECT c.id FROM contacts c" + " LEFT JOIN acpeerstates ps ON c.addr=ps.addr " + " WHERE c.addr!=? AND c.id>" DC_STRINGIFY(DC_CONTACT_ID_LAST_SPECIAL) " AND c.origin>=" DC_STRINGIFY(DC_ORIGIN_MIN_CONTACT_LIST) " AND c.blocked=0 AND (c.name LIKE ? OR c.addr LIKE ?)" /* see comments in dc_search_msgs() about the LIKE operator */ + " AND (1=? OR LENGTH(ps.verified_key_fingerprint)!=0) " + " ORDER BY LOWER(c.name||c.addr),c.id;"); + sqlite3_bind_text(stmt, 1, self_addr, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 2, s3strLikeCmd, -1, SQLITE_STATIC); + sqlite3_bind_text(stmt, 3, s3strLikeCmd, -1, SQLITE_STATIC); + sqlite3_bind_int (stmt, 4, (listflags&DC_GCL_VERIFIED_ONLY)? 0/*force checking for verified_key*/ : 1/*force statement being always true*/); + + self_name = dc_sqlite3_get_config(context->sql, "displayname", ""); + self_name2 = dc_stock_str(context, DC_STR_SELF); + if (query==NULL || dc_str_contains(self_addr, query) || dc_str_contains(self_name, query) || dc_str_contains(self_name2, query)) { + add_self = 1; + } + } + else + { + stmt = dc_sqlite3_prepare(context->sql, + "SELECT id FROM contacts" + " WHERE addr!=? AND id>" DC_STRINGIFY(DC_CONTACT_ID_LAST_SPECIAL) " AND origin>=" DC_STRINGIFY(DC_ORIGIN_MIN_CONTACT_LIST) " AND blocked=0" + " ORDER BY LOWER(name||addr),id;"); + sqlite3_bind_text(stmt, 1, self_addr, -1, SQLITE_STATIC); + + add_self = 1; + } + + while (sqlite3_step(stmt)==SQLITE_ROW) { + dc_array_add_id(ret, sqlite3_column_int(stmt, 0)); + } + + /* to the end of the list, add self - this is to be in sync with member lists and to allow the user to start a self talk */ + if ((listflags&DC_GCL_ADD_SELF) && add_self) { + dc_array_add_id(ret, DC_CONTACT_ID_SELF); + } + +cleanup: + sqlite3_finalize(stmt); + sqlite3_free(s3strLikeCmd); + free(self_addr); + free(self_name); + free(self_name2); + return ret; +} + + +/** + * Get blocked contacts. + * + * @memberof dc_context_t + * @param context The context object as created by dc_context_new(). + * @return An array containing all blocked contact IDs. Must be dc_array_unref()'d + * after usage. + */ +dc_array_t* dc_get_blocked_contacts(dc_context_t* context) +{ + dc_array_t* ret = dc_array_new(context, 100); + sqlite3_stmt* stmt = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + goto cleanup; + } + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT id FROM contacts" + " WHERE id>? AND blocked!=0" + " ORDER BY LOWER(name||addr),id;"); + sqlite3_bind_int(stmt, 1, DC_CONTACT_ID_LAST_SPECIAL); + while (sqlite3_step(stmt)==SQLITE_ROW) { + dc_array_add_id(ret, sqlite3_column_int(stmt, 0)); + } + +cleanup: + sqlite3_finalize(stmt); + return ret; +} + + +/** + * Get a single contact object. For a list, see eg. dc_get_contacts(). + * + * For contact DC_CONTACT_ID_SELF (1), the function returns sth. + * like "Me" in the selected language and the email address + * defined by dc_set_config(). + * + * @memberof dc_context_t + * @param context The context object as created by dc_context_new(). + * @param contact_id ID of the contact to get the object for. + * @return The contact object, must be freed using dc_contact_unref() when no + * longer used. NULL on errors. + */ +dc_contact_t* dc_get_contact(dc_context_t* context, uint32_t contact_id) +{ + dc_contact_t* ret = dc_contact_new(context); + + if (!dc_contact_load_from_db(ret, context->sql, contact_id)) { + dc_contact_unref(ret); + ret = NULL; + } + + return ret; /* may be NULL */ +} + + +/** + * Mark all messages sent by the given contact + * as _noticed_. See also dc_marknoticed_chat() and + * dc_markseen_msgs() + * + * @memberof dc_context_t + * @param context The context object as created by dc_context_new() + * @param contact_id The contact ID of which all messages should be marked as noticed. + * @return none + */ +void dc_marknoticed_contact(dc_context_t* context, uint32_t contact_id) +{ + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + return; + } + + sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, + "UPDATE msgs SET state=" DC_STRINGIFY(DC_STATE_IN_NOTICED) " WHERE from_id=? AND state=" DC_STRINGIFY(DC_STATE_IN_FRESH) ";"); + sqlite3_bind_int(stmt, 1, contact_id); + sqlite3_step(stmt); + sqlite3_finalize(stmt); +} + + +/** + * Get the number of blocked contacts. + * + * @memberof dc_context_t + * @param context The context object as created by dc_context_new(). + * @return The number of blocked contacts. + */ +int dc_get_blocked_cnt(dc_context_t* context) +{ + int ret = 0; + sqlite3_stmt* stmt = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + goto cleanup; + } + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT COUNT(*) FROM contacts" + " WHERE id>? AND blocked!=0"); + sqlite3_bind_int(stmt, 1, DC_CONTACT_ID_LAST_SPECIAL); + if (sqlite3_step(stmt)!=SQLITE_ROW) { + goto cleanup; + } + ret = sqlite3_column_int(stmt, 0); + +cleanup: + sqlite3_finalize(stmt); + return ret; +} + + +/** + * Block or unblock a contact. + * May result in a #DC_EVENT_CONTACTS_CHANGED event. + * + * @memberof dc_context_t + * @param context The context object as created by dc_context_new(). + * @param contact_id The ID of the contact to block or unblock. + * @param new_blocking 1=block contact, 0=unblock contact + * @return None. + */ +void dc_block_contact(dc_context_t* context, uint32_t contact_id, int new_blocking) +{ + int send_event = 0; + dc_contact_t* contact = dc_contact_new(context); + sqlite3_stmt* stmt = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || contact_id<=DC_CONTACT_ID_LAST_SPECIAL) { + goto cleanup; + } + + if (dc_contact_load_from_db(contact, context->sql, contact_id) + && contact->blocked!=new_blocking) + { + stmt = dc_sqlite3_prepare(context->sql, + "UPDATE contacts SET blocked=? WHERE id=?;"); + sqlite3_bind_int(stmt, 1, new_blocking); + sqlite3_bind_int(stmt, 2, contact_id); + if (sqlite3_step(stmt)!=SQLITE_DONE) { + goto cleanup; + } + sqlite3_finalize(stmt); + stmt = NULL; + + /* also (un)block all chats with _only_ this contact - we do not delete them to allow a non-destructive blocking->unblocking. + (Maybe, beside normal chats (type=100) we should also block group chats with only this user. + However, I'm not sure about this point; it may be confusing if the user wants to add other people; + this would result in recreating the same group...) */ + stmt = dc_sqlite3_prepare(context->sql, + "UPDATE chats SET blocked=? WHERE type=? AND id IN (SELECT chat_id FROM chats_contacts WHERE contact_id=?);"); + sqlite3_bind_int(stmt, 1, new_blocking); + sqlite3_bind_int(stmt, 2, DC_CHAT_TYPE_SINGLE); + sqlite3_bind_int(stmt, 3, contact_id); + if (sqlite3_step(stmt)!=SQLITE_DONE) { + goto cleanup; + } + + /* mark all messages from the blocked contact as being noticed (this is to remove the deaddrop popup) */ + dc_marknoticed_contact(context, contact_id); + + send_event = 1; + } + + if (send_event) { + context->cb(context, DC_EVENT_CONTACTS_CHANGED, 0, 0); + } + +cleanup: + sqlite3_finalize(stmt); + dc_contact_unref(contact); +} + + +static void cat_fingerprint(dc_strbuilder_t* ret, const char* addr, const char* fingerprint_verified, const char* fingerprint_unverified) +{ + dc_strbuilder_cat(ret, "\n\n"); + dc_strbuilder_cat(ret, addr); + dc_strbuilder_cat(ret, ":\n"); + dc_strbuilder_cat(ret, (fingerprint_verified&&fingerprint_verified[0])? fingerprint_verified : fingerprint_unverified); + + if (fingerprint_verified && fingerprint_verified[0] + && fingerprint_unverified && fingerprint_unverified[0] + && strcmp(fingerprint_verified, fingerprint_unverified)!=0) { + // might be that for verified chats the - older - verified gossiped key is used + // and for normal chats the - newer - unverified key :/ + dc_strbuilder_cat(ret, "\n\n"); + dc_strbuilder_cat(ret, addr); + dc_strbuilder_cat(ret, " (alternative):\n"); + dc_strbuilder_cat(ret, fingerprint_unverified); + } +} + + +/** + * Get encryption info for a contact. + * Get a multi-line encryption info, containing your fingerprint and the + * fingerprint of the contact, used eg. to compare the fingerprints for a simple out-of-band verification. + * + * @memberof dc_context_t + * @param context The context object as created by dc_context_new(). + * @param contact_id ID of the contact to get the encryption info for. + * @return multi-line text, must be free()'d after usage. + */ +char* dc_get_contact_encrinfo(dc_context_t* context, uint32_t contact_id) +{ + dc_loginparam_t* loginparam = dc_loginparam_new(); + dc_contact_t* contact = dc_contact_new(context); + dc_apeerstate_t* peerstate = dc_apeerstate_new(context); + dc_key_t* self_key = dc_key_new(); + char* fingerprint_self = NULL; + char* fingerprint_other_verified = NULL; + char* fingerprint_other_unverified = NULL; + char* p = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + goto cleanup; + } + + dc_strbuilder_t ret; + dc_strbuilder_init(&ret, 0); + + if (!dc_contact_load_from_db(contact, context->sql, contact_id)) { + goto cleanup; + } + dc_apeerstate_load_by_addr(peerstate, context->sql, contact->addr); + dc_loginparam_read(loginparam, context->sql, "configured_"); + + dc_key_load_self_public(self_key, loginparam->addr, context->sql); + + if (dc_apeerstate_peek_key(peerstate, DC_NOT_VERIFIED)) + { + // E2E available :) + p = dc_stock_str(context, peerstate->prefer_encrypt==DC_PE_MUTUAL? DC_STR_E2E_PREFERRED : DC_STR_E2E_AVAILABLE); dc_strbuilder_cat(&ret, p); free(p); + + if (self_key->binary==NULL) { + dc_pgp_rand_seed(context, peerstate->addr, strlen(peerstate->addr) /*just some random data*/); + dc_ensure_secret_key_exists(context); + dc_key_load_self_public(self_key, loginparam->addr, context->sql); + } + + dc_strbuilder_cat(&ret, " "); + p = dc_stock_str(context, DC_STR_FINGERPRINTS); dc_strbuilder_cat(&ret, p); free(p); + dc_strbuilder_cat(&ret, ":"); + + fingerprint_self = dc_key_get_formatted_fingerprint(self_key); + fingerprint_other_verified = dc_key_get_formatted_fingerprint(dc_apeerstate_peek_key(peerstate, DC_BIDIRECT_VERIFIED)); + fingerprint_other_unverified = dc_key_get_formatted_fingerprint(dc_apeerstate_peek_key(peerstate, DC_NOT_VERIFIED)); + + if (strcmp(loginparam->addr, peerstate->addr)<0) { + cat_fingerprint(&ret, loginparam->addr, fingerprint_self, NULL); + cat_fingerprint(&ret, peerstate->addr, fingerprint_other_verified, fingerprint_other_unverified); + } + else { + cat_fingerprint(&ret, peerstate->addr, fingerprint_other_verified, fingerprint_other_unverified); + cat_fingerprint(&ret, loginparam->addr, fingerprint_self, NULL); + } + } + else + { + // No E2E available + if (!(loginparam->server_flags&DC_LP_IMAP_SOCKET_PLAIN) + && !(loginparam->server_flags&DC_LP_SMTP_SOCKET_PLAIN)) + { + p = dc_stock_str(context, DC_STR_ENCR_TRANSP); dc_strbuilder_cat(&ret, p); free(p); + } + else + { + p = dc_stock_str(context, DC_STR_ENCR_NONE); dc_strbuilder_cat(&ret, p); free(p); + } + } + +cleanup: + dc_apeerstate_unref(peerstate); + dc_contact_unref(contact); + dc_loginparam_unref(loginparam); + dc_key_unref(self_key); + free(fingerprint_self); + free(fingerprint_other_verified); + free(fingerprint_other_unverified); + return ret.buf; +} + + +/** + * Delete a contact. The contact is deleted from the local device. It may happen that this is not + * possible as the contact is in use. In this case, the contact can be blocked. + * + * May result in a #DC_EVENT_CONTACTS_CHANGED event. + * + * @memberof dc_context_t + * @param context The context object as created by dc_context_new(). + * @param contact_id ID of the contact to delete. + * @return 1=success, 0=error + */ +int dc_delete_contact(dc_context_t* context, uint32_t contact_id) +{ + int success = 0; + sqlite3_stmt* stmt = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || contact_id<=DC_CONTACT_ID_LAST_SPECIAL) { + goto cleanup; + } + + /* we can only delete contacts that are not in use anywhere; this function is mainly for the user who has just + created an contact manually and wants to delete it a moment later */ + stmt = dc_sqlite3_prepare(context->sql, + "SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;"); + sqlite3_bind_int(stmt, 1, contact_id); + if (sqlite3_step(stmt)!=SQLITE_ROW || sqlite3_column_int(stmt, 0) >= 1) { + goto cleanup; + } + sqlite3_finalize(stmt); + stmt = NULL; + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;"); + sqlite3_bind_int(stmt, 1, contact_id); + sqlite3_bind_int(stmt, 2, contact_id); + if (sqlite3_step(stmt)!=SQLITE_ROW || sqlite3_column_int(stmt, 0) >= 1) { + goto cleanup; + } + sqlite3_finalize(stmt); + stmt = NULL; + + stmt = dc_sqlite3_prepare(context->sql, + "DELETE FROM contacts WHERE id=?;"); + sqlite3_bind_int(stmt, 1, contact_id); + if (sqlite3_step(stmt)!=SQLITE_DONE) { + goto cleanup; + } + + context->cb(context, DC_EVENT_CONTACTS_CHANGED, 0, 0); + + success = 1; + +cleanup: + sqlite3_finalize(stmt); + return success; +} diff --git a/src/dc_contact.h b/src/dc_contact.h index a9690a6f..4a3cf449 100644 --- a/src/dc_contact.h +++ b/src/dc_contact.h @@ -55,8 +55,6 @@ struct _dc_contact int origin; /**< The origin/source of the contact. One of the DC_ORIGIN_* constants. */ }; - -/* library-internal */ #define DC_ORIGIN_INCOMING_UNKNOWN_FROM 0x10 /* From: of incoming messages of unknown sender */ #define DC_ORIGIN_INCOMING_UNKNOWN_CC 0x20 /* Cc: of incoming messages of unknown sender */ #define DC_ORIGIN_INCOMING_UNKNOWN_TO 0x40 /* To: of incoming messages of unknown sender */ @@ -78,19 +76,31 @@ struct _dc_contact #define DC_ORIGIN_MIN_VERIFIED (DC_ORIGIN_INCOMING_REPLY_TO) /* contacts with at least this origin value are verified and known not to be spam */ #define DC_ORIGIN_MIN_START_NEW_NCHAT (0x7FFFFFFF) /* contacts with at least this origin value start a new "normal" chat, defaults to off */ - int dc_contact_load_from_db (dc_contact_t*, dc_sqlite3_t*, uint32_t contact_id); int dc_contact_n_peerstate_are_verified (const dc_contact_t*, const dc_apeerstate_t*); + +// Working with names void dc_normalize_name (char* full_name); char* dc_get_first_name (const char* full_name); + +// Working with e-mail-addresses int dc_addr_cmp (const char* addr1, const char* addr2); char* dc_addr_normalize (const char* addr); int dc_addr_equals_self (dc_context_t*, const char* addr); int dc_addr_equals_contact (dc_context_t*, const char* addr, uint32_t contact_id); +// Context functions to work with contacts +size_t dc_get_real_contact_cnt (dc_context_t*); +uint32_t dc_add_or_lookup_contact (dc_context_t*, const char* display_name /*can be NULL*/, const char* addr_spec, int origin, int* sth_modified); +int dc_get_contact_origin (dc_context_t*, uint32_t contact_id, int* ret_blocked); +int dc_is_contact_blocked (dc_context_t*, uint32_t contact_id); +int dc_real_contact_exists (dc_context_t*, uint32_t contact_id); +void dc_scaleup_contact_origin (dc_context_t*, uint32_t contact_id, int origin); + + #ifdef __cplusplus } /* /extern "C" */ #endif diff --git a/src/dc_context.c b/src/dc_context.c index fa555b59..686bd2f0 100644 --- a/src/dc_context.c +++ b/src/dc_context.c @@ -37,11 +37,6 @@ #include "dc_apeerstate.h" -/******************************************************************************* - * Main interface - ******************************************************************************/ - - static uintptr_t cb_dummy(dc_context_t* context, int event, uintptr_t data1, uintptr_t data2) { return 0; @@ -462,8 +457,8 @@ char* dc_get_info(dc_context_t* context) int dbversion = 0; int mdns_enabled = 0; int e2ee_enabled = 0; - int prv_key_count = 0; - int pub_key_count = 0; + int prv_key_cnt = 0; + int pub_key_cnt = 0; dc_key_t* self_public = dc_key_new(); dc_strbuilder_t ret; @@ -497,12 +492,12 @@ char* dc_get_info(dc_context_t* context) sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, "SELECT COUNT(*) FROM keypairs;"); sqlite3_step(stmt); - prv_key_count = sqlite3_column_int(stmt, 0); + prv_key_cnt = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); stmt = dc_sqlite3_prepare(context->sql, "SELECT COUNT(*) FROM acpeerstates;"); sqlite3_step(stmt); - pub_key_count = sqlite3_column_int(stmt, 0); + pub_key_cnt = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); if (dc_key_load_self_public(self_public, l2->addr, context->sql)) { @@ -551,7 +546,7 @@ char* dc_get_info(dc_context_t* context) , e2ee_enabled , DC_E2EE_DEFAULT_ENABLED - , prv_key_count, pub_key_count, fingerprint_str + , prv_key_cnt, pub_key_cnt, fingerprint_str , DC_VERSION_STR , SQLITE_VERSION, sqlite3_threadsafe() , libetpan_get_version_major(), libetpan_get_version_minor() @@ -590,452 +585,10 @@ char* dc_get_info(dc_context_t* context) /******************************************************************************* - * Handle chatlists + * Search ******************************************************************************/ -int dc_get_archived_count(dc_context_t* context) -{ - int ret = 0; - sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, - "SELECT COUNT(*) FROM chats WHERE blocked=0 AND archived=1;"); - if (sqlite3_step(stmt)==SQLITE_ROW) { - ret = sqlite3_column_int(stmt, 0); - } - sqlite3_finalize(stmt); - return ret; -} - - -/** - * Get a list of chats. The list can be filtered by query parameters. - * To get the chat messages, use dc_get_chat_msgs(). - * - * @memberof dc_context_t - * @param context The context object as returned by dc_context_new() - * @param listflags A combination of flags: - * - if the flag DC_GCL_ARCHIVED_ONLY is set, only archived chats are returned. - * if DC_GCL_ARCHIVED_ONLY is not set, only unarchived chats are returned and - * the pseudo-chat DC_CHAT_ID_ARCHIVED_LINK is added if there are _any_ archived - * chats - * - if the flag DC_GCL_NO_SPECIALS is set, deaddrop and archive link are not added - * to the list (may be used eg. for selecting chats on forwarding, the flag is - * not needed when DC_GCL_ARCHIVED_ONLY is already set) - * @param query_str An optional query for filtering the list. Only chats matching this query - * are returned. Give NULL for no filtering. - * @param query_id An optional contact ID for filtering the list. Only chats including this contact ID - * are returned. Give 0 for no filtering. - * @return A chatlist as an dc_chatlist_t object. Must be freed using - * dc_chatlist_unref() when no longer used - */ -dc_chatlist_t* dc_get_chatlist(dc_context_t* context, int listflags, const char* query_str, uint32_t query_id) -{ - int success = 0; - dc_chatlist_t* obj = dc_chatlist_new(context); - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - goto cleanup; - } - - if (!dc_chatlist_load_from_db(obj, listflags, query_str, query_id)) { - goto cleanup; - } - - success = 1; - -cleanup: - if (success) { - return obj; - } - else { - dc_chatlist_unref(obj); - return NULL; - } -} - - -/******************************************************************************* - * Handle chats - ******************************************************************************/ - - -/** - * Get chat object by a chat ID. - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param chat_id The ID of the chat to get the chat object for. - * @return A chat object of the type dc_chat_t, must be freed using dc_chat_unref() when done. - */ -dc_chat_t* dc_get_chat(dc_context_t* context, uint32_t chat_id) -{ - int success = 0; - dc_chat_t* obj = dc_chat_new(context); - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - goto cleanup; - } - - if (!dc_chat_load_from_db(obj, chat_id)) { - goto cleanup; - } - - success = 1; - -cleanup: - if (success) { - return obj; - } - else { - dc_chat_unref(obj); - return NULL; - } -} - - -/** - * Mark all messages in a chat as _noticed_. - * _Noticed_ messages are no longer _fresh_ and do not count as being unseen. - * IMAP/MDNs is not done for noticed messages. See also dc_marknoticed_contact() - * and dc_markseen_msgs() - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param chat_id The chat ID of which all messages should be marked as being noticed. - * @return None. - */ -void dc_marknoticed_chat(dc_context_t* context, uint32_t chat_id) -{ - /* marking a chat as "seen" is done by marking all fresh chat messages as "noticed" - - "noticed" messages are not counted as being unread but are still waiting for being marked as "seen" using dc_markseen_msgs() */ - sqlite3_stmt* stmt; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - return; - } - - stmt = dc_sqlite3_prepare(context->sql, - "UPDATE msgs SET state=" DC_STRINGIFY(DC_STATE_IN_NOTICED) " WHERE chat_id=? AND state=" DC_STRINGIFY(DC_STATE_IN_FRESH) ";"); - sqlite3_bind_int(stmt, 1, chat_id); - sqlite3_step(stmt); - sqlite3_finalize(stmt); -} - - -/** - * Check, if there is a normal chat with a given contact. - * To get the chat messages, use dc_get_chat_msgs(). - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param contact_id The contact ID to check. - * @return If there is a normal chat with the given contact_id, this chat_id is - * returned. If there is no normal chat with the contact_id, the function - * returns 0. - */ -uint32_t dc_get_chat_id_by_contact_id(dc_context_t* context, uint32_t contact_id) -{ - uint32_t chat_id = 0; - int chat_id_blocked = 0; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - return 0; - } - - dc_lookup_real_nchat_by_contact_id(context, contact_id, &chat_id, &chat_id_blocked); - - return chat_id_blocked? 0 : chat_id; /* from outside view, chats only existing in the deaddrop do not exist */ -} - - -uint32_t dc_get_chat_id_by_grpid(dc_context_t* context, const char* grpid, int* ret_blocked, int* ret_verified) -{ - uint32_t chat_id = 0; - sqlite3_stmt* stmt = NULL; - - if(ret_blocked) { *ret_blocked = 0; } - if(ret_verified) { *ret_verified = 0; } - - if (context==NULL || grpid==NULL) { - goto cleanup; - } - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT id, blocked, type FROM chats WHERE grpid=?;"); - sqlite3_bind_text (stmt, 1, grpid, -1, SQLITE_STATIC); - if (sqlite3_step(stmt)==SQLITE_ROW) { - chat_id = sqlite3_column_int(stmt, 0); - if(ret_blocked) { *ret_blocked = sqlite3_column_int(stmt, 1); } - if(ret_verified) { *ret_verified = (sqlite3_column_int(stmt, 2)==DC_CHAT_TYPE_VERIFIED_GROUP); } - } - -cleanup: - sqlite3_finalize(stmt); - return chat_id; -} - - -/** - * Create a normal chat with a single user. To create group chats, - * see dc_create_group_chat(). - * - * If there is already an exitant chat, this ID is returned and no new chat is - * crated. If there is no existant chat with the user, a new chat is created; - * this new chat may already contain messages, eg. from the deaddrop, to get the - * chat messages, use dc_get_chat_msgs(). - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param contact_id The contact ID to create the chat for. If there is already - * a chat with this contact, the already existing ID is returned. - * @return The created or reused chat ID on success. 0 on errors. - */ -uint32_t dc_create_chat_by_contact_id(dc_context_t* context, uint32_t contact_id) -{ - uint32_t chat_id = 0; - int chat_blocked = 0; - int send_event = 0; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - return 0; - } - - dc_lookup_real_nchat_by_contact_id(context, contact_id, &chat_id, &chat_blocked); - if (chat_id) { - if (chat_blocked) { - dc_unblock_chat(context, chat_id); /* unblock chat (typically move it from the deaddrop to view) */ - send_event = 1; - } - goto cleanup; /* success */ - } - - if (0==dc_real_contact_exists(context, contact_id) && contact_id!=DC_CONTACT_ID_SELF) { - dc_log_warning(context, 0, "Cannot create chat, contact %i does not exist.", (int)contact_id); - goto cleanup; - } - - dc_create_or_lookup_nchat_by_contact_id(context, contact_id, DC_CHAT_NOT_BLOCKED, &chat_id, NULL); - if (chat_id) { - send_event = 1; - } - - dc_scaleup_contact_origin(context, contact_id, DC_ORIGIN_CREATE_CHAT); - -cleanup: - if (send_event) { - context->cb(context, DC_EVENT_MSGS_CHANGED, 0, 0); - } - - return chat_id; -} - - -/** - * Create a normal chat or a group chat by a messages ID that comes typically - * from the deaddrop, DC_CHAT_ID_DEADDROP (1). - * - * If the given message ID already belongs to a normal chat or to a group chat, - * the chat ID of this chat is returned and no new chat is created. - * If a new chat is created, the given message ID is moved to this chat, however, - * there may be more messages moved to the chat from the deaddrop. To get the - * chat messages, use dc_get_chat_msgs(). - * - * If the user is asked before creation, he should be - * asked whether he wants to chat with the _contact_ belonging to the message; - * the group names may be really weired when take from the subject of implicit - * groups and this may look confusing. - * - * Moreover, this function also scales up the origin of the contact belonging - * to the message and, depending on the contacts origin, messages from the - * same group may be shown or not - so, all in all, it is fine to show the - * contact name only. - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param msg_id The message ID to create the chat for. - * @return The created or reused chat ID on success. 0 on errors. - */ -uint32_t dc_create_chat_by_msg_id(dc_context_t* context, uint32_t msg_id) -{ - uint32_t chat_id = 0; - int send_event = 0; - dc_msg_t* msg = dc_msg_new(); - dc_chat_t* chat = dc_chat_new(context); - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - goto cleanup; - } - - if (!dc_msg_load_from_db(msg, context, msg_id) - || !dc_chat_load_from_db(chat, msg->chat_id) - || chat->id<=DC_CHAT_ID_LAST_SPECIAL) { - goto cleanup; - } - - chat_id = chat->id; - - if (chat->blocked) { - dc_unblock_chat(context, chat->id); - send_event = 1; - } - - dc_scaleup_contact_origin(context, msg->from_id, DC_ORIGIN_CREATE_CHAT); - -cleanup: - dc_msg_unref(msg); - dc_chat_unref(chat); - if (send_event) { - context->cb(context, DC_EVENT_MSGS_CHANGED, 0, 0); - } - return chat_id; -} - - -/** - * Returns all message IDs of the given types in a chat. Typically used to show - * a gallery. The result must be dc_array_unref()'d - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param chat_id The chat ID to get all messages with media from. - * @param msg_type Specify a message type to query here, one of the DC_MSG_* constats. - * @param or_msg_type Another message type to return, one of the DC_MSG_* constats. - * The function will return both types then. 0 if you need only one. - * @return An array with messages from the given chat ID that have the wanted message types. - */ -dc_array_t* dc_get_chat_media(dc_context_t* context, uint32_t chat_id, int msg_type, int or_msg_type) -{ - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - return NULL; - } - - dc_array_t* ret = dc_array_new(context, 100); - - sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, - "SELECT id FROM msgs WHERE chat_id=? AND (type=? OR type=?) ORDER BY timestamp, id;"); - sqlite3_bind_int(stmt, 1, chat_id); - sqlite3_bind_int(stmt, 2, msg_type); - sqlite3_bind_int(stmt, 3, or_msg_type>0? or_msg_type : msg_type); - while (sqlite3_step(stmt)==SQLITE_ROW) { - dc_array_add_id(ret, sqlite3_column_int(stmt, 0)); - } - sqlite3_finalize(stmt); - - return ret; -} - - -/** - * Get next/previous message of the same type. - * Typically used to implement the "next" and "previous" buttons on a media - * player playing eg. voice messages. - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param curr_msg_id This is the current (image) message displayed. - * @param dir 1=get the next (image) message, -1=get the previous one. - * @return Returns the message ID that should be played next. The - * returned message is in the same chat as the given one and has the same type. - * Typically, this result is passed again to dc_get_next_media() - * later on the next swipe. If there is not next/previous message, the function returns 0. - */ -uint32_t dc_get_next_media(dc_context_t* context, uint32_t curr_msg_id, int dir) -{ - uint32_t ret_msg_id = 0; - dc_msg_t* msg = dc_msg_new(); - dc_array_t* list = NULL; - int i = 0; - int cnt = 0; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - goto cleanup; - } - - if (!dc_msg_load_from_db(msg, context, curr_msg_id)) { - goto cleanup; - } - - if ((list=dc_get_chat_media(context, msg->chat_id, msg->type, 0))==NULL) { - goto cleanup; - } - - cnt = dc_array_get_cnt(list); - for (i = 0; i < cnt; i++) { - if (curr_msg_id==dc_array_get_id(list, i)) - { - if (dir > 0) { - /* get the next message from the current position */ - if (i+1 < cnt) { - ret_msg_id = dc_array_get_id(list, i+1); - } - } - else if (dir < 0) { - /* get the previous message from the current position */ - if (i-1 >= 0) { - ret_msg_id = dc_array_get_id(list, i-1); - } - } - break; - } - } - - -cleanup: - dc_array_unref(list); - dc_msg_unref(msg); - return ret_msg_id; -} - - -/** - * Get contact IDs belonging to a chat. - * - * - for normal chats, the function always returns exactly one contact, - * DC_CONTACT_ID_SELF is _not_ returned. - * - * - for group chats all members are returned, DC_CONTACT_ID_SELF is returned - * explicitly as it may happen that oneself gets removed from a still existing - * group - * - * - for the deaddrop, all contacts are returned, DC_CONTACT_ID_SELF is not - * added - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param chat_id Chat ID to get the belonging contact IDs for. - * @return an array of contact IDs belonging to the chat; must be freed using dc_array_unref() when done. - */ -dc_array_t* dc_get_chat_contacts(dc_context_t* context, uint32_t chat_id) -{ - /* Normal chats do not include SELF. Group chats do (as it may happen that one is deleted from a - groupchat but the chats stays visible, moreover, this makes displaying lists easier) */ - dc_array_t* ret = dc_array_new(context, 100); - sqlite3_stmt* stmt = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - goto cleanup; - } - - if (chat_id==DC_CHAT_ID_DEADDROP) { - goto cleanup; /* we could also create a list for all contacts in the deaddrop by searching contacts belonging to chats with chats.blocked=2, however, currently this is not needed */ - } - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT cc.contact_id FROM chats_contacts cc" - " LEFT JOIN contacts c ON c.id=cc.contact_id" - " WHERE cc.chat_id=?" - " ORDER BY c.id=1, LOWER(c.name||c.addr), c.id;"); - sqlite3_bind_int(stmt, 1, chat_id); - while (sqlite3_step(stmt)==SQLITE_ROW) { - dc_array_add_id(ret, sqlite3_column_int(stmt, 0)); - } - -cleanup: - sqlite3_finalize(stmt); - return ret; -} - - /** * Returns the message IDs of all _fresh_ messages of any chat. Typically used for implementing * notification summaries. @@ -1087,116 +640,6 @@ cleanup: } -/** - * Get all message IDs belonging to a chat. - * Optionally, some special markers added to the ID-array may help to - * implement virtual lists. - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param chat_id The chat ID of which the messages IDs should be queried. - * @param flags If set to DC_GCM_ADD_DAY_MARKER, the marker DC_MSG_ID_DAYMARKER will - * be added before each day (regarding the local timezone). Set this to 0 if you do not want this behaviour. - * @param marker1before An optional message ID. If set, the id DC_MSG_ID_MARKER1 will be added just - * before the given ID in the returned array. Set this to 0 if you do not want this behaviour. - * @return Array of message IDs, must be dc_array_unref()'d when no longer used. - */ -dc_array_t* dc_get_chat_msgs(dc_context_t* context, uint32_t chat_id, uint32_t flags, uint32_t marker1before) -{ - //clock_t start = clock(); - - int success = 0; - dc_array_t* ret = dc_array_new(context, 512); - sqlite3_stmt* stmt = NULL; - - uint32_t curr_id; - time_t curr_local_timestamp; - int curr_day, last_day = 0; - long cnv_to_local = dc_gm2local_offset(); - #define SECONDS_PER_DAY 86400 - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || ret==NULL) { - goto cleanup; - } - - if (chat_id==DC_CHAT_ID_DEADDROP) - { - stmt = dc_sqlite3_prepare(context->sql, - "SELECT m.id, m.timestamp" - " FROM msgs m" - " LEFT JOIN chats ON m.chat_id=chats.id" - " LEFT JOIN contacts ON m.from_id=contacts.id" - " WHERE m.from_id!=" DC_STRINGIFY(DC_CONTACT_ID_SELF) - " AND m.hidden=0 " - " AND chats.blocked=" DC_STRINGIFY(DC_CHAT_DEADDROP_BLOCKED) - " AND contacts.blocked=0" - " ORDER BY m.timestamp,m.id;"); /* the list starts with the oldest message*/ - } - else if (chat_id==DC_CHAT_ID_STARRED) - { - stmt = dc_sqlite3_prepare(context->sql, - "SELECT m.id, m.timestamp" - " FROM msgs m" - " LEFT JOIN contacts ct ON m.from_id=ct.id" - " WHERE m.starred=1 " - " AND m.hidden=0 " - " AND ct.blocked=0" - " ORDER BY m.timestamp,m.id;"); /* the list starts with the oldest message*/ - } - else - { - stmt = dc_sqlite3_prepare(context->sql, - "SELECT m.id, m.timestamp" - " FROM msgs m" - //" LEFT JOIN contacts ct ON m.from_id=ct.id" - " WHERE m.chat_id=? " - " AND m.hidden=0 " - //" AND ct.blocked=0" -- we hide blocked-contacts from starred and deaddrop, but we have to show them in groups (otherwise it may be hard to follow conversation, wa and tg do the same. however, maybe this needs discussion some time :) - " ORDER BY m.timestamp,m.id;"); /* the list starts with the oldest message*/ - sqlite3_bind_int(stmt, 1, chat_id); - } - - while (sqlite3_step(stmt)==SQLITE_ROW) - { - curr_id = sqlite3_column_int(stmt, 0); - - /* add user marker */ - if (curr_id==marker1before) { - dc_array_add_id(ret, DC_MSG_ID_MARKER1); - } - - /* add daymarker, if needed */ - if (flags&DC_GCM_ADDDAYMARKER) { - curr_local_timestamp = (time_t)sqlite3_column_int64(stmt, 1) + cnv_to_local; - curr_day = curr_local_timestamp/SECONDS_PER_DAY; - if (curr_day!=last_day) { - dc_array_add_id(ret, DC_MSG_ID_DAYMARKER); - last_day = curr_day; - } - } - - dc_array_add_id(ret, curr_id); - } - - success = 1; - -cleanup: - sqlite3_finalize(stmt); - - //dc_log_info(context, 0, "Message list for chat #%i created in %.3f ms.", chat_id, (double)(clock()-start)*1000.0/CLOCKS_PER_SEC); - - if (success) { - return ret; - } - else { - if (ret) { - dc_array_unref(ret); - } - return NULL; - } -} - - /** * Search messages containing the given query string. * Searching can be done globally (chat_id=0) or in a specified chat only (chat_id @@ -1297,3012 +740,3 @@ cleanup: return NULL; } } - - -/** - * Save a draft for a chat. - * - * To get the draft for a given chat ID, use dc_chat_get_draft(). - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @param chat_id The chat ID to save the draft for. - * @param msg The message text to save as a draft. - * @return None. - */ -void dc_set_draft(dc_context_t* context, uint32_t chat_id, const char* msg) -{ - sqlite3_stmt* stmt = NULL; - dc_chat_t* chat = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - goto cleanup; - } - - if ((chat=dc_get_chat(context, chat_id))==NULL) { - goto cleanup; - } - - if (msg && msg[0]==0) { - msg = NULL; // an empty draft is no draft - } - - if (chat->draft_text==NULL && msg==NULL - && chat->draft_timestamp==0) { - goto cleanup; // nothing to do - there is no old and no new draft - } - - if (chat->draft_timestamp && chat->draft_text && msg && strcmp(chat->draft_text, msg)==0) { - goto cleanup; // for equal texts, we do not update the timestamp - } - - // save draft in object - NULL or empty: clear draft - free(chat->draft_text); - chat->draft_text = msg? dc_strdup(msg) : NULL; - chat->draft_timestamp = msg? time(NULL) : 0; - - // save draft in database - stmt = dc_sqlite3_prepare(context->sql, - "UPDATE chats SET draft_timestamp=?, draft_txt=? WHERE id=?;"); - sqlite3_bind_int64(stmt, 1, chat->draft_timestamp); - sqlite3_bind_text (stmt, 2, chat->draft_text? chat->draft_text : "", -1, SQLITE_STATIC); - sqlite3_bind_int (stmt, 3, chat->id); - sqlite3_step(stmt); - - context->cb(context, DC_EVENT_MSGS_CHANGED, 0, 0); - -cleanup: - sqlite3_finalize(stmt); - dc_chat_unref(chat); -} - - -uint32_t dc_get_last_deaddrop_fresh_msg(dc_context_t* context) -{ - uint32_t ret = 0; - sqlite3_stmt* stmt = NULL; - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT m.id " - " FROM msgs m " - " LEFT JOIN chats c ON c.id=m.chat_id " - " WHERE m.state=" DC_STRINGIFY(DC_STATE_IN_FRESH) - " AND m.hidden=0 " - " AND c.blocked=" DC_STRINGIFY(DC_CHAT_DEADDROP_BLOCKED) - " ORDER BY m.timestamp DESC, m.id DESC;"); /* we have an index over the state-column, this should be sufficient as there are typically only few fresh messages */ - - if (sqlite3_step(stmt)!=SQLITE_ROW) { - goto cleanup; - } - - ret = sqlite3_column_int(stmt, 0); - -cleanup: - sqlite3_finalize(stmt); - return ret; -} - - -size_t dc_get_chat_cnt(dc_context_t* context) -{ - size_t ret = 0; - sqlite3_stmt* stmt = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || context->sql->cobj==NULL) { - goto cleanup; /* no database, no chats - this is no error (needed eg. for information) */ - } - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT COUNT(*) FROM chats WHERE id>" DC_STRINGIFY(DC_CHAT_ID_LAST_SPECIAL) " AND blocked=0;"); - if (sqlite3_step(stmt)!=SQLITE_ROW) { - goto cleanup; - } - - ret = sqlite3_column_int(stmt, 0); - -cleanup: - sqlite3_finalize(stmt); - return ret; -} - - -void dc_lookup_real_nchat_by_contact_id(dc_context_t* context, uint32_t contact_id, uint32_t* ret_chat_id, int* ret_chat_blocked) -{ - /* checks for "real" chats or self-chat */ - sqlite3_stmt* stmt = NULL; - - if (ret_chat_id) { *ret_chat_id = 0; } - if (ret_chat_blocked) { *ret_chat_blocked = 0; } - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || context->sql->cobj==NULL) { - return; /* no database, no chats - this is no error (needed eg. for information) */ - } - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT c.id, c.blocked" - " FROM chats c" - " INNER JOIN chats_contacts j ON c.id=j.chat_id" - " WHERE c.type=" DC_STRINGIFY(DC_CHAT_TYPE_SINGLE) " AND c.id>" DC_STRINGIFY(DC_CHAT_ID_LAST_SPECIAL) " AND j.contact_id=?;"); - sqlite3_bind_int(stmt, 1, contact_id); - if (sqlite3_step(stmt)==SQLITE_ROW) { - if (ret_chat_id) { *ret_chat_id = sqlite3_column_int(stmt, 0); } - if (ret_chat_blocked) { *ret_chat_blocked = sqlite3_column_int(stmt, 1); } - } - sqlite3_finalize(stmt); -} - - -void dc_create_or_lookup_nchat_by_contact_id(dc_context_t* context, uint32_t contact_id, int create_blocked, uint32_t* ret_chat_id, int* ret_chat_blocked) -{ - uint32_t chat_id = 0; - int chat_blocked = 0; - dc_contact_t* contact = NULL; - char* chat_name = NULL; - char* q = NULL; - sqlite3_stmt* stmt = NULL; - - if (ret_chat_id) { *ret_chat_id = 0; } - if (ret_chat_blocked) { *ret_chat_blocked = 0; } - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || context->sql->cobj==NULL) { - return; /* database not opened - error */ - } - - if (contact_id==0) { - return; - } - - dc_lookup_real_nchat_by_contact_id(context, contact_id, &chat_id, &chat_blocked); - if (chat_id!=0) { - if (ret_chat_id) { *ret_chat_id = chat_id; } - if (ret_chat_blocked) { *ret_chat_blocked = chat_blocked; } - return; /* soon success */ - } - - /* get fine chat name */ - contact = dc_contact_new(context); - if (!dc_contact_load_from_db(contact, context->sql, contact_id)) { - goto cleanup; - } - - chat_name = (contact->name&&contact->name[0])? contact->name : contact->addr; - - /* create chat record; the grpid is only used to make dc_sqlite3_get_rowid() work (we cannot use last_insert_id() due multi-threading) */ - q = sqlite3_mprintf("INSERT INTO chats (type, name, param, blocked, grpid) VALUES(%i, %Q, %Q, %i, %Q)", DC_CHAT_TYPE_SINGLE, chat_name, - contact_id==DC_CONTACT_ID_SELF? "K=1" : "", create_blocked, contact->addr); - assert( DC_PARAM_SELFTALK=='K'); - stmt = dc_sqlite3_prepare(context->sql, q); - if (stmt==NULL) { - goto cleanup; - } - - if (sqlite3_step(stmt)!=SQLITE_DONE) { - goto cleanup; - } - - chat_id = dc_sqlite3_get_rowid(context->sql, "chats", "grpid", contact->addr); - - sqlite3_free(q); - q = NULL; - sqlite3_finalize(stmt); - stmt = NULL; - - /* add contact IDs to the new chat record (may be replaced by dc_add_to_chat_contacts_table()) */ - q = sqlite3_mprintf("INSERT INTO chats_contacts (chat_id, contact_id) VALUES(%i, %i)", chat_id, contact_id); - stmt = dc_sqlite3_prepare(context->sql, q); - - if (sqlite3_step(stmt)!=SQLITE_DONE) { - goto cleanup; - } - - sqlite3_free(q); - q = NULL; - sqlite3_finalize(stmt); - stmt = NULL; - -cleanup: - sqlite3_free(q); - sqlite3_finalize(stmt); - dc_contact_unref(contact); - - if (ret_chat_id) { *ret_chat_id = chat_id; } - if (ret_chat_blocked) { *ret_chat_blocked = create_blocked; } -} - - -void dc_unarchive_chat(dc_context_t* context, uint32_t chat_id) -{ - sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, - "UPDATE chats SET archived=0 WHERE id=?"); - sqlite3_bind_int (stmt, 1, chat_id); - sqlite3_step(stmt); - sqlite3_finalize(stmt); -} - - -/** - * Get the total number of messages in a chat. - * - * @memberof dc_context_t - * - * @param context The context object as returned from dc_context_new(). - * @param chat_id The ID of the chat to count the messages for. - * @return Number of total messages in the given chat. 0 for errors or empty chats. - */ -int dc_get_total_msg_count(dc_context_t* context, uint32_t chat_id) -{ - int ret = 0; - sqlite3_stmt* stmt = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - goto cleanup; - } - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT COUNT(*) FROM msgs WHERE chat_id=?;"); - sqlite3_bind_int(stmt, 1, chat_id); - if (sqlite3_step(stmt)!=SQLITE_ROW) { - goto cleanup; - } - - ret = sqlite3_column_int(stmt, 0); - -cleanup: - sqlite3_finalize(stmt); - return ret; -} - - -/** - * Get the number of _fresh_ messages in a chat. Typically used to implement - * a badge with a number in the chatlist. - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param chat_id The ID of the chat to count the messages for. - * @return Number of fresh messages in the given chat. 0 for errors or if there are no fresh messages. - */ -int dc_get_fresh_msg_count(dc_context_t* context, uint32_t chat_id) -{ - int ret = 0; - sqlite3_stmt* stmt = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - goto cleanup; - } - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT COUNT(*) FROM msgs " - " WHERE state=" DC_STRINGIFY(DC_STATE_IN_FRESH) - " AND hidden=0 " - " AND chat_id=?;"); /* we have an index over the state-column, this should be sufficient as there are typically only few fresh messages */ - sqlite3_bind_int(stmt, 1, chat_id); - - if (sqlite3_step(stmt)!=SQLITE_ROW) { - goto cleanup; - } - - ret = sqlite3_column_int(stmt, 0); - -cleanup: - sqlite3_finalize(stmt); - return ret; -} - - -/** - * Archive or unarchive a chat. - * - * Archived chats are not included in the default chatlist returned - * by dc_get_chatlist(). Instead, if there are _any_ archived chats, - * the pseudo-chat with the chat_id DC_CHAT_ID_ARCHIVED_LINK will be added the the - * end of the chatlist. - * - * - To get a list of archived chats, use dc_get_chatlist() with the flag DC_GCL_ARCHIVED_ONLY. - * - To find out the archived state of a given chat, use dc_chat_get_archived() - * - Calling this function usually results in the event #DC_EVENT_MSGS_CHANGED - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param chat_id The ID of the chat to archive or unarchive. - * @param archive 1=archive chat, 0=unarchive chat, all other values are reserved for future use - * @return None - */ -void dc_archive_chat(dc_context_t* context, uint32_t chat_id, int archive) -{ - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL || (archive!=0 && archive!=1)) { - return; - } - - sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, - "UPDATE chats SET archived=? WHERE id=?;"); - sqlite3_bind_int (stmt, 1, archive); - sqlite3_bind_int (stmt, 2, chat_id); - sqlite3_step(stmt); - sqlite3_finalize(stmt); - - context->cb(context, DC_EVENT_MSGS_CHANGED, 0, 0); -} - - -/******************************************************************************* - * Delete a chat - ******************************************************************************/ - - -/** - * Delete a chat. - * - * Messages are deleted from the device and the chat database entry is deleted. - * After that, the event #DC_EVENT_MSGS_CHANGED is posted. - * - * Things that are _not_ done implicitly: - * - * - Messages are **not deleted from the server**. - * - The chat or the contact is **not blocked**, so new messages from the user/the group may appear - * and the user may create the chat again. - * - **Groups are not left** - this would - * be unexpected as (1) deleting a normal chat also does not prevent new mails - * from arriving, (2) leaving a group requires sending a message to - * all group members - esp. for groups not used for a longer time, this is - * really unexpected when deletion results in contacting all members again, - * (3) only leaving groups is also a valid usecase. - * - * To leave a chat explicitly, use dc_remove_contact_from_chat() with - * chat_id=DC_CONTACT_ID_SELF) - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param chat_id The ID of the chat to delete. - * @return None - */ -void dc_delete_chat(dc_context_t* context, uint32_t chat_id) -{ - /* Up to 2017-11-02 deleting a group also implied leaving it, see above why we have changed this. */ - int pending_transaction = 0; - dc_chat_t* obj = dc_chat_new(context); - char* q3 = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL) { - goto cleanup; - } - - if (!dc_chat_load_from_db(obj, chat_id)) { - goto cleanup; - } - - dc_sqlite3_begin_transaction(context->sql); - pending_transaction = 1; - - q3 = sqlite3_mprintf("DELETE FROM msgs_mdns WHERE msg_id IN (SELECT id FROM msgs WHERE chat_id=%i);", chat_id); - if (!dc_sqlite3_execute(context->sql, q3)) { - goto cleanup; - } - sqlite3_free(q3); - q3 = NULL; - - q3 = sqlite3_mprintf("DELETE FROM msgs WHERE chat_id=%i;", chat_id); - if (!dc_sqlite3_execute(context->sql, q3)) { - goto cleanup; - } - sqlite3_free(q3); - q3 = NULL; - - q3 = sqlite3_mprintf("DELETE FROM chats_contacts WHERE chat_id=%i;", chat_id); - if (!dc_sqlite3_execute(context->sql, q3)) { - goto cleanup; - } - sqlite3_free(q3); - q3 = NULL; - - q3 = sqlite3_mprintf("DELETE FROM chats WHERE id=%i;", chat_id); - if (!dc_sqlite3_execute(context->sql, q3)) { - goto cleanup; - } - sqlite3_free(q3); - q3 = NULL; - - dc_sqlite3_commit(context->sql); - pending_transaction = 0; - - context->cb(context, DC_EVENT_MSGS_CHANGED, 0, 0); - -cleanup: - if (pending_transaction) { dc_sqlite3_rollback(context->sql); } - dc_chat_unref(obj); - sqlite3_free(q3); -} - - -/******************************************************************************* - * Sending messages - ******************************************************************************/ - - -static int last_msg_in_chat_encrypted(dc_sqlite3_t* sql, uint32_t chat_id) -{ - int last_is_encrypted = 0; - sqlite3_stmt* stmt = dc_sqlite3_prepare(sql, - "SELECT param " - " FROM msgs " - " WHERE timestamp=(SELECT MAX(timestamp) FROM msgs WHERE chat_id=?) " - " ORDER BY id DESC;"); - sqlite3_bind_int(stmt, 1, chat_id); - if (sqlite3_step(stmt)==SQLITE_ROW) { - dc_param_t* msg_param = dc_param_new(); - dc_param_set_packed(msg_param, (char*)sqlite3_column_text(stmt, 0)); - if (dc_param_exists(msg_param, DC_PARAM_GUARANTEE_E2EE)) { - last_is_encrypted = 1; - } - dc_param_unref(msg_param); - } - sqlite3_finalize(stmt); - return last_is_encrypted; -} - - -static uint32_t dc_send_msg_raw(dc_context_t* context, dc_chat_t* chat, const dc_msg_t* msg, time_t timestamp) -{ - char* rfc724_mid = NULL; - sqlite3_stmt* stmt = NULL; - uint32_t msg_id = 0; - uint32_t to_id = 0; - - if (!DC_CHAT_TYPE_CAN_SEND(chat->type)) { - dc_log_error(context, 0, "Cannot send to chat type #%i.", chat->type); - goto cleanup; - } - - if (DC_CHAT_TYPE_IS_MULTI(chat->type) && !dc_is_contact_in_chat(context, chat->id, DC_CONTACT_ID_SELF)) { - dc_log_error(context, DC_ERROR_SELF_NOT_IN_GROUP, NULL); - goto cleanup; - } - - { - char* from = dc_sqlite3_get_config(context->sql, "configured_addr", NULL); - if (from==NULL) { - dc_log_error(context, 0, "Cannot send message, not configured."); - goto cleanup; - } - rfc724_mid = dc_create_outgoing_rfc724_mid(DC_CHAT_TYPE_IS_MULTI(chat->type)? chat->grpid : NULL, from); - free(from); - } - - if (chat->type==DC_CHAT_TYPE_SINGLE) - { - stmt = dc_sqlite3_prepare(context->sql, - "SELECT contact_id FROM chats_contacts WHERE chat_id=?;"); - sqlite3_bind_int(stmt, 1, chat->id); - if (sqlite3_step(stmt)!=SQLITE_ROW) { - dc_log_error(context, 0, "Cannot send message, contact for chat #%i not found.", chat->id); - goto cleanup; - } - to_id = sqlite3_column_int(stmt, 0); - sqlite3_finalize(stmt); - stmt = NULL; - } - else if (DC_CHAT_TYPE_IS_MULTI(chat->type)) - { - if (dc_param_get_int(chat->param, DC_PARAM_UNPROMOTED, 0)==1) { - /* mark group as being no longer unpromoted */ - dc_param_set(chat->param, DC_PARAM_UNPROMOTED, NULL); - dc_chat_update_param(chat); - } - } - - /* check if we can guarantee E2EE for this message. If we can, we won't send the message without E2EE later (because of a reset, changed settings etc. - messages may be delayed significally if there is no network present) */ - int do_guarantee_e2ee = 0; - if (context->e2ee_enabled && dc_param_get_int(msg->param, DC_PARAM_FORCE_PLAINTEXT, 0)==0) - { - int can_encrypt = 1, all_mutual = 1; /* be optimistic */ - stmt = dc_sqlite3_prepare(context->sql, - "SELECT ps.prefer_encrypted " - " FROM chats_contacts cc " - " LEFT JOIN contacts c ON cc.contact_id=c.id " - " LEFT JOIN acpeerstates ps ON c.addr=ps.addr " - " WHERE cc.chat_id=? " /* take care that this statement returns NULL rows if there is no peerstates for a chat member! */ - " AND cc.contact_id>" DC_STRINGIFY(DC_CONTACT_ID_LAST_SPECIAL) ";"); /* for DC_PARAM_SELFTALK this statement does not return any row */ - sqlite3_bind_int(stmt, 1, chat->id); - while (sqlite3_step(stmt)==SQLITE_ROW) - { - if (sqlite3_column_type(stmt, 0)==SQLITE_NULL) { - can_encrypt = 0; - all_mutual = 0; - } - else { - /* the peerstate exist, so we have either public_key or gossip_key and can encrypt potentially */ - int prefer_encrypted = sqlite3_column_int(stmt, 0); - if (prefer_encrypted!=DC_PE_MUTUAL) { - all_mutual = 0; - } - } - } - sqlite3_finalize(stmt); - stmt = NULL; - - if (can_encrypt) - { - if (all_mutual) { - do_guarantee_e2ee = 1; - } - else { - if (last_msg_in_chat_encrypted(context->sql, chat->id)) { - do_guarantee_e2ee = 1; - } - } - } - } - - if (do_guarantee_e2ee) { - dc_param_set_int(msg->param, DC_PARAM_GUARANTEE_E2EE, 1); - } - dc_param_set(msg->param, DC_PARAM_ERRONEOUS_E2EE, NULL); /* reset eg. on forwarding */ - - /* add message to the database */ - stmt = dc_sqlite3_prepare(context->sql, - "INSERT INTO msgs (rfc724_mid,chat_id,from_id,to_id, timestamp,type,state, txt,param,hidden) VALUES (?,?,?,?, ?,?,?, ?,?,?);"); - sqlite3_bind_text (stmt, 1, rfc724_mid, -1, SQLITE_STATIC); - sqlite3_bind_int (stmt, 2, chat->id); - sqlite3_bind_int (stmt, 3, DC_CONTACT_ID_SELF); - sqlite3_bind_int (stmt, 4, to_id); - sqlite3_bind_int64(stmt, 5, timestamp); - sqlite3_bind_int (stmt, 6, msg->type); - sqlite3_bind_int (stmt, 7, DC_STATE_OUT_PENDING); - sqlite3_bind_text (stmt, 8, msg->text? msg->text : "", -1, SQLITE_STATIC); - sqlite3_bind_text (stmt, 9, msg->param->packed, -1, SQLITE_STATIC); - sqlite3_bind_int (stmt, 10, msg->hidden); - if (sqlite3_step(stmt)!=SQLITE_DONE) { - dc_log_error(context, 0, "Cannot send message, cannot insert to database.", chat->id); - goto cleanup; - } - - msg_id = dc_sqlite3_get_rowid(context->sql, "msgs", "rfc724_mid", rfc724_mid); - dc_job_add(context, DC_JOB_SEND_MSG_TO_SMTP, msg_id, NULL, 0); - -cleanup: - free(rfc724_mid); - sqlite3_finalize(stmt); - return msg_id; -} - - -/** - * Send a message of any type to a chat. The given message object is not unref'd - * by the function but some fields are set up. - * - * Sends the event #DC_EVENT_MSGS_CHANGED on succcess. - * However, this does not imply, the message really reached the recipient - - * sending may be delayed eg. due to network problems. However, from your - * view, you're done with the message. Sooner or later it will find its way. - * - * To send a simple text message, you can also use dc_send_text_msg() - * which is easier to use. - * - * @private @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param chat_id Chat ID to send the message to. - * @param msg Message object to send to the chat defined by the chat ID. - * 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. - */ -uint32_t dc_send_msg_object(dc_context_t* context, uint32_t chat_id, dc_msg_t* msg) -{ - char* pathNfilename = NULL; - dc_chat_t* chat = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || msg==NULL || chat_id<=DC_CHAT_ID_LAST_SPECIAL) { - return 0; - } - - msg->id = 0; - msg->context = context; - - if (msg->type==DC_MSG_TEXT) - { - ; /* the caller should check if the message text is empty */ - } - else if (DC_MSG_NEEDS_ATTACHMENT(msg->type)) - { - pathNfilename = dc_param_get(msg->param, DC_PARAM_FILE, NULL); - if (pathNfilename) - { - /* Got an attachment. Take care, the file may not be ready in this moment! - This is useful eg. if a video should be sent and already shown as "being processed" in the chat. - In this case, the user should create an `.increation`; when the file is deleted later on, the message is sent. - (we do not use a state in the database as this would make eg. forwarding such messages much more complicated) */ - - if (msg->type==DC_MSG_FILE || msg->type==DC_MSG_IMAGE) - { - /* Correct the type, take care not to correct already very special formats as GIF or VOICE. - Typical conversions: - - from FILE to AUDIO/VIDEO/IMAGE - - from FILE/IMAGE to GIF */ - int better_type = 0; - char* better_mime = NULL; - dc_msg_guess_msgtype_from_suffix(pathNfilename, &better_type, &better_mime); - if (better_type) { - msg->type = better_type; - dc_param_set(msg->param, DC_PARAM_MIMETYPE, better_mime); - } - free(better_mime); - } - - if ((msg->type==DC_MSG_IMAGE || msg->type==DC_MSG_GIF) - && (dc_param_get_int(msg->param, DC_PARAM_WIDTH, 0)<=0 || dc_param_get_int(msg->param, DC_PARAM_HEIGHT, 0)<=0)) { - /* set width/height of images, if not yet done */ - unsigned char* buf = NULL; size_t buf_bytes; uint32_t w, h; - if (dc_read_file(pathNfilename, (void**)&buf, &buf_bytes, msg->context)) { - if (dc_get_filemeta(buf, buf_bytes, &w, &h)) { - dc_param_set_int(msg->param, DC_PARAM_WIDTH, w); - dc_param_set_int(msg->param, DC_PARAM_HEIGHT, h); - } - } - free(buf); - } - - dc_log_info(context, 0, "Attaching \"%s\" for message type #%i.", pathNfilename, (int)msg->type); - - if (msg->text) { free(msg->text); } - if (msg->type==DC_MSG_AUDIO) { - char* filename = dc_get_filename(pathNfilename); - char* author = dc_param_get(msg->param, DC_PARAM_AUTHORNAME, ""); - char* title = dc_param_get(msg->param, DC_PARAM_TRACKNAME, ""); - msg->text = dc_mprintf("%s %s %s", filename, author, title); /* for outgoing messages, also add the mediainfo. For incoming messages, this is not needed as the filename is build from these information */ - free(filename); - free(author); - free(title); - } - else if (DC_MSG_MAKE_FILENAME_SEARCHABLE(msg->type)) { - msg->text = dc_get_filename(pathNfilename); - } - else if (DC_MSG_MAKE_SUFFIX_SEARCHABLE(msg->type)) { - msg->text = dc_get_filesuffix_lc(pathNfilename); - } - } - else - { - dc_log_error(context, 0, "Attachment missing for message of type #%i.", (int)msg->type); /* should not happen */ - goto cleanup; - } - } - else - { - dc_log_error(context, 0, "Cannot send messages of type #%i.", (int)msg->type); /* should not happen */ - goto cleanup; - } - - dc_unarchive_chat(context, chat_id); - - context->smtp->log_connect_errors = 1; - - chat = dc_chat_new(context); - if (dc_chat_load_from_db(chat, chat_id)) { - msg->id = dc_send_msg_raw(context, chat, msg, dc_create_smeared_timestamp(context)); - if (msg ->id==0) { - goto cleanup; /* error already logged */ - } - } - - context->cb(context, DC_EVENT_MSGS_CHANGED, chat_id, msg->id); - -cleanup: - dc_chat_unref(chat); - free(pathNfilename); - return msg->id; -} - - -/** - * Send a simple text message a given chat. - * - * Sends the event #DC_EVENT_MSGS_CHANGED on succcess. - * However, this does not imply, the message really reached the recipient - - * sending may be delayed eg. due to network problems. However, from your - * view, you're done with the message. Sooner or later it will find its way. - * - * See also dc_send_image_msg(). - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param chat_id Chat ID to send the text message to. - * @param text_to_send Text to send to the chat defined by the chat ID. - * @return The ID of the message that is about being sent. - */ -uint32_t dc_send_text_msg(dc_context_t* context, uint32_t chat_id, const char* text_to_send) -{ - dc_msg_t* msg = dc_msg_new(); - uint32_t ret = 0; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL || text_to_send==NULL) { - goto cleanup; - } - - msg->type = DC_MSG_TEXT; - msg->text = dc_strdup(text_to_send); - - ret = dc_send_msg_object(context, chat_id, msg); - -cleanup: - dc_msg_unref(msg); - return ret; -} - - -/** - * Send an image to a chat. - * - * Sends the event #DC_EVENT_MSGS_CHANGED on succcess. - * However, this does not imply, the message really reached the recipient - - * sending may be delayed eg. due to network problems. However, from your - * view, you're done with the message. Sooner or later it will find its way. - * - * See also dc_send_text_msg(). - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param chat_id Chat ID to send the image to. - * @param file Full path of the image file to send. The core may make a copy of the file. - * @param filemime Mime type of the file to send. NULL if you don't know or don't care. - * @param width Width in pixel of the file. 0 if you don't know or don't care. - * @param height Width in pixel of the file. 0 if you don't know or don't care. - * @return The ID of the message that is about being sent. - */ -uint32_t dc_send_image_msg(dc_context_t* context, uint32_t chat_id, const char* file, const char* filemime, int width, int height) -{ - dc_msg_t* msg = dc_msg_new(); - uint32_t ret = 0; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL || file==NULL) { - goto cleanup; - } - - msg->type = DC_MSG_IMAGE; - dc_param_set (msg->param, DC_PARAM_FILE, file); - dc_param_set_int(msg->param, DC_PARAM_WIDTH, width); /* set in sending job, if 0 */ - dc_param_set_int(msg->param, DC_PARAM_HEIGHT, height); /* set in sending job, if 0 */ - - ret = dc_send_msg_object(context, chat_id, msg); - -cleanup: - dc_msg_unref(msg); - return ret; - -} - - -/** - * Send a video to a chat. - * - * Sends the event #DC_EVENT_MSGS_CHANGED on succcess. - * However, this does not imply, the message really reached the recipient - - * sending may be delayed eg. due to network problems. However, from your - * view, you're done with the message. Sooner or later it will find its way. - * - * See also dc_send_image_msg(). - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param chat_id Chat ID to send the video to. - * @param file Full path of the video file to send. The core may make a copy of the file. - * @param filemime Mime type of the file to send. NULL if you don't know or don't care. - * @param width Width in video of the file, if known. 0 if you don't know or don't care. - * @param height Width in video of the file, if known. 0 if you don't know or don't care. - * @param duration Length of the video in milliseconds. 0 if you don't know or don't care. - * @return The ID of the message that is about being sent. - */ -uint32_t dc_send_video_msg(dc_context_t* context, uint32_t chat_id, const char* file, const char* filemime, int width, int height, int duration) -{ - dc_msg_t* msg = dc_msg_new(); - uint32_t ret = 0; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL || file==NULL) { - goto cleanup; - } - - msg->type = DC_MSG_VIDEO; - dc_param_set (msg->param, DC_PARAM_FILE, file); - dc_param_set (msg->param, DC_PARAM_MIMETYPE, filemime); - dc_param_set_int(msg->param, DC_PARAM_WIDTH, width); - dc_param_set_int(msg->param, DC_PARAM_HEIGHT, height); - dc_param_set_int(msg->param, DC_PARAM_DURATION, duration); - - ret = dc_send_msg_object(context, chat_id, msg); - -cleanup: - dc_msg_unref(msg); - return ret; - -} - - -/** - * Send a voice message to a chat. Voice messages are messages just recorded though the device microphone. - * For sending music or other audio data, use dc_send_audio_msg(). - * - * Sends the event #DC_EVENT_MSGS_CHANGED on succcess. - * However, this does not imply, the message really reached the recipient - - * sending may be delayed eg. due to network problems. However, from your - * view, you're done with the message. Sooner or later it will find its way. - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param chat_id Chat ID to send the voice message to. - * @param file Full path of the file to send. The core may make a copy of the file. - * @param filemime Mime type of the file to send. NULL if you don't know or don't care. - * @param duration Length of the voice message in milliseconds. 0 if you don't know or don't care. - * @return The ID of the message that is about being sent. - */ -uint32_t dc_send_voice_msg(dc_context_t* context, uint32_t chat_id, const char* file, const char* filemime, int duration) -{ - dc_msg_t* msg = dc_msg_new(); - uint32_t ret = 0; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL || file==NULL) { - goto cleanup; - } - - msg->type = DC_MSG_VOICE; - dc_param_set (msg->param, DC_PARAM_FILE, file); - dc_param_set (msg->param, DC_PARAM_MIMETYPE, filemime); - dc_param_set_int(msg->param, DC_PARAM_DURATION, duration); - - ret = dc_send_msg_object(context, chat_id, msg); - -cleanup: - dc_msg_unref(msg); - return ret; -} - - -/** - * Send an audio file to a chat. Audio messages are eg. music tracks. - * For voice messages just recorded though the device microphone, use dc_send_voice_msg(). - * - * Sends the event #DC_EVENT_MSGS_CHANGED on succcess. - * However, this does not imply, the message really reached the recipient - - * sending may be delayed eg. due to network problems. However, from your - * view, you're done with the message. Sooner or later it will find its way. - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param chat_id Chat ID to send the audio to. - * @param file Full path of the file to send. The core may make a copy of the file. - * @param filemime Mime type of the file to send. NULL if you don't know or don't care. - * @param duration Length of the audio in milliseconds. 0 if you don't know or don't care. - * @param author Author or artist of the file. NULL if you don't know or don't care. - * @param trackname Trackname or title of the file. NULL if you don't know or don't care. - * @return The ID of the message that is about being sent. - */ -uint32_t dc_send_audio_msg(dc_context_t* context, uint32_t chat_id, const char* file, const char* filemime, int duration, const char* author, const char* trackname) -{ - dc_msg_t* msg = dc_msg_new(); - uint32_t ret = 0; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL || file==NULL) { - goto cleanup; - } - - msg->type = DC_MSG_AUDIO; - dc_param_set (msg->param, DC_PARAM_FILE, file); - dc_param_set (msg->param, DC_PARAM_MIMETYPE, filemime); - dc_param_set_int(msg->param, DC_PARAM_DURATION, duration); - dc_param_set (msg->param, DC_PARAM_AUTHORNAME, author); - dc_param_set (msg->param, DC_PARAM_TRACKNAME, trackname); - - ret = dc_send_msg_object(context, chat_id, msg); - -cleanup: - dc_msg_unref(msg); - return ret; -} - - -/** - * Send a document to a chat. Use this function to send any document or file to - * a chat. - * - * Sends the event #DC_EVENT_MSGS_CHANGED on succcess. - * However, this does not imply, the message really reached the recipient - - * sending may be delayed eg. due to network problems. However, from your - * view, you're done with the message. Sooner or later it will find its way. - * - * @memberof dc_context_t - * @param context The context object as returned from dc_context_new(). - * @param chat_id Chat ID to send the document to. - * @param file Full path of the file to send. The core may make a copy of the file. - * @param filemime Mime type of the file to send. NULL if you don't know or don't care. - * @return The ID of the message that is about being sent. - */ -uint32_t dc_send_file_msg(dc_context_t* context, uint32_t chat_id, const char* file, const char* filemime) -{ - dc_msg_t* msg = dc_msg_new(); - uint32_t ret = 0; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL || file==NULL) { - goto cleanup; - } - - msg->type = DC_MSG_FILE; - dc_param_set(msg->param, DC_PARAM_FILE, file); - dc_param_set(msg->param, DC_PARAM_MIMETYPE, filemime); - - ret = dc_send_msg_object(context, chat_id, msg); - -cleanup: - dc_msg_unref(msg); - return ret; -} - - -/** - * Send foreign contact data to a chat. - * - * Sends the name and the email address of another contact to a chat. - * The contact this may or may not be a member of the chat. - * - * Typically used to share a contact to another member or to a group of members. - * - * Internally, the function just creates an appropriate text message and sends it - * using dc_send_text_msg(). - * - * NB: The "vcard" in the function name is just an abbreviation of "visiting card" and - * is not related to the VCARD data format. - * - * @memberof dc_context_t - * @param context The context object. - * @param chat_id The chat to send the message to. - * @param contact_id The contact whichs data should be shared to the chat. - * @return Returns the ID of the message sent. - */ -uint32_t dc_send_vcard_msg(dc_context_t* context, uint32_t chat_id, uint32_t contact_id) -{ - uint32_t ret = 0; - dc_msg_t* msg = dc_msg_new(); - dc_contact_t* contact = NULL; - char* text_to_send = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL) { - goto cleanup; - } - - if ((contact=dc_get_contact(context, contact_id))==NULL) { - goto cleanup; - } - - if (contact->authname && contact->authname[0]) { - text_to_send = dc_mprintf("%s: %s", contact->authname, contact->addr); - } - else { - text_to_send = dc_strdup(contact->addr); - } - - ret = dc_send_text_msg(context, chat_id, text_to_send); - -cleanup: - dc_msg_unref(msg); - dc_contact_unref(contact); - free(text_to_send); - return ret; -} - - -/* - * Log a device message. - * Such a message is typically shown in the "middle" of the chat, the user can check this using dc_msg_is_info(). - * Texts are typically "Alice has added Bob to the group" or "Alice fingerprint verified." - */ -void dc_add_device_msg(dc_context_t* context, uint32_t chat_id, const char* text) -{ - uint32_t msg_id = 0; - sqlite3_stmt* stmt = NULL; - char* rfc724_mid = dc_create_outgoing_rfc724_mid(NULL, "@device"); - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || text==NULL) { - goto cleanup; - } - - stmt = dc_sqlite3_prepare(context->sql, - "INSERT INTO msgs (chat_id,from_id,to_id, timestamp,type,state, txt,rfc724_mid) VALUES (?,?,?, ?,?,?, ?,?);"); - sqlite3_bind_int (stmt, 1, chat_id); - sqlite3_bind_int (stmt, 2, DC_CONTACT_ID_DEVICE); - sqlite3_bind_int (stmt, 3, DC_CONTACT_ID_DEVICE); - sqlite3_bind_int64(stmt, 4, dc_create_smeared_timestamp(context)); - sqlite3_bind_int (stmt, 5, DC_MSG_TEXT); - sqlite3_bind_int (stmt, 6, DC_STATE_IN_NOTICED); - sqlite3_bind_text (stmt, 7, text, -1, SQLITE_STATIC); - sqlite3_bind_text (stmt, 8, rfc724_mid, -1, SQLITE_STATIC); - if (sqlite3_step(stmt)!=SQLITE_DONE) { - goto cleanup; - } - msg_id = dc_sqlite3_get_rowid(context->sql, "msgs", "rfc724_mid", rfc724_mid); - context->cb(context, DC_EVENT_MSGS_CHANGED, chat_id, msg_id); - -cleanup: - free(rfc724_mid); - sqlite3_finalize(stmt); -} - - -/******************************************************************************* - * Handle Group Chats - ******************************************************************************/ - - -#define IS_SELF_IN_GROUP (dc_is_contact_in_chat(context, chat_id, DC_CONTACT_ID_SELF)==1) -#define DO_SEND_STATUS_MAILS (dc_param_get_int(chat->param, DC_PARAM_UNPROMOTED, 0)==0) - - -int dc_is_group_explicitly_left(dc_context_t* context, const char* grpid) -{ - sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, "SELECT id FROM leftgrps WHERE grpid=?;"); - sqlite3_bind_text (stmt, 1, grpid, -1, SQLITE_STATIC); - int ret = (sqlite3_step(stmt)==SQLITE_ROW); - sqlite3_finalize(stmt); - return ret; -} - - -void dc_set_group_explicitly_left(dc_context_t* context, const char* grpid) -{ - if (!dc_is_group_explicitly_left(context, grpid)) - { - sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, "INSERT INTO leftgrps (grpid) VALUES(?);"); - sqlite3_bind_text (stmt, 1, grpid, -1, SQLITE_STATIC); - sqlite3_step(stmt); - sqlite3_finalize(stmt); - } -} - - -static int dc_real_group_exists(dc_context_t* context, uint32_t chat_id) -{ - // check if a group or a verified group exists under the given ID - sqlite3_stmt* stmt = NULL; - int ret = 0; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || context->sql->cobj==NULL - || chat_id<=DC_CHAT_ID_LAST_SPECIAL) { - return 0; - } - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT id FROM chats " - " WHERE id=? " - " AND (type=" DC_STRINGIFY(DC_CHAT_TYPE_GROUP) " OR type=" DC_STRINGIFY(DC_CHAT_TYPE_VERIFIED_GROUP) ");"); - sqlite3_bind_int(stmt, 1, chat_id); - if (sqlite3_step(stmt)==SQLITE_ROW) { - ret = 1; - } - sqlite3_finalize(stmt); - - return ret; -} - - -int dc_add_to_chat_contacts_table(dc_context_t* context, uint32_t chat_id, uint32_t contact_id) -{ - /* add a contact to a chat; the function does not check the type or if any of the record exist or are already added to the chat! */ - int ret = 0; - sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, - "INSERT INTO chats_contacts (chat_id, contact_id) VALUES(?, ?)"); - sqlite3_bind_int(stmt, 1, chat_id); - sqlite3_bind_int(stmt, 2, contact_id); - ret = (sqlite3_step(stmt)==SQLITE_DONE)? 1 : 0; - sqlite3_finalize(stmt); - return ret; -} - - -/** - * Create a new group chat. - * - * After creation, the group has one member with the - * ID DC_CONTACT_ID_SELF and is in _unpromoted_ state. This means, you can - * add or remove members, change the name, the group image and so on without - * messages being sent to all group members. - * - * This changes as soon as the first message is sent to the group members and - * the group becomes _promoted_. After that, all changes are synced with all - * group members by sending status message. - * - * To check, if a chat is still unpromoted, you dc_chat_is_unpromoted() - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @param verified If set to 1 the function creates a secure verfied group. - * Only secure-verified members are allowd in these groups and end-to-end-encryption is always enabled. - * @param chat_name The name of the group chat to create. - * The name may be changed later using dc_set_chat_name(). - * To find out the name of a group later, see dc_chat_get_name() - * @return The chat ID of the new group chat, 0 on errors. - */ -uint32_t dc_create_group_chat(dc_context_t* context, int verified, const char* chat_name) -{ - uint32_t chat_id = 0; - char* draft_txt = NULL; - char* grpid = NULL; - sqlite3_stmt* stmt = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_name==NULL || chat_name[0]==0) { - return 0; - } - - draft_txt = dc_stock_str_repl_string(context, DC_STR_NEWGROUPDRAFT, chat_name); - grpid = dc_create_id(); - - stmt = dc_sqlite3_prepare(context->sql, - "INSERT INTO chats (type, name, draft_timestamp, draft_txt, grpid, param) VALUES(?, ?, ?, ?, ?, 'U=1');" /*U=DC_PARAM_UNPROMOTED*/); - sqlite3_bind_int (stmt, 1, verified? DC_CHAT_TYPE_VERIFIED_GROUP : DC_CHAT_TYPE_GROUP); - sqlite3_bind_text (stmt, 2, chat_name, -1, SQLITE_STATIC); - sqlite3_bind_int64(stmt, 3, time(NULL)); - sqlite3_bind_text (stmt, 4, draft_txt, -1, SQLITE_STATIC); - sqlite3_bind_text (stmt, 5, grpid, -1, SQLITE_STATIC); - if ( sqlite3_step(stmt)!=SQLITE_DONE) { - goto cleanup; - } - - if ((chat_id=dc_sqlite3_get_rowid(context->sql, "chats", "grpid", grpid))==0) { - goto cleanup; - } - - if (dc_add_to_chat_contacts_table(context, chat_id, DC_CONTACT_ID_SELF)) { - goto cleanup; - } - -cleanup: - sqlite3_finalize(stmt); - free(draft_txt); - free(grpid); - - if (chat_id) { - context->cb(context, DC_EVENT_MSGS_CHANGED, 0, 0); - } - - return chat_id; -} - - -/** - * Set group name. - * - * If the group is already _promoted_ (any message was sent to the group), - * all group members are informed by a special status message that is sent automatically by this function. - * - * Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent. - * - * @memberof dc_context_t - * @param chat_id The chat ID to set the name for. Must be a group chat. - * @param new_name New name of the group. - * @param context The context as created by dc_context_new(). - * @return 1=success, 0=error - */ -int dc_set_chat_name(dc_context_t* context, uint32_t chat_id, const char* new_name) -{ - /* the function only sets the names of group chats; normal chats get their names from the contacts */ - int success = 0; - dc_chat_t* chat = dc_chat_new(context); - dc_msg_t* msg = dc_msg_new(); - char* q3 = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || new_name==NULL || new_name[0]==0 || chat_id<=DC_CHAT_ID_LAST_SPECIAL) { - goto cleanup; - } - - if (0==dc_real_group_exists(context, chat_id) - || 0==dc_chat_load_from_db(chat, chat_id)) { - goto cleanup; - } - - if (strcmp(chat->name, new_name)==0) { - success = 1; - goto cleanup; /* name not modified */ - } - - if (!IS_SELF_IN_GROUP) { - dc_log_error(context, DC_ERROR_SELF_NOT_IN_GROUP, NULL); - goto cleanup; /* we shoud respect this - whatever we send to the group, it gets discarded anyway! */ - } - - q3 = sqlite3_mprintf("UPDATE chats SET name=%Q WHERE id=%i;", new_name, chat_id); - if (!dc_sqlite3_execute(context->sql, q3)) { - goto cleanup; - } - - /* send a status mail to all group members, also needed for outself to allow multi-client */ - if (DO_SEND_STATUS_MAILS) - { - msg->type = DC_MSG_TEXT; - msg->text = dc_stock_str_repl_string2(context, DC_STR_MSGGRPNAME, chat->name, new_name); - dc_param_set_int(msg->param, DC_PARAM_CMD, DC_CMD_GROUPNAME_CHANGED); - msg->id = dc_send_msg_object(context, chat_id, msg); - context->cb(context, DC_EVENT_MSGS_CHANGED, chat_id, msg->id); - } - context->cb(context, DC_EVENT_CHAT_MODIFIED, chat_id, 0); - - success = 1; - -cleanup: - sqlite3_free(q3); - dc_chat_unref(chat); - dc_msg_unref(msg); - return success; -} - - -/** - * Set group profile image. - * - * If the group is already _promoted_ (any message was sent to the group), - * all group members are informed by a special status message that is sent automatically by this function. - * - * Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent. - * - * To find out the profile image of a chat, use dc_chat_get_profile_image() - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @param chat_id The chat ID to set the image for. - * @param new_image Full path of the image to use as the group image. If you pass NULL here, - * the group image is deleted (for promoted groups, all members are informed about this change anyway). - * @return 1=success, 0=error - */ -int dc_set_chat_profile_image(dc_context_t* context, uint32_t chat_id, const char* new_image /*NULL=remove image*/) -{ - int success = 0; - dc_chat_t* chat = dc_chat_new(context); - dc_msg_t* msg = dc_msg_new(); - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL) { - goto cleanup; - } - - if (0==dc_real_group_exists(context, chat_id) - || 0==dc_chat_load_from_db(chat, chat_id)) { - goto cleanup; - } - - if (!IS_SELF_IN_GROUP) { - dc_log_error(context, DC_ERROR_SELF_NOT_IN_GROUP, NULL); - goto cleanup; /* we shoud respect this - whatever we send to the group, it gets discarded anyway! */ - } - - dc_param_set(chat->param, DC_PARAM_PROFILE_IMAGE, new_image/*may be NULL*/); - if (!dc_chat_update_param(chat)) { - goto cleanup; - } - - /* send a status mail to all group members, also needed for outself to allow multi-client */ - if (DO_SEND_STATUS_MAILS) - { - dc_param_set_int(msg->param, DC_PARAM_CMD, DC_CMD_GROUPIMAGE_CHANGED); - dc_param_set (msg->param, DC_PARAM_CMD_ARG, new_image); - msg->type = DC_MSG_TEXT; - msg->text = dc_stock_str(context, new_image? DC_STR_MSGGRPIMGCHANGED : DC_STR_MSGGRPIMGDELETED); - msg->id = dc_send_msg_object(context, chat_id, msg); - context->cb(context, DC_EVENT_MSGS_CHANGED, chat_id, msg->id); - } - context->cb(context, DC_EVENT_CHAT_MODIFIED, chat_id, 0); - - success = 1; - -cleanup: - dc_chat_unref(chat); - dc_msg_unref(msg); - return success; -} - - -int dc_get_chat_contact_count(dc_context_t* context, uint32_t chat_id) -{ - int ret = 0; - sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, - "SELECT COUNT(*) FROM chats_contacts WHERE chat_id=?;"); - sqlite3_bind_int(stmt, 1, chat_id); - if (sqlite3_step(stmt)==SQLITE_ROW) { - ret = sqlite3_column_int(stmt, 0); - } - sqlite3_finalize(stmt); - return ret; -} - - -/** - * Check if a given contact ID is a member of a group chat. - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @param chat_id The chat ID to check. - * @param contact_id The contact ID to check. To check if yourself is member - * of the chat, pass DC_CONTACT_ID_SELF (1) here. - * @return 1=contact ID is member of chat ID, 0=contact is not in chat - */ -int dc_is_contact_in_chat(dc_context_t* context, uint32_t chat_id, uint32_t contact_id) -{ - /* this function works for group and for normal chats, however, it is more useful for group chats. - DC_CONTACT_ID_SELF may be used to check, if the user itself is in a group chat (DC_CONTACT_ID_SELF is not added to normal chats) */ - int ret = 0; - sqlite3_stmt* stmt = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - goto cleanup; - } - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT contact_id FROM chats_contacts WHERE chat_id=? AND contact_id=?;"); - sqlite3_bind_int(stmt, 1, chat_id); - sqlite3_bind_int(stmt, 2, contact_id); - ret = (sqlite3_step(stmt)==SQLITE_ROW)? 1 : 0; - -cleanup: - sqlite3_finalize(stmt); - return ret; -} - - -int dc_add_contact_to_chat_ex(dc_context_t* context, uint32_t chat_id, uint32_t contact_id, int flags) -{ - int success = 0; - dc_contact_t* contact = dc_get_contact(context, contact_id); - dc_apeerstate_t* peerstate = dc_apeerstate_new(context); - dc_chat_t* chat = dc_chat_new(context); - dc_msg_t* msg = dc_msg_new(); - char* self_addr = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || contact==NULL || chat_id<=DC_CHAT_ID_LAST_SPECIAL) { - goto cleanup; - } - - if (0==dc_real_group_exists(context, chat_id) /*this also makes sure, not contacts are added to special or normal chats*/ - || (0==dc_real_contact_exists(context, contact_id) && contact_id!=DC_CONTACT_ID_SELF) - || 0==dc_chat_load_from_db(chat, chat_id)) { - goto cleanup; - } - - if (!IS_SELF_IN_GROUP) { - dc_log_error(context, DC_ERROR_SELF_NOT_IN_GROUP, NULL); - goto cleanup; /* we shoud respect this - whatever we send to the group, it gets discarded anyway! */ - } - - if ((flags&DC_FROM_HANDSHAKE) && dc_param_get_int(chat->param, DC_PARAM_UNPROMOTED, 0)==1) { - // after a handshake, force sending the `Chat-Group-Member-Added` message - dc_param_set(chat->param, DC_PARAM_UNPROMOTED, NULL); - dc_chat_update_param(chat); - } - - self_addr = dc_sqlite3_get_config(context->sql, "configured_addr", ""); - if (strcasecmp(contact->addr, self_addr)==0) { - goto cleanup; /* ourself is added using DC_CONTACT_ID_SELF, do not add it explicitly. if SELF is not in the group, members cannot be added at all. */ - } - - if (dc_is_contact_in_chat(context, chat_id, contact_id)) - { - if (!(flags&DC_FROM_HANDSHAKE)) { - success = 1; - goto cleanup; - } - // else continue and send status mail - } - else - { - if (chat->type==DC_CHAT_TYPE_VERIFIED_GROUP) - { - if (!dc_apeerstate_load_by_addr(peerstate, context->sql, contact->addr) - || dc_contact_n_peerstate_are_verified(contact, peerstate)!=DC_BIDIRECT_VERIFIED) { - dc_log_error(context, 0, "Only bidirectional verified contacts can be added to verfied groups."); - goto cleanup; - } - } - - if (0==dc_add_to_chat_contacts_table(context, chat_id, contact_id)) { - goto cleanup; - } - } - - /* send a status mail to all group members */ - if (DO_SEND_STATUS_MAILS) - { - msg->type = DC_MSG_TEXT; - msg->text = dc_stock_str_repl_string(context, DC_STR_MSGADDMEMBER, (contact->authname&&contact->authname[0])? contact->authname : contact->addr); - dc_param_set_int(msg->param, DC_PARAM_CMD, DC_CMD_MEMBER_ADDED_TO_GROUP); - dc_param_set (msg->param, DC_PARAM_CMD_ARG, contact->addr); - dc_param_set_int(msg->param, DC_PARAM_CMD_ARG2, flags); // combine the Secure-Join protocol headers with the Chat-Group-Member-Added header - msg->id = dc_send_msg_object(context, chat_id, msg); - context->cb(context, DC_EVENT_MSGS_CHANGED, chat_id, msg->id); - } - context->cb(context, DC_EVENT_CHAT_MODIFIED, chat_id, 0); - - success = 1; - -cleanup: - dc_chat_unref(chat); - dc_contact_unref(contact); - dc_apeerstate_unref(peerstate); - dc_msg_unref(msg); - free(self_addr); - return success; -} - - -/** - * Add a member to a group. - * - * If the group is already _promoted_ (any message was sent to the group), - * all group members are informed by a special status message that is sent automatically by this function. - * - * If the group is a verified group, only verified contacts can be added to the group. - * - * Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent. - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @param chat_id The chat ID to add the contact to. Must be a group chat. - * @param contact_id The contact ID to add to the chat. - * @return 1=member added to group, 0=error - */ -int dc_add_contact_to_chat(dc_context_t* context, uint32_t chat_id, uint32_t contact_id /*may be DC_CONTACT_ID_SELF*/) -{ - return dc_add_contact_to_chat_ex(context, chat_id, contact_id, 0); -} - - -/** - * Remove a member from a group. - * - * If the group is already _promoted_ (any message was sent to the group), - * all group members are informed by a special status message that is sent automatically by this function. - * - * Sends out #DC_EVENT_CHAT_MODIFIED and #DC_EVENT_MSGS_CHANGED if a status message was sent. - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @param chat_id The chat ID to remove the contact from. Must be a group chat. - * @param contact_id The contact ID to remove from the chat. - * @return 1=member removed from group, 0=error - */ -int dc_remove_contact_from_chat(dc_context_t* context, uint32_t chat_id, uint32_t contact_id /*may be DC_CONTACT_ID_SELF*/) -{ - int success = 0; - dc_contact_t* contact = dc_get_contact(context, contact_id); - dc_chat_t* chat = dc_chat_new(context); - dc_msg_t* msg = dc_msg_new(); - char* q3 = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || chat_id<=DC_CHAT_ID_LAST_SPECIAL || (contact_id<=DC_CONTACT_ID_LAST_SPECIAL && contact_id!=DC_CONTACT_ID_SELF)) { - goto cleanup; /* we do not check if "contact_id" exists but just delete all records with the id from chats_contacts */ - } /* this allows to delete pending references to deleted contacts. Of course, this should _not_ happen. */ - - if (0==dc_real_group_exists(context, chat_id) - || 0==dc_chat_load_from_db(chat, chat_id)) { - goto cleanup; - } - - if (!IS_SELF_IN_GROUP) { - dc_log_error(context, DC_ERROR_SELF_NOT_IN_GROUP, NULL); - goto cleanup; /* we shoud respect this - whatever we send to the group, it gets discarded anyway! */ - } - - /* send a status mail to all group members - we need to do this before we update the database - - otherwise the !IS_SELF_IN_GROUP__-check in dc_chat_send_msg() will fail. */ - if (contact) - { - if (DO_SEND_STATUS_MAILS) - { - msg->type = DC_MSG_TEXT; - if (contact->id==DC_CONTACT_ID_SELF) { - dc_set_group_explicitly_left(context, chat->grpid); - msg->text = dc_stock_str(context, DC_STR_MSGGROUPLEFT); - } - else { - msg->text = dc_stock_str_repl_string(context, DC_STR_MSGDELMEMBER, (contact->authname&&contact->authname[0])? contact->authname : contact->addr); - } - dc_param_set_int(msg->param, DC_PARAM_CMD, DC_CMD_MEMBER_REMOVED_FROM_GROUP); - dc_param_set (msg->param, DC_PARAM_CMD_ARG, contact->addr); - msg->id = dc_send_msg_object(context, chat_id, msg); - context->cb(context, DC_EVENT_MSGS_CHANGED, chat_id, msg->id); - } - } - - q3 = sqlite3_mprintf("DELETE FROM chats_contacts WHERE chat_id=%i AND contact_id=%i;", chat_id, contact_id); - if (!dc_sqlite3_execute(context->sql, q3)) { - goto cleanup; - } - - context->cb(context, DC_EVENT_CHAT_MODIFIED, chat_id, 0); - - success = 1; - -cleanup: - sqlite3_free(q3); - dc_chat_unref(chat); - dc_contact_unref(contact); - dc_msg_unref(msg); - return success; -} - - -/******************************************************************************* - * Handle Contacts - ******************************************************************************/ - - -int dc_real_contact_exists(dc_context_t* context, uint32_t contact_id) -{ - sqlite3_stmt* stmt = NULL; - int ret = 0; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || context->sql->cobj==NULL - || contact_id<=DC_CONTACT_ID_LAST_SPECIAL) { - goto cleanup; - } - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT id FROM contacts WHERE id=?;"); - sqlite3_bind_int(stmt, 1, contact_id); - - if (sqlite3_step(stmt)==SQLITE_ROW) { - ret = 1; - } - -cleanup: - sqlite3_finalize(stmt); - return ret; -} - - -size_t dc_get_real_contact_cnt(dc_context_t* context) -{ - size_t ret = 0; - sqlite3_stmt* stmt = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || context->sql->cobj==NULL) { - goto cleanup; - } - - stmt = dc_sqlite3_prepare(context->sql, "SELECT COUNT(*) FROM contacts WHERE id>?;"); - sqlite3_bind_int(stmt, 1, DC_CONTACT_ID_LAST_SPECIAL); - if (sqlite3_step(stmt)!=SQLITE_ROW) { - goto cleanup; - } - - ret = sqlite3_column_int(stmt, 0); - -cleanup: - sqlite3_finalize(stmt); - return ret; -} - - -uint32_t dc_add_or_lookup_contact( dc_context_t* context, - const char* name /*can be NULL, the caller may use dc_normalize_name() before*/, - const char* addr__, - int origin, - int* sth_modified ) -{ - #define CONTACT_MODIFIED 1 - #define CONTACT_CREATED 2 - sqlite3_stmt* stmt = NULL; - uint32_t row_id = 0; - int dummy = 0; - char* addr = NULL; - char* row_name = NULL; - char* row_addr = NULL; - char* row_authname = NULL; - - if (sth_modified==NULL) { - sth_modified = &dummy; - } - - *sth_modified = 0; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || addr__==NULL || origin<=0) { - goto cleanup; - } - - /* normalize the email-address: - - remove leading `mailto:` */ - addr = dc_addr_normalize(addr__); - - /* rough check if email-address is valid */ - if (strlen(addr) < 3 || strchr(addr, '@')==NULL || strchr(addr, '.')==NULL) { - dc_log_warning(context, 0, "Bad address \"%s\" for contact \"%s\".", addr, name?name:""); - goto cleanup; - } - - /* insert email-address to database or modify the record with the given email-address. - we treat all email-addresses case-insensitive. */ - stmt = dc_sqlite3_prepare(context->sql, - "SELECT id, name, addr, origin, authname FROM contacts WHERE addr=? COLLATE NOCASE;"); - sqlite3_bind_text(stmt, 1, (const char*)addr, -1, SQLITE_STATIC); - if (sqlite3_step(stmt)==SQLITE_ROW) - { - - int row_origin, update_addr = 0, update_name = 0, update_authname = 0; - - row_id = sqlite3_column_int(stmt, 0); - row_name = dc_strdup((char*)sqlite3_column_text(stmt, 1)); - row_addr = dc_strdup((char*)sqlite3_column_text(stmt, 2)); - row_origin = sqlite3_column_int(stmt, 3); - row_authname = dc_strdup((char*)sqlite3_column_text(stmt, 4)); - sqlite3_finalize (stmt); - stmt = NULL; - - if (name && name[0]) { - if (row_name[0]) { - if (origin>=row_origin && strcmp(name, row_name)!=0) { - update_name = 1; - } - } - else { - update_name = 1; - } - - if (origin==DC_ORIGIN_INCOMING_UNKNOWN_FROM && strcmp(name, row_authname)!=0) { - update_authname = 1; - } - } - - if (origin>=row_origin && strcmp(addr, row_addr)!=0 /*really compare case-sensitive here*/) { - update_addr = 1; - } - - if (update_name || update_authname || update_addr || origin>row_origin) - { - stmt = dc_sqlite3_prepare(context->sql, - "UPDATE contacts SET name=?, addr=?, origin=?, authname=? WHERE id=?;"); - sqlite3_bind_text(stmt, 1, update_name? name : row_name, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, update_addr? addr : row_addr, -1, SQLITE_STATIC); - sqlite3_bind_int (stmt, 3, origin>row_origin? origin : row_origin); - sqlite3_bind_text(stmt, 4, update_authname? name : row_authname, -1, SQLITE_STATIC); - sqlite3_bind_int (stmt, 5, row_id); - sqlite3_step (stmt); - sqlite3_finalize (stmt); - stmt = NULL; - - if (update_name) - { - /* Update the contact name also if it is used as a group name. - This is one of the few duplicated data, however, getting the chat list is easier this way.*/ - stmt = dc_sqlite3_prepare(context->sql, - "UPDATE chats SET name=? WHERE type=? AND id IN(SELECT chat_id FROM chats_contacts WHERE contact_id=?);"); - sqlite3_bind_text(stmt, 1, name, -1, SQLITE_STATIC); - sqlite3_bind_int (stmt, 2, DC_CHAT_TYPE_SINGLE); - sqlite3_bind_int (stmt, 3, row_id); - sqlite3_step (stmt); - } - - *sth_modified = CONTACT_MODIFIED; - } - } - else - { - sqlite3_finalize (stmt); - stmt = NULL; - - stmt = dc_sqlite3_prepare(context->sql, - "INSERT INTO contacts (name, addr, origin) VALUES(?, ?, ?);"); - sqlite3_bind_text(stmt, 1, name? name : "", -1, SQLITE_STATIC); /* avoid NULL-fields in column */ - sqlite3_bind_text(stmt, 2, addr, -1, SQLITE_STATIC); - sqlite3_bind_int (stmt, 3, origin); - if (sqlite3_step(stmt)==SQLITE_DONE) - { - row_id = dc_sqlite3_get_rowid(context->sql, "contacts", "addr", addr); - *sth_modified = CONTACT_CREATED; - } - else - { - dc_log_error(context, 0, "Cannot add contact."); /* should not happen */ - } - } - -cleanup: - free(addr); - free(row_addr); - free(row_name); - free(row_authname); - sqlite3_finalize(stmt); - return row_id; -} - - -void dc_scaleup_contact_origin(dc_context_t* context, uint32_t contact_id, int origin) -{ - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - return; - } - - sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, - "UPDATE contacts SET origin=? WHERE id=? AND originsql, contact_id)) { - if (contact->blocked) { - is_blocked = 1; - } - } - - dc_contact_unref(contact); - return is_blocked; -} - - -int dc_get_contact_origin(dc_context_t* context, uint32_t contact_id, int* ret_blocked) -{ - int ret = 0; - int dummy = 0; if (ret_blocked==NULL) { ret_blocked = &dummy; } - dc_contact_t* contact = dc_contact_new(context); - - *ret_blocked = 0; - - if (!dc_contact_load_from_db(contact, context->sql, contact_id)) { /* we could optimize this by loading only the needed fields */ - goto cleanup; - } - - if (contact->blocked) { - *ret_blocked = 1; - goto cleanup; - } - - ret = contact->origin; - -cleanup: - dc_contact_unref(contact); - return ret; -} - - -/** - * Add a single contact as a result of an _explicit_ user action. - * - * We assume, the contact name, if any, is entered by the user and is used "as is" therefore, - * normalize() is _not_ called for the name. If the contact is blocked, it is unblocked. - * - * To add a number of contacts, see dc_add_address_book() which is much faster for adding - * a bunch of addresses. - * - * May result in a #DC_EVENT_CONTACTS_CHANGED event. - * - * @memberof dc_context_t - * @param context The context object as created by dc_context_new(). - * @param name Name of the contact to add. If you do not know the name belonging - * to the address, you can give NULL here. - * @param addr E-mail-address of the contact to add. If the email address - * already exists, the name is updated and the origin is increased to - * "manually created". - * @return Contact ID of the created or reused contact. - */ -uint32_t dc_create_contact(dc_context_t* context, const char* name, const char* addr) -{ - uint32_t contact_id = 0; - int sth_modified = 0; - int blocked = 0; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || addr==NULL || addr[0]==0) { - goto cleanup; - } - - contact_id = dc_add_or_lookup_contact(context, name, addr, DC_ORIGIN_MANUALLY_CREATED, &sth_modified); - - blocked = dc_is_contact_blocked(context, contact_id); - - context->cb(context, DC_EVENT_CONTACTS_CHANGED, sth_modified==CONTACT_CREATED? contact_id : 0, 0); - - if (blocked) { - dc_block_contact(context, contact_id, 0); - } - -cleanup: - return contact_id; -} - - -/** - * Add a number of contacts. - * - * Typically used to add the whole address book from the OS. As names here are typically not - * well formatted, we call normalize() for each name given. - * - * To add a single contact entered by the user, you should prefer dc_create_contact(), - * however, for adding a bunch of addresses, this function is _much_ faster. - * - * The function takes are of not overwriting names manually added or edited by dc_create_contact(). - * - * @memberof dc_context_t - * @param context the context object as created by dc_context_new(). - * @param adr_book A multi-line string in the format in the format - * `Name one\nAddress one\nName two\Address two`. If an email address - * already exists, the name is updated and the origin is increased to - * "manually created". - * @return The number of modified or added contacts. - */ -int dc_add_address_book(dc_context_t* context, const char* adr_book) /* format: Name one\nAddress one\nName two\Address two */ -{ - carray* lines = NULL; - size_t i = 0; - size_t iCnt = 0; - int sth_modified = 0; - int modify_cnt = 0; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || adr_book==NULL) { - goto cleanup; - } - - if ((lines=dc_split_into_lines(adr_book))==NULL) { - goto cleanup; - } - - dc_sqlite3_begin_transaction(context->sql); - - iCnt = carray_count(lines); - for (i = 0; i+1 < iCnt; i += 2) { - char* name = (char*)carray_get(lines, i); - char* addr = (char*)carray_get(lines, i+1); - dc_normalize_name(name); - dc_add_or_lookup_contact(context, name, addr, DC_ORIGIN_ADRESS_BOOK, &sth_modified); - if (sth_modified) { - modify_cnt++; - } - } - - dc_sqlite3_commit(context->sql); - -cleanup: - dc_free_splitted_lines(lines); - - return modify_cnt; -} - - -/** - * Returns known and unblocked contacts. - * - * To get information about a single contact, see dc_get_contact(). - * - * @memberof dc_context_t - * @param context The context object as created by dc_context_new(). - * @param listflags A combination of flags: - * - if the flag DC_GCL_ADD_SELF is set, SELF is added to the list unless filtered by other parameters - * - if the flag DC_GCL_VERIFIED_ONLY is set, only verified contacts are returned. - * if DC_GCL_VERIFIED_ONLY is not set, verified and unverified contacts are returned. - * @param query A string to filter the list. Typically used to implement an - * incremental search. NULL for no filtering. - * @return An array containing all contact IDs. Must be dc_array_unref()'d - * after usage. - */ -dc_array_t* dc_get_contacts(dc_context_t* context, uint32_t listflags, const char* query) -{ - char* self_addr = NULL; - char* self_name = NULL; - char* self_name2 = NULL; - int add_self = 0; - dc_array_t* ret = dc_array_new(context, 100); - char* s3strLikeCmd = NULL; - sqlite3_stmt* stmt = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - goto cleanup; - } - - self_addr = dc_sqlite3_get_config(context->sql, "configured_addr", ""); /* we add DC_CONTACT_ID_SELF explicitly; so avoid doubles if the address is present as a normal entry for some case */ - - if ((listflags&DC_GCL_VERIFIED_ONLY) || query) - { - if ((s3strLikeCmd=sqlite3_mprintf("%%%s%%", query? query : ""))==NULL) { - goto cleanup; - } - stmt = dc_sqlite3_prepare(context->sql, - "SELECT c.id FROM contacts c" - " LEFT JOIN acpeerstates ps ON c.addr=ps.addr " - " WHERE c.addr!=? AND c.id>" DC_STRINGIFY(DC_CONTACT_ID_LAST_SPECIAL) " AND c.origin>=" DC_STRINGIFY(DC_ORIGIN_MIN_CONTACT_LIST) " AND c.blocked=0 AND (c.name LIKE ? OR c.addr LIKE ?)" /* see comments in dc_search_msgs() about the LIKE operator */ - " AND (1=? OR LENGTH(ps.verified_key_fingerprint)!=0) " - " ORDER BY LOWER(c.name||c.addr),c.id;"); - sqlite3_bind_text(stmt, 1, self_addr, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 2, s3strLikeCmd, -1, SQLITE_STATIC); - sqlite3_bind_text(stmt, 3, s3strLikeCmd, -1, SQLITE_STATIC); - sqlite3_bind_int (stmt, 4, (listflags&DC_GCL_VERIFIED_ONLY)? 0/*force checking for verified_key*/ : 1/*force statement being always true*/); - - self_name = dc_sqlite3_get_config(context->sql, "displayname", ""); - self_name2 = dc_stock_str(context, DC_STR_SELF); - if (query==NULL || dc_str_contains(self_addr, query) || dc_str_contains(self_name, query) || dc_str_contains(self_name2, query)) { - add_self = 1; - } - } - else - { - stmt = dc_sqlite3_prepare(context->sql, - "SELECT id FROM contacts" - " WHERE addr!=? AND id>" DC_STRINGIFY(DC_CONTACT_ID_LAST_SPECIAL) " AND origin>=" DC_STRINGIFY(DC_ORIGIN_MIN_CONTACT_LIST) " AND blocked=0" - " ORDER BY LOWER(name||addr),id;"); - sqlite3_bind_text(stmt, 1, self_addr, -1, SQLITE_STATIC); - - add_self = 1; - } - - while (sqlite3_step(stmt)==SQLITE_ROW) { - dc_array_add_id(ret, sqlite3_column_int(stmt, 0)); - } - - /* to the end of the list, add self - this is to be in sync with member lists and to allow the user to start a self talk */ - if ((listflags&DC_GCL_ADD_SELF) && add_self) { - dc_array_add_id(ret, DC_CONTACT_ID_SELF); - } - -cleanup: - sqlite3_finalize(stmt); - sqlite3_free(s3strLikeCmd); - free(self_addr); - free(self_name); - free(self_name2); - return ret; -} - - -/** - * Get blocked contacts. - * - * @memberof dc_context_t - * @param context The context object as created by dc_context_new(). - * @return An array containing all blocked contact IDs. Must be dc_array_unref()'d - * after usage. - */ -dc_array_t* dc_get_blocked_contacts(dc_context_t* context) -{ - dc_array_t* ret = dc_array_new(context, 100); - sqlite3_stmt* stmt = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - goto cleanup; - } - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT id FROM contacts" - " WHERE id>? AND blocked!=0" - " ORDER BY LOWER(name||addr),id;"); - sqlite3_bind_int(stmt, 1, DC_CONTACT_ID_LAST_SPECIAL); - while (sqlite3_step(stmt)==SQLITE_ROW) { - dc_array_add_id(ret, sqlite3_column_int(stmt, 0)); - } - -cleanup: - sqlite3_finalize(stmt); - return ret; -} - - -/** - * Get the number of blocked contacts. - * - * @memberof dc_context_t - * @param context The context object as created by dc_context_new(). - * @return The number of blocked contacts. - */ -int dc_get_blocked_count(dc_context_t* context) -{ - int ret = 0; - sqlite3_stmt* stmt = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - goto cleanup; - } - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT COUNT(*) FROM contacts" - " WHERE id>? AND blocked!=0"); - sqlite3_bind_int(stmt, 1, DC_CONTACT_ID_LAST_SPECIAL); - if (sqlite3_step(stmt)!=SQLITE_ROW) { - goto cleanup; - } - ret = sqlite3_column_int(stmt, 0); - -cleanup: - sqlite3_finalize(stmt); - return ret; -} - - -/** - * Get a single contact object. For a list, see eg. dc_get_contacts(). - * - * For contact DC_CONTACT_ID_SELF (1), the function returns sth. - * like "Me" in the selected language and the email address - * defined by dc_set_config(). - * - * @memberof dc_context_t - * @param context The context object as created by dc_context_new(). - * @param contact_id ID of the contact to get the object for. - * @return The contact object, must be freed using dc_contact_unref() when no - * longer used. NULL on errors. - */ -dc_contact_t* dc_get_contact(dc_context_t* context, uint32_t contact_id) -{ - dc_contact_t* ret = dc_contact_new(context); - - if (!dc_contact_load_from_db(ret, context->sql, contact_id)) { - dc_contact_unref(ret); - ret = NULL; - } - - return ret; /* may be NULL */ -} - - -/** - * Mark all messages sent by the given contact - * as _noticed_. See also dc_marknoticed_chat() and - * dc_markseen_msgs() - * - * @memberof dc_context_t - * @param context The context object as created by dc_context_new() - * @param contact_id The contact ID of which all messages should be marked as noticed. - * @return none - */ -void dc_marknoticed_contact(dc_context_t* context, uint32_t contact_id) -{ - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - return; - } - - sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, - "UPDATE msgs SET state=" DC_STRINGIFY(DC_STATE_IN_NOTICED) " WHERE from_id=? AND state=" DC_STRINGIFY(DC_STATE_IN_FRESH) ";"); - sqlite3_bind_int(stmt, 1, contact_id); - sqlite3_step(stmt); - sqlite3_finalize(stmt); -} - - -void dc_block_chat(dc_context_t* context, uint32_t chat_id, int new_blocking) -{ - sqlite3_stmt* stmt = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - return; - } - - stmt = dc_sqlite3_prepare(context->sql, - "UPDATE chats SET blocked=? WHERE id=?;"); - sqlite3_bind_int(stmt, 1, new_blocking); - sqlite3_bind_int(stmt, 2, chat_id); - sqlite3_step(stmt); - sqlite3_finalize(stmt); -} - - -void dc_unblock_chat(dc_context_t* context, uint32_t chat_id) -{ - dc_block_chat(context, chat_id, DC_CHAT_NOT_BLOCKED); -} - - -/** - * Block or unblock a contact. - * May result in a #DC_EVENT_CONTACTS_CHANGED event. - * - * @memberof dc_context_t - * @param context The context object as created by dc_context_new(). - * @param contact_id The ID of the contact to block or unblock. - * @param new_blocking 1=block contact, 0=unblock contact - * @return None. - */ -void dc_block_contact(dc_context_t* context, uint32_t contact_id, int new_blocking) -{ - int send_event = 0; - dc_contact_t* contact = dc_contact_new(context); - sqlite3_stmt* stmt = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || contact_id<=DC_CONTACT_ID_LAST_SPECIAL) { - goto cleanup; - } - - if (dc_contact_load_from_db(contact, context->sql, contact_id) - && contact->blocked!=new_blocking) - { - stmt = dc_sqlite3_prepare(context->sql, - "UPDATE contacts SET blocked=? WHERE id=?;"); - sqlite3_bind_int(stmt, 1, new_blocking); - sqlite3_bind_int(stmt, 2, contact_id); - if (sqlite3_step(stmt)!=SQLITE_DONE) { - goto cleanup; - } - sqlite3_finalize(stmt); - stmt = NULL; - - /* also (un)block all chats with _only_ this contact - we do not delete them to allow a non-destructive blocking->unblocking. - (Maybe, beside normal chats (type=100) we should also block group chats with only this user. - However, I'm not sure about this point; it may be confusing if the user wants to add other people; - this would result in recreating the same group...) */ - stmt = dc_sqlite3_prepare(context->sql, - "UPDATE chats SET blocked=? WHERE type=? AND id IN (SELECT chat_id FROM chats_contacts WHERE contact_id=?);"); - sqlite3_bind_int(stmt, 1, new_blocking); - sqlite3_bind_int(stmt, 2, DC_CHAT_TYPE_SINGLE); - sqlite3_bind_int(stmt, 3, contact_id); - if (sqlite3_step(stmt)!=SQLITE_DONE) { - goto cleanup; - } - - /* mark all messages from the blocked contact as being noticed (this is to remove the deaddrop popup) */ - dc_marknoticed_contact(context, contact_id); - - send_event = 1; - } - - if (send_event) { - context->cb(context, DC_EVENT_CONTACTS_CHANGED, 0, 0); - } - -cleanup: - sqlite3_finalize(stmt); - dc_contact_unref(contact); -} - - -static void cat_fingerprint(dc_strbuilder_t* ret, const char* addr, const char* fingerprint_verified, const char* fingerprint_unverified) -{ - dc_strbuilder_cat(ret, "\n\n"); - dc_strbuilder_cat(ret, addr); - dc_strbuilder_cat(ret, ":\n"); - dc_strbuilder_cat(ret, (fingerprint_verified&&fingerprint_verified[0])? fingerprint_verified : fingerprint_unverified); - - if (fingerprint_verified && fingerprint_verified[0] - && fingerprint_unverified && fingerprint_unverified[0] - && strcmp(fingerprint_verified, fingerprint_unverified)!=0) { - // might be that for verified chats the - older - verified gossiped key is used - // and for normal chats the - newer - unverified key :/ - dc_strbuilder_cat(ret, "\n\n"); - dc_strbuilder_cat(ret, addr); - dc_strbuilder_cat(ret, " (alternative):\n"); - dc_strbuilder_cat(ret, fingerprint_unverified); - } -} - - -/** - * Get encryption info for a contact. - * Get a multi-line encryption info, containing your fingerprint and the - * fingerprint of the contact, used eg. to compare the fingerprints for a simple out-of-band verification. - * - * @memberof dc_context_t - * @param context The context object as created by dc_context_new(). - * @param contact_id ID of the contact to get the encryption info for. - * @return multi-line text, must be free()'d after usage. - */ -char* dc_get_contact_encrinfo(dc_context_t* context, uint32_t contact_id) -{ - dc_loginparam_t* loginparam = dc_loginparam_new(); - dc_contact_t* contact = dc_contact_new(context); - dc_apeerstate_t* peerstate = dc_apeerstate_new(context); - dc_key_t* self_key = dc_key_new(); - char* fingerprint_self = NULL; - char* fingerprint_other_verified = NULL; - char* fingerprint_other_unverified = NULL; - char* p = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - goto cleanup; - } - - dc_strbuilder_t ret; - dc_strbuilder_init(&ret, 0); - - if (!dc_contact_load_from_db(contact, context->sql, contact_id)) { - goto cleanup; - } - dc_apeerstate_load_by_addr(peerstate, context->sql, contact->addr); - dc_loginparam_read(loginparam, context->sql, "configured_"); - - dc_key_load_self_public(self_key, loginparam->addr, context->sql); - - if (dc_apeerstate_peek_key(peerstate, DC_NOT_VERIFIED)) - { - // E2E available :) - p = dc_stock_str(context, peerstate->prefer_encrypt==DC_PE_MUTUAL? DC_STR_E2E_PREFERRED : DC_STR_E2E_AVAILABLE); dc_strbuilder_cat(&ret, p); free(p); - - if (self_key->binary==NULL) { - dc_pgp_rand_seed(context, peerstate->addr, strlen(peerstate->addr) /*just some random data*/); - dc_ensure_secret_key_exists(context); - dc_key_load_self_public(self_key, loginparam->addr, context->sql); - } - - dc_strbuilder_cat(&ret, " "); - p = dc_stock_str(context, DC_STR_FINGERPRINTS); dc_strbuilder_cat(&ret, p); free(p); - dc_strbuilder_cat(&ret, ":"); - - fingerprint_self = dc_key_get_formatted_fingerprint(self_key); - fingerprint_other_verified = dc_key_get_formatted_fingerprint(dc_apeerstate_peek_key(peerstate, DC_BIDIRECT_VERIFIED)); - fingerprint_other_unverified = dc_key_get_formatted_fingerprint(dc_apeerstate_peek_key(peerstate, DC_NOT_VERIFIED)); - - if (strcmp(loginparam->addr, peerstate->addr)<0) { - cat_fingerprint(&ret, loginparam->addr, fingerprint_self, NULL); - cat_fingerprint(&ret, peerstate->addr, fingerprint_other_verified, fingerprint_other_unverified); - } - else { - cat_fingerprint(&ret, peerstate->addr, fingerprint_other_verified, fingerprint_other_unverified); - cat_fingerprint(&ret, loginparam->addr, fingerprint_self, NULL); - } - } - else - { - // No E2E available - if (!(loginparam->server_flags&DC_LP_IMAP_SOCKET_PLAIN) - && !(loginparam->server_flags&DC_LP_SMTP_SOCKET_PLAIN)) - { - p = dc_stock_str(context, DC_STR_ENCR_TRANSP); dc_strbuilder_cat(&ret, p); free(p); - } - else - { - p = dc_stock_str(context, DC_STR_ENCR_NONE); dc_strbuilder_cat(&ret, p); free(p); - } - } - -cleanup: - dc_apeerstate_unref(peerstate); - dc_contact_unref(contact); - dc_loginparam_unref(loginparam); - dc_key_unref(self_key); - free(fingerprint_self); - free(fingerprint_other_verified); - free(fingerprint_other_unverified); - return ret.buf; -} - - -/** - * Delete a contact. The contact is deleted from the local device. It may happen that this is not - * possible as the contact is in use. In this case, the contact can be blocked. - * - * May result in a #DC_EVENT_CONTACTS_CHANGED event. - * - * @memberof dc_context_t - * @param context The context object as created by dc_context_new(). - * @param contact_id ID of the contact to delete. - * @return 1=success, 0=error - */ -int dc_delete_contact(dc_context_t* context, uint32_t contact_id) -{ - int success = 0; - sqlite3_stmt* stmt = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || contact_id<=DC_CONTACT_ID_LAST_SPECIAL) { - goto cleanup; - } - - /* we can only delete contacts that are not in use anywhere; this function is mainly for the user who has just - created an contact manually and wants to delete it a moment later */ - stmt = dc_sqlite3_prepare(context->sql, - "SELECT COUNT(*) FROM chats_contacts WHERE contact_id=?;"); - sqlite3_bind_int(stmt, 1, contact_id); - if (sqlite3_step(stmt)!=SQLITE_ROW || sqlite3_column_int(stmt, 0) >= 1) { - goto cleanup; - } - sqlite3_finalize(stmt); - stmt = NULL; - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT COUNT(*) FROM msgs WHERE from_id=? OR to_id=?;"); - sqlite3_bind_int(stmt, 1, contact_id); - sqlite3_bind_int(stmt, 2, contact_id); - if (sqlite3_step(stmt)!=SQLITE_ROW || sqlite3_column_int(stmt, 0) >= 1) { - goto cleanup; - } - sqlite3_finalize(stmt); - stmt = NULL; - - stmt = dc_sqlite3_prepare(context->sql, - "DELETE FROM contacts WHERE id=?;"); - sqlite3_bind_int(stmt, 1, contact_id); - if (sqlite3_step(stmt)!=SQLITE_DONE) { - goto cleanup; - } - - context->cb(context, DC_EVENT_CONTACTS_CHANGED, 0, 0); - - success = 1; - -cleanup: - sqlite3_finalize(stmt); - return success; -} - - -/******************************************************************************* - * Handle Messages - ******************************************************************************/ - - -void dc_update_msg_chat_id(dc_context_t* context, uint32_t msg_id, uint32_t chat_id) -{ - sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, - "UPDATE msgs SET chat_id=? WHERE id=?;"); - sqlite3_bind_int(stmt, 1, chat_id); - sqlite3_bind_int(stmt, 2, msg_id); - sqlite3_step(stmt); - sqlite3_finalize(stmt); -} - - -void dc_update_msg_state(dc_context_t* context, uint32_t msg_id, int state) -{ - sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, - "UPDATE msgs SET state=? WHERE id=?;"); - sqlite3_bind_int(stmt, 1, state); - sqlite3_bind_int(stmt, 2, msg_id); - sqlite3_step(stmt); - sqlite3_finalize(stmt); -} - - -size_t dc_get_real_msg_cnt(dc_context_t* context) -{ - sqlite3_stmt* stmt = NULL; - size_t ret = 0; - - if (context->sql->cobj==NULL) { - goto cleanup; - } - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT COUNT(*) " - " FROM msgs m " - " LEFT JOIN chats c ON c.id=m.chat_id " - " WHERE m.id>" DC_STRINGIFY(DC_MSG_ID_LAST_SPECIAL) - " AND m.chat_id>" DC_STRINGIFY(DC_CHAT_ID_LAST_SPECIAL) - " AND c.blocked=0;"); - if (sqlite3_step(stmt)!=SQLITE_ROW) { - dc_sqlite3_log_error(context->sql, "dc_get_real_msg_cnt() failed."); - goto cleanup; - } - - ret = sqlite3_column_int(stmt, 0); - -cleanup: - sqlite3_finalize(stmt); - return ret; -} - - -size_t dc_get_deaddrop_msg_cnt(dc_context_t* context) -{ - sqlite3_stmt* stmt = NULL; - size_t ret = 0; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || context->sql->cobj==NULL) { - goto cleanup; - } - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT COUNT(*) FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id WHERE c.blocked=2;"); - if (sqlite3_step(stmt)!=SQLITE_ROW) { - goto cleanup; - } - - ret = sqlite3_column_int(stmt, 0); - -cleanup: - sqlite3_finalize(stmt); - return ret; -} - - -int dc_rfc724_mid_cnt(dc_context_t* context, const char* rfc724_mid) -{ - /* check the number of messages with the same rfc724_mid */ - int ret = 0; - sqlite3_stmt* stmt = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || context->sql->cobj==NULL) { - goto cleanup; - } - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT COUNT(*) FROM msgs WHERE rfc724_mid=?;"); - sqlite3_bind_text(stmt, 1, rfc724_mid, -1, SQLITE_STATIC); - if (sqlite3_step(stmt)!=SQLITE_ROW) { - goto cleanup; - } - - ret = sqlite3_column_int(stmt, 0); - -cleanup: - sqlite3_finalize(stmt); - return ret; -} - - -/* check, if the given Message-ID exists in the database (if not, the message is normally downloaded from the server and parsed, -so, we should even keep unuseful messages in the database (we can leave the other fields empty to save space) */ -uint32_t dc_rfc724_mid_exists(dc_context_t* context, const char* rfc724_mid, char** ret_server_folder, uint32_t* ret_server_uid) -{ - uint32_t ret = 0; - sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, - "SELECT server_folder, server_uid, id FROM msgs WHERE rfc724_mid=?;"); - sqlite3_bind_text(stmt, 1, rfc724_mid, -1, SQLITE_STATIC); - if (sqlite3_step(stmt)!=SQLITE_ROW) { - if (ret_server_folder) { *ret_server_folder = NULL; } - if (ret_server_uid) { *ret_server_uid = 0; } - goto cleanup; - } - - if (ret_server_folder) { *ret_server_folder = dc_strdup((char*)sqlite3_column_text(stmt, 0)); } - if (ret_server_uid) { *ret_server_uid = sqlite3_column_int(stmt, 1); /* may be 0 */ } - ret = sqlite3_column_int(stmt, 2); - -cleanup: - sqlite3_finalize(stmt); - return ret; -} - - -void dc_update_server_uid(dc_context_t* context, const char* rfc724_mid, const char* server_folder, uint32_t server_uid) -{ - sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, - "UPDATE msgs SET server_folder=?, server_uid=? WHERE rfc724_mid=?;"); /* we update by "rfc724_mid" instead of "id" as there may be several db-entries refering to the same "rfc724_mid" */ - sqlite3_bind_text(stmt, 1, server_folder, -1, SQLITE_STATIC); - sqlite3_bind_int (stmt, 2, server_uid); - sqlite3_bind_text(stmt, 3, rfc724_mid, -1, SQLITE_STATIC); - sqlite3_step(stmt); - sqlite3_finalize(stmt); -} - - -/** - * Get a single message object of the type dc_msg_t. - * For a list of messages in a chat, see dc_get_chat_msgs() - * For a list or chats, see dc_get_chatlist() - * - * @memberof dc_context_t - * @param context The context as created by dc_context_new(). - * @param msg_id The message ID for which the message object should be created. - * @return A dc_msg_t message object. When done, the object must be freed using dc_msg_unref() - */ -dc_msg_t* dc_get_msg(dc_context_t* context, uint32_t msg_id) -{ - int success = 0; - dc_msg_t* obj = dc_msg_new(); - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - goto cleanup; - } - - if (!dc_msg_load_from_db(obj, context, msg_id)) { - goto cleanup; - } - - success = 1; - -cleanup: - if (success) { - return obj; - } - else { - dc_msg_unref(obj); - return NULL; - } -} - - -/** - * Get an informational text for a single message. the text is multiline and may - * contain eg. the raw text of the message. - * - * The max. text returned is typically longer (about 100000 characters) than the - * max. text returned by dc_msg_get_text() (about 30000 characters). - * - * If the library is compiled for andoid, some basic html-formatting for he - * subject and the footer is added. However we should change this function so - * that it returns eg. an array of pairwise key-value strings and the caller - * can show the whole stuff eg. in a table. - * - * @memberof dc_context_t - * @param context the context object as created by dc_context_new(). - * @param msg_id the message id for which information should be generated - * @return text string, must be free()'d after usage - */ -char* dc_get_msg_info(dc_context_t* context, uint32_t msg_id) -{ - sqlite3_stmt* stmt = NULL; - dc_msg_t* msg = dc_msg_new(); - dc_contact_t* contact_from = dc_contact_new(context); - char* rawtxt = NULL; - char* p = NULL; - dc_strbuilder_t ret; - dc_strbuilder_init(&ret, 0); - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { - goto cleanup; - } - - dc_msg_load_from_db(msg, context, msg_id); - dc_contact_load_from_db(contact_from, context->sql, msg->from_id); - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT txt_raw FROM msgs WHERE id=?;"); - sqlite3_bind_int(stmt, 1, msg_id); - if (sqlite3_step(stmt)!=SQLITE_ROW) { - p = dc_mprintf("Cannot load message #%i.", (int)msg_id); dc_strbuilder_cat(&ret, p); free(p); - goto cleanup; - } - rawtxt = dc_strdup((char*)sqlite3_column_text(stmt, 0)); - sqlite3_finalize(stmt); - stmt = NULL; - - #ifdef __ANDROID__ - p = strchr(rawtxt, '\n'); - if (p) { - char* subject = rawtxt; - *p = 0; - p++; - rawtxt = dc_mprintf("%s\n%s", subject, p); - free(subject); - } - #endif - - dc_trim(rawtxt); - dc_truncate_str(rawtxt, DC_MAX_GET_INFO_LEN); - - /* add time */ - dc_strbuilder_cat(&ret, "Sent: "); - p = dc_timestamp_to_str(dc_msg_get_timestamp(msg)); dc_strbuilder_cat(&ret, p); free(p); - dc_strbuilder_cat(&ret, "\n"); - - if (msg->from_id!=DC_CONTACT_ID_SELF) { - dc_strbuilder_cat(&ret, "Received: "); - p = dc_timestamp_to_str(msg->timestamp_rcvd? msg->timestamp_rcvd : msg->timestamp); dc_strbuilder_cat(&ret, p); free(p); - dc_strbuilder_cat(&ret, "\n"); - } - - if (msg->from_id==DC_CONTACT_ID_DEVICE || msg->to_id==DC_CONTACT_ID_DEVICE) { - goto cleanup; // device-internal message, no further details needed - } - - /* add mdn's time and readers */ - stmt = dc_sqlite3_prepare(context->sql, - "SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;"); - sqlite3_bind_int (stmt, 1, msg_id); - while (sqlite3_step(stmt)==SQLITE_ROW) { - dc_strbuilder_cat(&ret, "Read: "); - p = dc_timestamp_to_str(sqlite3_column_int64(stmt, 1)); dc_strbuilder_cat(&ret, p); free(p); - dc_strbuilder_cat(&ret, " by "); - - dc_contact_t* contact = dc_contact_new(context); - dc_contact_load_from_db(contact, context->sql, sqlite3_column_int64(stmt, 0)); - p = dc_contact_get_display_name(contact); dc_strbuilder_cat(&ret, p); free(p); - dc_contact_unref(contact); - dc_strbuilder_cat(&ret, "\n"); - } - sqlite3_finalize(stmt); - stmt = NULL; - - /* add state */ - p = NULL; - switch (msg->state) { - case DC_STATE_IN_FRESH: p = dc_strdup("Fresh"); break; - case DC_STATE_IN_NOTICED: p = dc_strdup("Noticed"); break; - case DC_STATE_IN_SEEN: p = dc_strdup("Seen"); break; - case DC_STATE_OUT_DELIVERED: p = dc_strdup("Delivered"); break; - case DC_STATE_OUT_ERROR: p = dc_strdup("Error"); break; - case DC_STATE_OUT_MDN_RCVD: p = dc_strdup("Read"); break; - case DC_STATE_OUT_PENDING: p = dc_strdup("Pending"); break; - default: p = dc_mprintf("%i", msg->state); break; - } - dc_strbuilder_catf(&ret, "State: %s", p); - free(p); - - p = NULL; - int e2ee_errors; - if ((e2ee_errors=dc_param_get_int(msg->param, DC_PARAM_ERRONEOUS_E2EE, 0))) { - if (e2ee_errors&DC_E2EE_NO_VALID_SIGNATURE) { - p = dc_strdup("Encrypted, no valid signature"); - } - } - else if (dc_param_get_int(msg->param, DC_PARAM_GUARANTEE_E2EE, 0)) { - p = dc_strdup("Encrypted"); - } - - if (p) { - dc_strbuilder_catf(&ret, ", %s", p); - free(p); - } - dc_strbuilder_cat(&ret, "\n"); - - /* add sender (only for info messages as the avatar may not be shown for them) */ - if (dc_msg_is_info(msg)) { - dc_strbuilder_cat(&ret, "Sender: "); - p = dc_contact_get_name_n_addr(contact_from); dc_strbuilder_cat(&ret, p); free(p); - dc_strbuilder_cat(&ret, "\n"); - } - - /* add file info */ - char* file = dc_param_get(msg->param, DC_PARAM_FILE, NULL); - if (file) { - p = dc_mprintf("\nFile: %s, %i bytes\n", file, (int)dc_get_filebytes(file)); dc_strbuilder_cat(&ret, p); free(p); - } - - if (msg->type!=DC_MSG_TEXT) { - p = NULL; - switch (msg->type) { - case DC_MSG_AUDIO: p = dc_strdup("Audio"); break; - case DC_MSG_FILE: p = dc_strdup("File"); break; - case DC_MSG_GIF: p = dc_strdup("GIF"); break; - case DC_MSG_IMAGE: p = dc_strdup("Image"); break; - case DC_MSG_VIDEO: p = dc_strdup("Video"); break; - case DC_MSG_VOICE: p = dc_strdup("Voice"); break; - default: p = dc_mprintf("%i", msg->type); break; - } - dc_strbuilder_catf(&ret, "Type: %s\n", p); - free(p); - } - - int w = dc_param_get_int(msg->param, DC_PARAM_WIDTH, 0), h = dc_param_get_int(msg->param, DC_PARAM_HEIGHT, 0); - if (w!=0 || h!=0) { - p = dc_mprintf("Dimension: %i x %i\n", w, h); dc_strbuilder_cat(&ret, p); free(p); - } - - int duration = dc_param_get_int(msg->param, DC_PARAM_DURATION, 0); - if (duration!=0) { - p = dc_mprintf("Duration: %i ms\n", duration); dc_strbuilder_cat(&ret, p); free(p); - } - - /* add rawtext */ - if (rawtxt && rawtxt[0]) { - dc_strbuilder_cat(&ret, "\n"); - dc_strbuilder_cat(&ret, rawtxt); - dc_strbuilder_cat(&ret, "\n"); - } - - /* add Message-ID, Server-Folder and Server-UID; the database ID is normally only of interest if you have access to sqlite; if so you can easily get it from the "msgs" table. */ - #ifdef __ANDROID__ - dc_strbuilder_cat(&ret, ""); - #endif - - if (msg->rfc724_mid && msg->rfc724_mid[0]) { - dc_strbuilder_catf(&ret, "\nMessage-ID: %s", msg->rfc724_mid); - } - - if (msg->server_folder && msg->server_folder[0]) { - dc_strbuilder_catf(&ret, "\nLast seen as: %s/%i", msg->server_folder, (int)msg->server_uid); - } - - #ifdef __ANDROID__ - dc_strbuilder_cat(&ret, ""); - #endif - -cleanup: - sqlite3_finalize(stmt); - dc_msg_unref(msg); - dc_contact_unref(contact_from); - free(rawtxt); - return ret.buf; -} - - -/** - * Forward messages to another chat. - * - * @memberof dc_context_t - * @param context the context object as created by dc_context_new() - * @param msg_ids an array of uint32_t containing all message IDs that should be forwarded - * @param msg_cnt the number of messages IDs in the msg_ids array - * @param chat_id The destination chat ID. - * @return none - */ -void dc_forward_msgs(dc_context_t* context, const uint32_t* msg_ids, int msg_cnt, uint32_t chat_id) -{ - dc_msg_t* msg = dc_msg_new(); - dc_chat_t* chat = dc_chat_new(context); - dc_contact_t* contact = dc_contact_new(context); - int transaction_pending = 0; - carray* created_db_entries = carray_new(16); - char* idsstr = NULL; - char* q3 = NULL; - sqlite3_stmt* stmt = NULL; - time_t curr_timestamp = 0; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || msg_ids==NULL || msg_cnt<=0 || chat_id<=DC_CHAT_ID_LAST_SPECIAL) { - goto cleanup; - } - - dc_sqlite3_begin_transaction(context->sql); - transaction_pending = 1; - - dc_unarchive_chat(context, chat_id); - - context->smtp->log_connect_errors = 1; - - if (!dc_chat_load_from_db(chat, chat_id)) { - goto cleanup; - } - - curr_timestamp = dc_create_smeared_timestamps(context, msg_cnt); - - idsstr = dc_arr_to_string(msg_ids, msg_cnt); - q3 = sqlite3_mprintf("SELECT id FROM msgs WHERE id IN(%s) ORDER BY timestamp,id", idsstr); - stmt = dc_sqlite3_prepare(context->sql, q3); - while (sqlite3_step(stmt)==SQLITE_ROW) - { - int src_msg_id = sqlite3_column_int(stmt, 0); - if (!dc_msg_load_from_db(msg, context, src_msg_id)) { - goto cleanup; - } - - dc_param_set_int(msg->param, DC_PARAM_FORWARDED, 1); - dc_param_set (msg->param, DC_PARAM_GUARANTEE_E2EE, NULL); - dc_param_set (msg->param, DC_PARAM_FORCE_PLAINTEXT, NULL); - - uint32_t new_msg_id = dc_send_msg_raw(context, chat, msg, curr_timestamp++); - carray_add(created_db_entries, (void*)(uintptr_t)chat_id, NULL); - carray_add(created_db_entries, (void*)(uintptr_t)new_msg_id, NULL); - } - - dc_sqlite3_commit(context->sql); - transaction_pending = 0; - -cleanup: - if (transaction_pending) { dc_sqlite3_rollback(context->sql); } - if (created_db_entries) { - size_t i, icnt = carray_count(created_db_entries); - for (i = 0; i < icnt; i += 2) { - context->cb(context, DC_EVENT_MSGS_CHANGED, (uintptr_t)carray_get(created_db_entries, i), (uintptr_t)carray_get(created_db_entries, i+1)); - } - carray_free(created_db_entries); - } - dc_contact_unref(contact); - dc_msg_unref(msg); - dc_chat_unref(chat); - sqlite3_finalize(stmt); - free(idsstr); - sqlite3_free(q3); -} - - -/** - * Star/unstar messages by setting the last parameter to 0 (unstar) or 1 (star). - * Starred messages are collected in a virtual chat that can be shown using - * dc_get_chat_msgs() using the chat_id DC_CHAT_ID_STARRED. - * - * @memberof dc_context_t - * @param context The context object as created by dc_context_new() - * @param msg_ids An array of uint32_t message IDs defining the messages to star or unstar - * @param msg_cnt The number of IDs in msg_ids - * @param star 0=unstar the messages in msg_ids, 1=star them - * @return none - */ -void dc_star_msgs(dc_context_t* context, const uint32_t* msg_ids, int msg_cnt, int star) -{ - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || msg_ids==NULL || msg_cnt<=0 || (star!=0 && star!=1)) { - return; - } - - dc_sqlite3_begin_transaction(context->sql); - - sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, - "UPDATE msgs SET starred=? WHERE id=?;"); - for (int i = 0; i < msg_cnt; i++) - { - sqlite3_reset(stmt); - sqlite3_bind_int(stmt, 1, star); - sqlite3_bind_int(stmt, 2, msg_ids[i]); - sqlite3_step(stmt); - } - sqlite3_finalize(stmt); - - dc_sqlite3_commit(context->sql); -} - - -/******************************************************************************* - * Delete messages - ******************************************************************************/ - - -/** - * Delete messages. The messages are deleted on the current device and - * on the IMAP server. - * - * @memberof dc_context_t - * @param context the context object as created by dc_context_new() - * @param msg_ids an array of uint32_t containing all message IDs that should be deleted - * @param msg_cnt the number of messages IDs in the msg_ids array - * @return none - */ -void dc_delete_msgs(dc_context_t* context, const uint32_t* msg_ids, int msg_cnt) -{ - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || msg_ids==NULL || msg_cnt<=0) { - return; - } - - dc_sqlite3_begin_transaction(context->sql); - - for (int i = 0; i < msg_cnt; i++) - { - dc_update_msg_chat_id(context, msg_ids[i], DC_CHAT_ID_TRASH); - dc_job_add(context, DC_JOB_DELETE_MSG_ON_IMAP, msg_ids[i], NULL, 0); - } - - dc_sqlite3_commit(context->sql); -} - - -/******************************************************************************* - * mark message as seen - ******************************************************************************/ - - -/** - * Mark a message as _seen_, updates the IMAP state and - * sends MDNs. If the message is not in a real chat (eg. a contact request), the - * message is only marked as NOTICED and no IMAP/MDNs is done. See also - * dc_marknoticed_chat() and dc_marknoticed_contact() - * - * @memberof dc_context_t - * @param context The context object. - * @param msg_ids an array of uint32_t containing all the messages IDs that should be marked as seen. - * @param msg_cnt The number of message IDs in msg_ids. - * @return none - */ -void dc_markseen_msgs(dc_context_t* context, const uint32_t* msg_ids, int msg_cnt) -{ - int transaction_pending = 0; - int i = 0; - int send_event = 0; - int curr_state = 0; - int curr_blocked = 0; - sqlite3_stmt* stmt = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || msg_ids==NULL || msg_cnt<=0) { - goto cleanup; - } - - dc_sqlite3_begin_transaction(context->sql); - transaction_pending = 1; - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT m.state, c.blocked " - " FROM msgs m " - " LEFT JOIN chats c ON c.id=m.chat_id " - " WHERE m.id=? AND m.chat_id>" DC_STRINGIFY(DC_CHAT_ID_LAST_SPECIAL)); - for (i = 0; i < msg_cnt; i++) - { - sqlite3_reset(stmt); - sqlite3_bind_int(stmt, 1, msg_ids[i]); - if (sqlite3_step(stmt)!=SQLITE_ROW) { - continue; - } - curr_state = sqlite3_column_int(stmt, 0); - curr_blocked = sqlite3_column_int(stmt, 1); - if (curr_blocked==0) - { - if (curr_state==DC_STATE_IN_FRESH || curr_state==DC_STATE_IN_NOTICED) { - dc_update_msg_state(context, msg_ids[i], DC_STATE_IN_SEEN); - dc_log_info(context, 0, "Seen message #%i.", msg_ids[i]); - dc_job_add(context, DC_JOB_MARKSEEN_MSG_ON_IMAP, msg_ids[i], NULL, 0); /* results in a call to dc_markseen_msg_on_imap() */ - send_event = 1; - } - } - else - { - /* message may be in contact requests, mark as NOTICED, this does not force IMAP updated nor send MDNs */ - if (curr_state==DC_STATE_IN_FRESH) { - dc_update_msg_state(context, msg_ids[i], DC_STATE_IN_NOTICED); - send_event = 1; - } - } - } - - dc_sqlite3_commit(context->sql); - transaction_pending = 0; - - /* the event is needed eg. to remove the deaddrop from the chatlist */ - if (send_event) { - context->cb(context, DC_EVENT_MSGS_CHANGED, 0, 0); - } - -cleanup: - if (transaction_pending) { dc_sqlite3_rollback(context->sql); } - sqlite3_finalize(stmt); -} - - -int dc_mdn_from_ext(dc_context_t* context, uint32_t from_id, const char* rfc724_mid, time_t timestamp_sent, - uint32_t* ret_chat_id, uint32_t* ret_msg_id) -{ - int read_by_all = 0; - sqlite3_stmt* stmt = NULL; - - if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || from_id<=DC_CONTACT_ID_LAST_SPECIAL || rfc724_mid==NULL || ret_chat_id==NULL || ret_msg_id==NULL - || *ret_chat_id!=0 || *ret_msg_id!=0) { - goto cleanup; - } - - stmt = dc_sqlite3_prepare(context->sql, - "SELECT m.id, c.id, c.type, m.state FROM msgs m " - " LEFT JOIN chats c ON m.chat_id=c.id " - " WHERE rfc724_mid=? AND from_id=1 " - " ORDER BY m.id;"); /* the ORDER BY makes sure, if one rfc724_mid is splitted into its parts, we always catch the same one. However, we do not send multiparts, we do not request MDNs for multiparts, and should not receive read requests for multiparts. So this is currently more theoretical. */ - sqlite3_bind_text(stmt, 1, rfc724_mid, -1, SQLITE_STATIC); - if (sqlite3_step(stmt)!=SQLITE_ROW) { - goto cleanup; - } - *ret_msg_id = sqlite3_column_int(stmt, 0); - *ret_chat_id = sqlite3_column_int(stmt, 1); - int chat_type = sqlite3_column_int(stmt, 2); - int msg_state = sqlite3_column_int(stmt, 3); - sqlite3_finalize(stmt); - stmt = NULL; - - if (msg_state!=DC_STATE_OUT_PENDING && msg_state!=DC_STATE_OUT_DELIVERED) { - goto cleanup; /* eg. already marked as MDNS_RCVD. however, it is importent, that the message ID is set above as this will allow the caller eg. to move the message away */ - } - - // collect receipt senders, we do this also for normal chats as we may want to show the timestamp - stmt = dc_sqlite3_prepare(context->sql, - "SELECT contact_id FROM msgs_mdns WHERE msg_id=? AND contact_id=?;"); - sqlite3_bind_int(stmt, 1, *ret_msg_id); - sqlite3_bind_int(stmt, 2, from_id); - int mdn_already_in_table = (sqlite3_step(stmt)==SQLITE_ROW)? 1 : 0; - sqlite3_finalize(stmt); - stmt = NULL; - - if (!mdn_already_in_table) { - stmt = dc_sqlite3_prepare(context->sql, - "INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);"); - sqlite3_bind_int (stmt, 1, *ret_msg_id); - sqlite3_bind_int (stmt, 2, from_id); - sqlite3_bind_int64(stmt, 3, timestamp_sent); - sqlite3_step(stmt); - sqlite3_finalize(stmt); - stmt = NULL; - } - - // Normal chat? that's quite easy. - if (chat_type==DC_CHAT_TYPE_SINGLE) { - dc_update_msg_state(context, *ret_msg_id, DC_STATE_OUT_MDN_RCVD); - read_by_all = 1; - goto cleanup; /* send event about new state */ - } - - // Group chat: get the number of receipt senders - stmt = dc_sqlite3_prepare(context->sql, - "SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?;"); - sqlite3_bind_int(stmt, 1, *ret_msg_id); - if (sqlite3_step(stmt)!=SQLITE_ROW) { - goto cleanup; /* error */ - } - int ist_cnt = sqlite3_column_int(stmt, 0); - sqlite3_finalize(stmt); - stmt = NULL; - - /* - Groupsize: Min. MDNs - - 1 S n/a - 2 SR 1 - 3 SRR 2 - 4 SRRR 2 - 5 SRRRR 3 - 6 SRRRRR 3 - - (S=Sender, R=Recipient) - */ - int soll_cnt = (dc_get_chat_contact_count(context, *ret_chat_id)+1/*for rounding, SELF is already included!*/) / 2; - if (ist_cnt < soll_cnt) { - goto cleanup; /* wait for more receipts */ - } - - /* got enough receipts :-) */ - dc_update_msg_state(context, *ret_msg_id, DC_STATE_OUT_MDN_RCVD); - read_by_all = 1; - -cleanup: - sqlite3_finalize(stmt); - return read_by_all; -} diff --git a/src/dc_context.h b/src/dc_context.h index 3caf43f4..8eec6917 100644 --- a/src/dc_context.h +++ b/src/dc_context.h @@ -118,50 +118,11 @@ struct _dc_context int shall_stop_ongoing; }; - -/* logging and error handling */ void dc_log_error (dc_context_t*, int code, const char* msg, ...); void dc_log_error_if (int* condition, dc_context_t*, int code, const char* msg, ...); void dc_log_warning (dc_context_t*, int code, const char* msg, ...); void dc_log_info (dc_context_t*, int code, const char* msg, ...); - - -/* misc.*/ void dc_receive_imf (dc_context_t*, const char* imf_raw_not_terminated, size_t imf_raw_bytes, const char* server_folder, uint32_t server_uid, uint32_t flags); -uint32_t dc_send_msg_object (dc_context_t*, uint32_t chat_id, dc_msg_t*); -int dc_get_archived_count (dc_context_t*); -size_t dc_get_real_contact_cnt (dc_context_t*); -uint32_t dc_add_or_lookup_contact (dc_context_t*, const char* display_name /*can be NULL*/, const char* addr_spec, int origin, int* sth_modified); -int dc_get_contact_origin (dc_context_t*, uint32_t id, int* ret_blocked); -int dc_is_contact_blocked (dc_context_t*, uint32_t id); -int dc_real_contact_exists (dc_context_t*, uint32_t id); -void dc_scaleup_contact_origin (dc_context_t*, uint32_t contact_id, int origin); -void dc_unarchive_chat (dc_context_t*, uint32_t chat_id); -size_t dc_get_chat_cnt (dc_context_t*); -void dc_block_chat (dc_context_t*, uint32_t chat_id, int new_blocking); -void dc_unblock_chat (dc_context_t*, uint32_t chat_id); -void dc_create_or_lookup_nchat_by_contact_id (dc_context_t*, uint32_t contact_id, int create_blocked, uint32_t* ret_chat_id, int* ret_chat_blocked); -void dc_lookup_real_nchat_by_contact_id (dc_context_t*, uint32_t contact_id, uint32_t* ret_chat_id, int* ret_chat_blocked); -uint32_t dc_get_last_deaddrop_fresh_msg (dc_context_t*); -int dc_add_to_chat_contacts_table (dc_context_t*, uint32_t chat_id, uint32_t contact_id); -int dc_is_contact_in_chat (dc_context_t*, uint32_t chat_id, uint32_t contact_id); -int dc_get_chat_contact_count (dc_context_t*, uint32_t chat_id); -int dc_is_group_explicitly_left (dc_context_t*, const char* grpid); -void dc_set_group_explicitly_left (dc_context_t*, const char* grpid); -size_t dc_get_real_msg_cnt (dc_context_t*); /* the number of messages assigned to real chat (!=deaddrop, !=trash) */ -size_t dc_get_deaddrop_msg_cnt (dc_context_t*); -int dc_rfc724_mid_cnt (dc_context_t*, const char* rfc724_mid); -uint32_t dc_rfc724_mid_exists (dc_context_t*, const char* rfc724_mid, char** ret_server_folder, uint32_t* ret_server_uid); -void dc_update_server_uid (dc_context_t*, const char* rfc724_mid, const char* server_folder, uint32_t server_uid); -void dc_update_msg_chat_id (dc_context_t*, uint32_t msg_id, uint32_t chat_id); -void dc_update_msg_state (dc_context_t*, uint32_t msg_id, int state); -int dc_mdn_from_ext (dc_context_t*, uint32_t from_id, const char* rfc724_mid, time_t, uint32_t* ret_chat_id, uint32_t* ret_msg_id); /* returns 1 if an event should be send */ -void dc_add_device_msg (dc_context_t*, uint32_t chat_id, const char* text); - -#define DC_FROM_HANDSHAKE 0x01 -int dc_add_contact_to_chat_ex (dc_context_t*, uint32_t chat_id, uint32_t contact_id, int flags); - -uint32_t dc_get_chat_id_by_grpid (dc_context_t*, const char* grpid, int* ret_blocked, int* ret_verified); #define DC_BAK_PREFIX "delta-chat" #define DC_BAK_SUFFIX "bak" diff --git a/src/dc_e2ee.c b/src/dc_e2ee.c index b86d1afc..2a859231 100644 --- a/src/dc_e2ee.c +++ b/src/dc_e2ee.c @@ -621,7 +621,7 @@ static int decrypt_part(dc_context_t* context, goto cleanup; } - dc_hash_t* add_signatures = dc_hash_count(ret_valid_signatures)<=0? + dc_hash_t* add_signatures = dc_hash_cnt(ret_valid_signatures)<=0? ret_valid_signatures : NULL; /*if we already have fingerprints, do not add more; this ensures, only the fingerprints from the outer-most part are collected */ if (!dc_pgp_pk_decrypt(context, decoded_data, decoded_data_bytes, private_keyring, public_keyring_for_validate, 1, &plain_buf, &plain_bytes, add_signatures) @@ -684,7 +684,7 @@ static int decrypt_recursive(dc_context_t* context, { /* remember the header containing potentially Autocrypt-Gossip */ if (*ret_gossip_headers == NULL /* use the outermost decrypted part */ - && dc_hash_count(ret_valid_signatures) > 0 /* do not trust the gossipped keys when the message cannot be validated eg. due to a bad signature */) + && dc_hash_cnt(ret_valid_signatures) > 0 /* do not trust the gossipped keys when the message cannot be validated eg. due to a bad signature */) { size_t dummy = 0; struct mailimf_fields* test = NULL; diff --git a/src/dc_hash.h b/src/dc_hash.h index ed39ead0..6d51191c 100644 --- a/src/dc_hash.h +++ b/src/dc_hash.h @@ -127,7 +127,7 @@ void dc_hash_clear (dc_hash_t*); /* * Number of entries in a hash table */ -#define dc_hash_count(H) ((H)->count) +#define dc_hash_cnt(H) ((H)->count) #ifdef __cplusplus diff --git a/src/dc_imex.c b/src/dc_imex.c index 25a244ca..6aa90719 100644 --- a/src/dc_imex.c +++ b/src/dc_imex.c @@ -746,7 +746,7 @@ static int import_self_keys(dc_context_t* context, const char* dir_name) Maybe we should make the "default" key handlong also a little bit smarter (currently, the last imported key is the standard key unless it contains the string "legacy" in its name) */ - int imported_count = 0; + int imported_cnt = 0; DIR* dir_handle = NULL; struct dirent* dir_entry = NULL; char* suffix = NULL; @@ -809,10 +809,10 @@ static int import_self_keys(dc_context_t* context, const char* dir_name) continue; } - imported_count++; + imported_cnt++; } - if (imported_count == 0) { + if (imported_cnt == 0) { dc_log_error(context, 0, "No private keys found in \"%s\".", dir_name); goto cleanup; } @@ -823,7 +823,7 @@ cleanup: free(path_plus_name); free(buf); free(buf2); - return imported_count; + return imported_cnt; } @@ -835,8 +835,8 @@ cleanup: /* the FILE_PROGRESS macro calls the callback with the permille of files processed. The macro avoids weird values of 0% or 100% while still working. */ #define FILE_PROGRESS \ - processed_files_count++; \ - int permille = (processed_files_count*1000)/total_files_count; \ + processed_files_cnt++; \ + int permille = (processed_files_cnt*1000)/total_files_cnt; \ if (permille < 10) { permille = 10; } \ if (permille > 990) { permille = 990; } \ context->cb(context, DC_EVENT_IMEX_PROGRESS, permille, 0); @@ -857,8 +857,8 @@ static int export_backup(dc_context_t* context, const char* dir) void* buf = NULL; size_t buf_bytes = 0; sqlite3_stmt* stmt = NULL; - int total_files_count = 0; - int processed_files_count = 0; + int total_files_cnt = 0; + int processed_files_cnt = 0; int delete_dest_file = 0; /* get a fine backup file name (the name includes the date so that multiple backup instances are possible) @@ -899,20 +899,20 @@ static int export_backup(dc_context_t* context, const char* dir) } /* scan directory, pass 1: collect file info */ - total_files_count = 0; + total_files_cnt = 0; if ((dir_handle=opendir(context->blobdir))==NULL) { dc_log_error(context, 0, "Backup: Cannot get info for blob-directory \"%s\".", context->blobdir); goto cleanup; } while ((dir_entry=readdir(dir_handle))!=NULL) { - total_files_count++; + total_files_cnt++; } closedir(dir_handle); dir_handle = NULL; - if (total_files_count>0) + if (total_files_cnt>0) { /* scan directory, pass 2: copy files */ if ((dir_handle=opendir(context->blobdir))==NULL) { @@ -1008,8 +1008,8 @@ static int import_backup(dc_context_t* context, const char* backup_to_import) */ int success = 0; - int processed_files_count = 0; - int total_files_count = 0; + int processed_files_cnt = 0; + int total_files_cnt = 0; sqlite3_stmt* stmt = NULL; char* pathNfilename = NULL; char* repl_from = NULL; @@ -1051,7 +1051,7 @@ static int import_backup(dc_context_t* context, const char* backup_to_import) /* copy all blobs to files */ stmt = dc_sqlite3_prepare(context->sql, "SELECT COUNT(*) FROM backup_blobs;"); sqlite3_step(stmt); - total_files_count = sqlite3_column_int(stmt, 0); + total_files_cnt = sqlite3_column_int(stmt, 0); sqlite3_finalize(stmt); stmt = NULL; diff --git a/src/dc_mimeparser.c b/src/dc_mimeparser.c index b4fac8b2..3f80f1e6 100644 --- a/src/dc_mimeparser.c +++ b/src/dc_mimeparser.c @@ -939,7 +939,7 @@ void dc_mimeparser_empty(dc_mimeparser_t* mimeparser) static void do_add_single_part(dc_mimeparser_t* parser, dc_mimepart_t* part) { /* add a single part to the list of parts, the parser takes the ownership of the part, so you MUST NOT unref it after calling this function. */ - if (parser->e2ee_helper->encrypted && dc_hash_count(parser->e2ee_helper->signatures)>0) { + if (parser->e2ee_helper->encrypted && dc_hash_cnt(parser->e2ee_helper->signatures)>0) { dc_param_set_int(part->param, DC_PARAM_GUARANTEE_E2EE, 1); } else if (parser->e2ee_helper->encrypted) { @@ -1877,7 +1877,7 @@ int dc_mimeparser_sender_equals_recipient(dc_mimeparser_t* mimeparser) /* get To:/Cc: and check there is exactly one recipent */ recipients = mailimf_get_recipients(mimeparser->header_root); - if (dc_hash_count(recipients) != 1) { + if (dc_hash_cnt(recipients) != 1) { goto cleanup; } diff --git a/src/dc_msg.c b/src/dc_msg.c index d6174d60..b892aae5 100644 --- a/src/dc_msg.c +++ b/src/dc_msg.c @@ -106,11 +106,6 @@ void dc_msg_empty(dc_msg_t* msg) } -/******************************************************************************* - * Getters - ******************************************************************************/ - - /** * Get the ID of the message. * @@ -812,11 +807,6 @@ cleanup: } -/******************************************************************************* - * Misc. - ******************************************************************************/ - - #define DC_MSG_FIELDS " m.id,rfc724_mid,m.server_folder,m.server_uid,m.chat_id, " \ " m.from_id,m.to_id,m.timestamp,m.timestamp_sent,m.timestamp_rcvd, m.type,m.state,m.msgrmsg,m.txt, " \ " m.param,m.starred,m.hidden,c.blocked " @@ -1139,3 +1129,618 @@ void dc_msg_latefiling_mediasize(dc_msg_t* msg, int width, int height, int durat cleanup: ; } + + +/******************************************************************************* + * Context functions to work with messages + ******************************************************************************/ + + +void dc_update_msg_chat_id(dc_context_t* context, uint32_t msg_id, uint32_t chat_id) +{ + sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, + "UPDATE msgs SET chat_id=? WHERE id=?;"); + sqlite3_bind_int(stmt, 1, chat_id); + sqlite3_bind_int(stmt, 2, msg_id); + sqlite3_step(stmt); + sqlite3_finalize(stmt); +} + + +void dc_update_msg_state(dc_context_t* context, uint32_t msg_id, int state) +{ + sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, + "UPDATE msgs SET state=? WHERE id=?;"); + sqlite3_bind_int(stmt, 1, state); + sqlite3_bind_int(stmt, 2, msg_id); + sqlite3_step(stmt); + sqlite3_finalize(stmt); +} + + +size_t dc_get_real_msg_cnt(dc_context_t* context) +{ + sqlite3_stmt* stmt = NULL; + size_t ret = 0; + + if (context->sql->cobj==NULL) { + goto cleanup; + } + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT COUNT(*) " + " FROM msgs m " + " LEFT JOIN chats c ON c.id=m.chat_id " + " WHERE m.id>" DC_STRINGIFY(DC_MSG_ID_LAST_SPECIAL) + " AND m.chat_id>" DC_STRINGIFY(DC_CHAT_ID_LAST_SPECIAL) + " AND c.blocked=0;"); + if (sqlite3_step(stmt)!=SQLITE_ROW) { + dc_sqlite3_log_error(context->sql, "dc_get_real_msg_cnt() failed."); + goto cleanup; + } + + ret = sqlite3_column_int(stmt, 0); + +cleanup: + sqlite3_finalize(stmt); + return ret; +} + + +size_t dc_get_deaddrop_msg_cnt(dc_context_t* context) +{ + sqlite3_stmt* stmt = NULL; + size_t ret = 0; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || context->sql->cobj==NULL) { + goto cleanup; + } + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT COUNT(*) FROM msgs m LEFT JOIN chats c ON c.id=m.chat_id WHERE c.blocked=2;"); + if (sqlite3_step(stmt)!=SQLITE_ROW) { + goto cleanup; + } + + ret = sqlite3_column_int(stmt, 0); + +cleanup: + sqlite3_finalize(stmt); + return ret; +} + + +int dc_rfc724_mid_cnt(dc_context_t* context, const char* rfc724_mid) +{ + /* check the number of messages with the same rfc724_mid */ + int ret = 0; + sqlite3_stmt* stmt = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || context->sql->cobj==NULL) { + goto cleanup; + } + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT COUNT(*) FROM msgs WHERE rfc724_mid=?;"); + sqlite3_bind_text(stmt, 1, rfc724_mid, -1, SQLITE_STATIC); + if (sqlite3_step(stmt)!=SQLITE_ROW) { + goto cleanup; + } + + ret = sqlite3_column_int(stmt, 0); + +cleanup: + sqlite3_finalize(stmt); + return ret; +} + + +/* check, if the given Message-ID exists in the database (if not, the message is normally downloaded from the server and parsed, +so, we should even keep unuseful messages in the database (we can leave the other fields empty to save space) */ +uint32_t dc_rfc724_mid_exists(dc_context_t* context, const char* rfc724_mid, char** ret_server_folder, uint32_t* ret_server_uid) +{ + uint32_t ret = 0; + sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, + "SELECT server_folder, server_uid, id FROM msgs WHERE rfc724_mid=?;"); + sqlite3_bind_text(stmt, 1, rfc724_mid, -1, SQLITE_STATIC); + if (sqlite3_step(stmt)!=SQLITE_ROW) { + if (ret_server_folder) { *ret_server_folder = NULL; } + if (ret_server_uid) { *ret_server_uid = 0; } + goto cleanup; + } + + if (ret_server_folder) { *ret_server_folder = dc_strdup((char*)sqlite3_column_text(stmt, 0)); } + if (ret_server_uid) { *ret_server_uid = sqlite3_column_int(stmt, 1); /* may be 0 */ } + ret = sqlite3_column_int(stmt, 2); + +cleanup: + sqlite3_finalize(stmt); + return ret; +} + + +void dc_update_server_uid(dc_context_t* context, const char* rfc724_mid, const char* server_folder, uint32_t server_uid) +{ + sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, + "UPDATE msgs SET server_folder=?, server_uid=? WHERE rfc724_mid=?;"); /* we update by "rfc724_mid" instead of "id" as there may be several db-entries refering to the same "rfc724_mid" */ + sqlite3_bind_text(stmt, 1, server_folder, -1, SQLITE_STATIC); + sqlite3_bind_int (stmt, 2, server_uid); + sqlite3_bind_text(stmt, 3, rfc724_mid, -1, SQLITE_STATIC); + sqlite3_step(stmt); + sqlite3_finalize(stmt); +} + + +/** + * Get a single message object of the type dc_msg_t. + * For a list of messages in a chat, see dc_get_chat_msgs() + * For a list or chats, see dc_get_chatlist() + * + * @memberof dc_context_t + * @param context The context as created by dc_context_new(). + * @param msg_id The message ID for which the message object should be created. + * @return A dc_msg_t message object. When done, the object must be freed using dc_msg_unref() + */ +dc_msg_t* dc_get_msg(dc_context_t* context, uint32_t msg_id) +{ + int success = 0; + dc_msg_t* obj = dc_msg_new(); + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + goto cleanup; + } + + if (!dc_msg_load_from_db(obj, context, msg_id)) { + goto cleanup; + } + + success = 1; + +cleanup: + if (success) { + return obj; + } + else { + dc_msg_unref(obj); + return NULL; + } +} + + +/** + * Get an informational text for a single message. the text is multiline and may + * contain eg. the raw text of the message. + * + * The max. text returned is typically longer (about 100000 characters) than the + * max. text returned by dc_msg_get_text() (about 30000 characters). + * + * If the library is compiled for andoid, some basic html-formatting for he + * subject and the footer is added. However we should change this function so + * that it returns eg. an array of pairwise key-value strings and the caller + * can show the whole stuff eg. in a table. + * + * @memberof dc_context_t + * @param context the context object as created by dc_context_new(). + * @param msg_id the message id for which information should be generated + * @return text string, must be free()'d after usage + */ +char* dc_get_msg_info(dc_context_t* context, uint32_t msg_id) +{ + sqlite3_stmt* stmt = NULL; + dc_msg_t* msg = dc_msg_new(); + dc_contact_t* contact_from = dc_contact_new(context); + char* rawtxt = NULL; + char* p = NULL; + dc_strbuilder_t ret; + dc_strbuilder_init(&ret, 0); + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC) { + goto cleanup; + } + + dc_msg_load_from_db(msg, context, msg_id); + dc_contact_load_from_db(contact_from, context->sql, msg->from_id); + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT txt_raw FROM msgs WHERE id=?;"); + sqlite3_bind_int(stmt, 1, msg_id); + if (sqlite3_step(stmt)!=SQLITE_ROW) { + p = dc_mprintf("Cannot load message #%i.", (int)msg_id); dc_strbuilder_cat(&ret, p); free(p); + goto cleanup; + } + rawtxt = dc_strdup((char*)sqlite3_column_text(stmt, 0)); + sqlite3_finalize(stmt); + stmt = NULL; + + #ifdef __ANDROID__ + p = strchr(rawtxt, '\n'); + if (p) { + char* subject = rawtxt; + *p = 0; + p++; + rawtxt = dc_mprintf("%s\n%s", subject, p); + free(subject); + } + #endif + + dc_trim(rawtxt); + dc_truncate_str(rawtxt, DC_MAX_GET_INFO_LEN); + + /* add time */ + dc_strbuilder_cat(&ret, "Sent: "); + p = dc_timestamp_to_str(dc_msg_get_timestamp(msg)); dc_strbuilder_cat(&ret, p); free(p); + dc_strbuilder_cat(&ret, "\n"); + + if (msg->from_id!=DC_CONTACT_ID_SELF) { + dc_strbuilder_cat(&ret, "Received: "); + p = dc_timestamp_to_str(msg->timestamp_rcvd? msg->timestamp_rcvd : msg->timestamp); dc_strbuilder_cat(&ret, p); free(p); + dc_strbuilder_cat(&ret, "\n"); + } + + if (msg->from_id==DC_CONTACT_ID_DEVICE || msg->to_id==DC_CONTACT_ID_DEVICE) { + goto cleanup; // device-internal message, no further details needed + } + + /* add mdn's time and readers */ + stmt = dc_sqlite3_prepare(context->sql, + "SELECT contact_id, timestamp_sent FROM msgs_mdns WHERE msg_id=?;"); + sqlite3_bind_int (stmt, 1, msg_id); + while (sqlite3_step(stmt)==SQLITE_ROW) { + dc_strbuilder_cat(&ret, "Read: "); + p = dc_timestamp_to_str(sqlite3_column_int64(stmt, 1)); dc_strbuilder_cat(&ret, p); free(p); + dc_strbuilder_cat(&ret, " by "); + + dc_contact_t* contact = dc_contact_new(context); + dc_contact_load_from_db(contact, context->sql, sqlite3_column_int64(stmt, 0)); + p = dc_contact_get_display_name(contact); dc_strbuilder_cat(&ret, p); free(p); + dc_contact_unref(contact); + dc_strbuilder_cat(&ret, "\n"); + } + sqlite3_finalize(stmt); + stmt = NULL; + + /* add state */ + p = NULL; + switch (msg->state) { + case DC_STATE_IN_FRESH: p = dc_strdup("Fresh"); break; + case DC_STATE_IN_NOTICED: p = dc_strdup("Noticed"); break; + case DC_STATE_IN_SEEN: p = dc_strdup("Seen"); break; + case DC_STATE_OUT_DELIVERED: p = dc_strdup("Delivered"); break; + case DC_STATE_OUT_ERROR: p = dc_strdup("Error"); break; + case DC_STATE_OUT_MDN_RCVD: p = dc_strdup("Read"); break; + case DC_STATE_OUT_PENDING: p = dc_strdup("Pending"); break; + default: p = dc_mprintf("%i", msg->state); break; + } + dc_strbuilder_catf(&ret, "State: %s", p); + free(p); + + p = NULL; + int e2ee_errors; + if ((e2ee_errors=dc_param_get_int(msg->param, DC_PARAM_ERRONEOUS_E2EE, 0))) { + if (e2ee_errors&DC_E2EE_NO_VALID_SIGNATURE) { + p = dc_strdup("Encrypted, no valid signature"); + } + } + else if (dc_param_get_int(msg->param, DC_PARAM_GUARANTEE_E2EE, 0)) { + p = dc_strdup("Encrypted"); + } + + if (p) { + dc_strbuilder_catf(&ret, ", %s", p); + free(p); + } + dc_strbuilder_cat(&ret, "\n"); + + /* add sender (only for info messages as the avatar may not be shown for them) */ + if (dc_msg_is_info(msg)) { + dc_strbuilder_cat(&ret, "Sender: "); + p = dc_contact_get_name_n_addr(contact_from); dc_strbuilder_cat(&ret, p); free(p); + dc_strbuilder_cat(&ret, "\n"); + } + + /* add file info */ + char* file = dc_param_get(msg->param, DC_PARAM_FILE, NULL); + if (file) { + p = dc_mprintf("\nFile: %s, %i bytes\n", file, (int)dc_get_filebytes(file)); dc_strbuilder_cat(&ret, p); free(p); + } + + if (msg->type!=DC_MSG_TEXT) { + p = NULL; + switch (msg->type) { + case DC_MSG_AUDIO: p = dc_strdup("Audio"); break; + case DC_MSG_FILE: p = dc_strdup("File"); break; + case DC_MSG_GIF: p = dc_strdup("GIF"); break; + case DC_MSG_IMAGE: p = dc_strdup("Image"); break; + case DC_MSG_VIDEO: p = dc_strdup("Video"); break; + case DC_MSG_VOICE: p = dc_strdup("Voice"); break; + default: p = dc_mprintf("%i", msg->type); break; + } + dc_strbuilder_catf(&ret, "Type: %s\n", p); + free(p); + } + + int w = dc_param_get_int(msg->param, DC_PARAM_WIDTH, 0), h = dc_param_get_int(msg->param, DC_PARAM_HEIGHT, 0); + if (w!=0 || h!=0) { + p = dc_mprintf("Dimension: %i x %i\n", w, h); dc_strbuilder_cat(&ret, p); free(p); + } + + int duration = dc_param_get_int(msg->param, DC_PARAM_DURATION, 0); + if (duration!=0) { + p = dc_mprintf("Duration: %i ms\n", duration); dc_strbuilder_cat(&ret, p); free(p); + } + + /* add rawtext */ + if (rawtxt && rawtxt[0]) { + dc_strbuilder_cat(&ret, "\n"); + dc_strbuilder_cat(&ret, rawtxt); + dc_strbuilder_cat(&ret, "\n"); + } + + /* add Message-ID, Server-Folder and Server-UID; the database ID is normally only of interest if you have access to sqlite; if so you can easily get it from the "msgs" table. */ + #ifdef __ANDROID__ + dc_strbuilder_cat(&ret, ""); + #endif + + if (msg->rfc724_mid && msg->rfc724_mid[0]) { + dc_strbuilder_catf(&ret, "\nMessage-ID: %s", msg->rfc724_mid); + } + + if (msg->server_folder && msg->server_folder[0]) { + dc_strbuilder_catf(&ret, "\nLast seen as: %s/%i", msg->server_folder, (int)msg->server_uid); + } + + #ifdef __ANDROID__ + dc_strbuilder_cat(&ret, ""); + #endif + +cleanup: + sqlite3_finalize(stmt); + dc_msg_unref(msg); + dc_contact_unref(contact_from); + free(rawtxt); + return ret.buf; +} + + +/** + * Star/unstar messages by setting the last parameter to 0 (unstar) or 1 (star). + * Starred messages are collected in a virtual chat that can be shown using + * dc_get_chat_msgs() using the chat_id DC_CHAT_ID_STARRED. + * + * @memberof dc_context_t + * @param context The context object as created by dc_context_new() + * @param msg_ids An array of uint32_t message IDs defining the messages to star or unstar + * @param msg_cnt The number of IDs in msg_ids + * @param star 0=unstar the messages in msg_ids, 1=star them + * @return none + */ +void dc_star_msgs(dc_context_t* context, const uint32_t* msg_ids, int msg_cnt, int star) +{ + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || msg_ids==NULL || msg_cnt<=0 || (star!=0 && star!=1)) { + return; + } + + dc_sqlite3_begin_transaction(context->sql); + + sqlite3_stmt* stmt = dc_sqlite3_prepare(context->sql, + "UPDATE msgs SET starred=? WHERE id=?;"); + for (int i = 0; i < msg_cnt; i++) + { + sqlite3_reset(stmt); + sqlite3_bind_int(stmt, 1, star); + sqlite3_bind_int(stmt, 2, msg_ids[i]); + sqlite3_step(stmt); + } + sqlite3_finalize(stmt); + + dc_sqlite3_commit(context->sql); +} + + +/******************************************************************************* + * Delete messages + ******************************************************************************/ + + +/** + * Delete messages. The messages are deleted on the current device and + * on the IMAP server. + * + * @memberof dc_context_t + * @param context the context object as created by dc_context_new() + * @param msg_ids an array of uint32_t containing all message IDs that should be deleted + * @param msg_cnt the number of messages IDs in the msg_ids array + * @return none + */ +void dc_delete_msgs(dc_context_t* context, const uint32_t* msg_ids, int msg_cnt) +{ + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || msg_ids==NULL || msg_cnt<=0) { + return; + } + + dc_sqlite3_begin_transaction(context->sql); + + for (int i = 0; i < msg_cnt; i++) + { + dc_update_msg_chat_id(context, msg_ids[i], DC_CHAT_ID_TRASH); + dc_job_add(context, DC_JOB_DELETE_MSG_ON_IMAP, msg_ids[i], NULL, 0); + } + + dc_sqlite3_commit(context->sql); +} + + +/******************************************************************************* + * mark message as seen + ******************************************************************************/ + + +/** + * Mark a message as _seen_, updates the IMAP state and + * sends MDNs. If the message is not in a real chat (eg. a contact request), the + * message is only marked as NOTICED and no IMAP/MDNs is done. See also + * dc_marknoticed_chat() and dc_marknoticed_contact() + * + * @memberof dc_context_t + * @param context The context object. + * @param msg_ids an array of uint32_t containing all the messages IDs that should be marked as seen. + * @param msg_cnt The number of message IDs in msg_ids. + * @return none + */ +void dc_markseen_msgs(dc_context_t* context, const uint32_t* msg_ids, int msg_cnt) +{ + int transaction_pending = 0; + int i = 0; + int send_event = 0; + int curr_state = 0; + int curr_blocked = 0; + sqlite3_stmt* stmt = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || msg_ids==NULL || msg_cnt<=0) { + goto cleanup; + } + + dc_sqlite3_begin_transaction(context->sql); + transaction_pending = 1; + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT m.state, c.blocked " + " FROM msgs m " + " LEFT JOIN chats c ON c.id=m.chat_id " + " WHERE m.id=? AND m.chat_id>" DC_STRINGIFY(DC_CHAT_ID_LAST_SPECIAL)); + for (i = 0; i < msg_cnt; i++) + { + sqlite3_reset(stmt); + sqlite3_bind_int(stmt, 1, msg_ids[i]); + if (sqlite3_step(stmt)!=SQLITE_ROW) { + continue; + } + curr_state = sqlite3_column_int(stmt, 0); + curr_blocked = sqlite3_column_int(stmt, 1); + if (curr_blocked==0) + { + if (curr_state==DC_STATE_IN_FRESH || curr_state==DC_STATE_IN_NOTICED) { + dc_update_msg_state(context, msg_ids[i], DC_STATE_IN_SEEN); + dc_log_info(context, 0, "Seen message #%i.", msg_ids[i]); + dc_job_add(context, DC_JOB_MARKSEEN_MSG_ON_IMAP, msg_ids[i], NULL, 0); /* results in a call to dc_markseen_msg_on_imap() */ + send_event = 1; + } + } + else + { + /* message may be in contact requests, mark as NOTICED, this does not force IMAP updated nor send MDNs */ + if (curr_state==DC_STATE_IN_FRESH) { + dc_update_msg_state(context, msg_ids[i], DC_STATE_IN_NOTICED); + send_event = 1; + } + } + } + + dc_sqlite3_commit(context->sql); + transaction_pending = 0; + + /* the event is needed eg. to remove the deaddrop from the chatlist */ + if (send_event) { + context->cb(context, DC_EVENT_MSGS_CHANGED, 0, 0); + } + +cleanup: + if (transaction_pending) { dc_sqlite3_rollback(context->sql); } + sqlite3_finalize(stmt); +} + + +int dc_mdn_from_ext(dc_context_t* context, uint32_t from_id, const char* rfc724_mid, time_t timestamp_sent, + uint32_t* ret_chat_id, uint32_t* ret_msg_id) +{ + int read_by_all = 0; + sqlite3_stmt* stmt = NULL; + + if (context==NULL || context->magic!=DC_CONTEXT_MAGIC || from_id<=DC_CONTACT_ID_LAST_SPECIAL || rfc724_mid==NULL || ret_chat_id==NULL || ret_msg_id==NULL + || *ret_chat_id!=0 || *ret_msg_id!=0) { + goto cleanup; + } + + stmt = dc_sqlite3_prepare(context->sql, + "SELECT m.id, c.id, c.type, m.state FROM msgs m " + " LEFT JOIN chats c ON m.chat_id=c.id " + " WHERE rfc724_mid=? AND from_id=1 " + " ORDER BY m.id;"); /* the ORDER BY makes sure, if one rfc724_mid is splitted into its parts, we always catch the same one. However, we do not send multiparts, we do not request MDNs for multiparts, and should not receive read requests for multiparts. So this is currently more theoretical. */ + sqlite3_bind_text(stmt, 1, rfc724_mid, -1, SQLITE_STATIC); + if (sqlite3_step(stmt)!=SQLITE_ROW) { + goto cleanup; + } + *ret_msg_id = sqlite3_column_int(stmt, 0); + *ret_chat_id = sqlite3_column_int(stmt, 1); + int chat_type = sqlite3_column_int(stmt, 2); + int msg_state = sqlite3_column_int(stmt, 3); + sqlite3_finalize(stmt); + stmt = NULL; + + if (msg_state!=DC_STATE_OUT_PENDING && msg_state!=DC_STATE_OUT_DELIVERED) { + goto cleanup; /* eg. already marked as MDNS_RCVD. however, it is importent, that the message ID is set above as this will allow the caller eg. to move the message away */ + } + + // collect receipt senders, we do this also for normal chats as we may want to show the timestamp + stmt = dc_sqlite3_prepare(context->sql, + "SELECT contact_id FROM msgs_mdns WHERE msg_id=? AND contact_id=?;"); + sqlite3_bind_int(stmt, 1, *ret_msg_id); + sqlite3_bind_int(stmt, 2, from_id); + int mdn_already_in_table = (sqlite3_step(stmt)==SQLITE_ROW)? 1 : 0; + sqlite3_finalize(stmt); + stmt = NULL; + + if (!mdn_already_in_table) { + stmt = dc_sqlite3_prepare(context->sql, + "INSERT INTO msgs_mdns (msg_id, contact_id, timestamp_sent) VALUES (?, ?, ?);"); + sqlite3_bind_int (stmt, 1, *ret_msg_id); + sqlite3_bind_int (stmt, 2, from_id); + sqlite3_bind_int64(stmt, 3, timestamp_sent); + sqlite3_step(stmt); + sqlite3_finalize(stmt); + stmt = NULL; + } + + // Normal chat? that's quite easy. + if (chat_type==DC_CHAT_TYPE_SINGLE) { + dc_update_msg_state(context, *ret_msg_id, DC_STATE_OUT_MDN_RCVD); + read_by_all = 1; + goto cleanup; /* send event about new state */ + } + + // Group chat: get the number of receipt senders + stmt = dc_sqlite3_prepare(context->sql, + "SELECT COUNT(*) FROM msgs_mdns WHERE msg_id=?;"); + sqlite3_bind_int(stmt, 1, *ret_msg_id); + if (sqlite3_step(stmt)!=SQLITE_ROW) { + goto cleanup; /* error */ + } + int ist_cnt = sqlite3_column_int(stmt, 0); + sqlite3_finalize(stmt); + stmt = NULL; + + /* + Groupsize: Min. MDNs + + 1 S n/a + 2 SR 1 + 3 SRR 2 + 4 SRRR 2 + 5 SRRRR 3 + 6 SRRRRR 3 + + (S=Sender, R=Recipient) + */ + int soll_cnt = (dc_get_chat_contact_cnt(context, *ret_chat_id)+1/*for rounding, SELF is already included!*/) / 2; + if (ist_cnt < soll_cnt) { + goto cleanup; /* wait for more receipts */ + } + + /* got enough receipts :-) */ + dc_update_msg_state(context, *ret_msg_id, DC_STATE_OUT_MDN_RCVD); + read_by_all = 1; + +cleanup: + sqlite3_finalize(stmt); + return read_by_all; +} diff --git a/src/dc_msg.h b/src/dc_msg.h index fc3a8228..dc1e2fb0 100644 --- a/src/dc_msg.h +++ b/src/dc_msg.h @@ -99,11 +99,23 @@ void dc_msg_get_authorNtitle_from_filename (const char* pathNfilename #define DC_MSG_MAKE_FILENAME_SEARCHABLE(a) ((a)==DC_MSG_AUDIO || (a)==DC_MSG_FILE || (a)==DC_MSG_VIDEO) /* add filename.ext (without path) to text? this is needed for the fulltext search. The extension is useful to get all PDF, all MP3 etc. */ #define DC_MSG_MAKE_SUFFIX_SEARCHABLE(a) ((a)==DC_MSG_IMAGE || (a)==DC_MSG_GIF || (a)==DC_MSG_VOICE) -#define DC_APPROX_SUBJECT_CHARS 32 /* as we do not cut inside words, this results in about 32-42 characters. - Do not use too long subjects - we add a tag after the subject which gets truncated by the clients otherwise. - It should also be very clear, the subject is _not_ the whole message. - The value is also used for CC:-summaries */ +/* as we do not cut inside words, this results in about 32-42 characters. +Do not use too long subjects - we add a tag after the subject which gets truncated by the clients otherwise. +It should also be very clear, the subject is _not_ the whole message. +The value is also used for CC:-summaries */ +#define DC_APPROX_SUBJECT_CHARS 32 + + +// Context functions to work with messages +void dc_update_msg_chat_id (dc_context_t*, uint32_t msg_id, uint32_t chat_id); +void dc_update_msg_state (dc_context_t*, uint32_t msg_id, int state); +int dc_mdn_from_ext (dc_context_t*, uint32_t from_id, const char* rfc724_mid, time_t, uint32_t* ret_chat_id, uint32_t* ret_msg_id); /* returns 1 if an event should be send */ +size_t dc_get_real_msg_cnt (dc_context_t*); /* the number of messages assigned to real chat (!=deaddrop, !=trash) */ +size_t dc_get_deaddrop_msg_cnt (dc_context_t*); +int dc_rfc724_mid_cnt (dc_context_t*, const char* rfc724_mid); +uint32_t dc_rfc724_mid_exists (dc_context_t*, const char* rfc724_mid, char** ret_server_folder, uint32_t* ret_server_uid); +void dc_update_server_uid (dc_context_t*, const char* rfc724_mid, const char* server_folder, uint32_t server_uid); #ifdef __cplusplus diff --git a/src/dc_pgp.c b/src/dc_pgp.c index 6e41478c..da8aa547 100644 --- a/src/dc_pgp.c +++ b/src/dc_pgp.c @@ -640,7 +640,7 @@ int dc_pgp_pk_decrypt( dc_context_t* context, pgp_keyring_t* dummy_keys = calloc(1, sizeof(pgp_keyring_t)); pgp_validation_t* vresult = calloc(1, sizeof(pgp_validation_t)); key_id_t* recipients_key_ids = NULL; - unsigned recipients_count = 0; + unsigned recipients_cnt = 0; pgp_memory_t* keysmem = pgp_memory_new(); int i = 0; int success = 0; @@ -677,7 +677,7 @@ int dc_pgp_pk_decrypt( dc_context_t* context, /* decrypt */ { pgp_memory_t* outmem = pgp_decrypt_and_validate_buf(&s_io, vresult, ctext, ctext_bytes, private_keys, public_keys, - use_armor, &recipients_key_ids, &recipients_count); + use_armor, &recipients_key_ids, &recipients_cnt); if (outmem == NULL) { dc_log_warning(context, 0, "Decryption failed."); goto cleanup; diff --git a/src/dc_receive_imf.c b/src/dc_receive_imf.c index 569f9a02..b85d5576 100644 --- a/src/dc_receive_imf.c +++ b/src/dc_receive_imf.c @@ -914,7 +914,7 @@ static void create_or_lookup_group(dc_context_t* context, dc_mimeparser_t* mime_ /* check the number of receivers - the only critical situation is if the user hits "Reply" instead of "Reply all" in a non-messenger-client */ if (to_ids_cnt == 1 && mime_parser->is_send_by_messenger==0) { - int is_contact_cnt = dc_get_chat_contact_count(context, chat_id); + int is_contact_cnt = dc_get_chat_contact_cnt(context, chat_id); if (is_contact_cnt > 3 /* to_ids_cnt==1 may be "From: A, To: B, SELF" as SELF is not counted in to_ids_cnt. So everything up to 3 is no error. */) { chat_id = 0; create_or_lookup_adhoc_group(context, mime_parser, create_blocked, from_id, to_ids, &chat_id, &chat_id_blocked); @@ -996,7 +996,7 @@ void dc_receive_imf(dc_context_t* context, const char* imf_raw_not_terminated, s we use mailmime_parse() through dc_mimeparser (both call mailimf_struct_multiple_parse() somewhen, I did not found out anything that speaks against this approach yet) */ dc_mimeparser_parse(mime_parser, imf_raw_not_terminated, imf_raw_bytes); - if (dc_hash_count(&mime_parser->header)==0) { + if (dc_hash_cnt(&mime_parser->header)==0) { dc_log_info(context, 0, "No header."); goto cleanup; /* Error - even adding an empty record won't help as we do not know the message ID */ } diff --git a/src/dc_securejoin.c b/src/dc_securejoin.c index f6b978c8..4bd049c4 100644 --- a/src/dc_securejoin.c +++ b/src/dc_securejoin.c @@ -89,7 +89,7 @@ static int encrypted_and_signed(dc_mimeparser_t* mimeparser, const char* expecte return 0; } - if (dc_hash_count(mimeparser->e2ee_helper->signatures)<=0) { + if (dc_hash_cnt(mimeparser->e2ee_helper->signatures)<=0) { dc_log_warning(mimeparser->context, 0, "Message not signed."); return 0; } diff --git a/src/deltachat.h b/src/deltachat.h index 48b128df..31235062 100644 --- a/src/deltachat.h +++ b/src/deltachat.h @@ -265,8 +265,8 @@ void dc_set_draft (dc_context_t*, uint32_t chat_id, c #define DC_GCM_ADDDAYMARKER 0x01 dc_array_t* dc_get_chat_msgs (dc_context_t*, uint32_t chat_id, uint32_t flags, uint32_t marker1before); -int dc_get_total_msg_count (dc_context_t*, uint32_t chat_id); -int dc_get_fresh_msg_count (dc_context_t*, uint32_t chat_id); +int dc_get_msg_cnt (dc_context_t*, uint32_t chat_id); +int dc_get_fresh_msg_cnt (dc_context_t*, uint32_t chat_id); dc_array_t* dc_get_fresh_msgs (dc_context_t*); void dc_marknoticed_chat (dc_context_t*, uint32_t chat_id); dc_array_t* dc_get_chat_media (dc_context_t*, uint32_t chat_id, int msg_type, int or_msg_type); @@ -308,7 +308,7 @@ int dc_add_address_book (dc_context_t*, const char*); #define DC_GCL_ADD_SELF 0x02 dc_array_t* dc_get_contacts (dc_context_t*, uint32_t flags, const char* query); -int dc_get_blocked_count (dc_context_t*); +int dc_get_blocked_cnt (dc_context_t*); dc_array_t* dc_get_blocked_contacts (dc_context_t*); void dc_block_contact (dc_context_t*, uint32_t contact_id, int block); char* dc_get_contact_encrinfo (dc_context_t*, uint32_t contact_id); diff --git a/src/mrmailbox.h b/src/mrmailbox.h index 54253f70..fcd8e477 100644 --- a/src/mrmailbox.h +++ b/src/mrmailbox.h @@ -64,8 +64,8 @@ extern "C" { #define mrmailbox_send_vcard_msg dc_send_vcard_msg #define mrmailbox_set_draft dc_set_draft #define mrmailbox_get_chat_msgs dc_get_chat_msgs -#define mrmailbox_get_total_msg_count dc_get_total_msg_count -#define mrmailbox_get_fresh_msg_count dc_get_fresh_msg_count +#define mrmailbox_get_total_msg_count dc_get_msg_cnt +#define mrmailbox_get_fresh_msg_count dc_get_fresh_msg_cnt #define mrmailbox_get_fresh_msgs dc_get_fresh_msgs #define mrmailbox_marknoticed_chat dc_marknoticed_chat #define mrmailbox_get_chat_media dc_get_chat_media @@ -91,7 +91,7 @@ extern "C" { #define mrmailbox_create_contact dc_create_contact #define mrmailbox_add_address_book dc_add_address_book #define mrmailbox_get_contacts dc_get_contacts -#define mrmailbox_get_blocked_count dc_get_blocked_count +#define mrmailbox_get_blocked_count dc_get_blocked_cnt #define mrmailbox_get_blocked_contacts dc_get_blocked_contacts #define mrmailbox_block_contact dc_block_contact #define mrmailbox_get_contact_encrinfo dc_get_contact_encrinfo