1
0
Fork 0
mirror of https://github.com/deltachat/deltachat-core.git synced 2025-10-05 19:42:04 +02:00
deltachat-core/src/dc_chat.c
2018-06-25 18:35:10 +02:00

574 lines
15 KiB
C

/*******************************************************************************
*
* Delta Chat Core
* Copyright (C) 2017 Björn Petersen
* Contact: r10s@b44t.com, http://b44t.com
*
* This program is free software: you can redistribute it and/or modify it under
* the terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any later
* version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along with
* this program. If not, see http://www.gnu.org/licenses/ .
*
******************************************************************************/
#include "dc_context.h"
#include "dc_job.h"
#include "dc_smtp.h"
#include "dc_imap.h"
#include "dc_mimefactory.h"
#define MR_CHAT_MAGIC 0xc4a7c4a7
/**
* Create a chat object in memory.
*
* @private @memberof dc_chat_t
*
* @param mailbox The mailbox object that should be stored in the chat object.
*
* @return New and empty chat object, must be freed using dc_chat_unref().
*/
dc_chat_t* dc_chat_new(dc_context_t* mailbox)
{
dc_chat_t* ths = NULL;
if( mailbox == NULL || (ths=calloc(1, sizeof(dc_chat_t)))==NULL ) {
exit(14); /* cannot allocate little memory, unrecoverable error */
}
ths->m_magic = MR_CHAT_MAGIC;
ths->m_context = mailbox;
ths->m_type = DC_CHAT_TYPE_UNDEFINED;
ths->m_param = dc_param_new();
return ths;
}
/**
* Free a chat object.
*
* @memberof dc_chat_t
*
* @param chat Chat object are returned eg. by dc_get_chat().
*
* @return None.
*/
void dc_chat_unref(dc_chat_t* chat)
{
if( chat==NULL || chat->m_magic != MR_CHAT_MAGIC ) {
return;
}
dc_chat_empty(chat);
dc_param_unref(chat->m_param);
chat->m_magic = 0;
free(chat);
}
/**
* Empty a chat object.
*
* @private @memberof dc_chat_t
*
* @param chat The chat object to empty.
*
* @return None.
*/
void dc_chat_empty(dc_chat_t* chat)
{
if( chat == NULL || chat->m_magic != MR_CHAT_MAGIC ) {
return;
}
free(chat->m_name);
chat->m_name = NULL;
chat->m_draft_timestamp = 0;
free(chat->m_draft_text);
chat->m_draft_text = NULL;
chat->m_type = DC_CHAT_TYPE_UNDEFINED;
chat->m_id = 0;
free(chat->m_grpid);
chat->m_grpid = NULL;
chat->m_blocked = 0;
dc_param_set_packed(chat->m_param, NULL);
}
/*******************************************************************************
* Getters
******************************************************************************/
/**
* Get chat ID. The chat ID is the ID under which the chat is filed in the database.
*
* Special IDs:
* - DC_CHAT_ID_DEADDROP (1) - Virtual chat containing messages which senders are not confirmed by the user.
* - DC_CHAT_ID_STARRED (5) - Virtual chat containing all starred messages-
* - DC_CHAT_ID_ARCHIVED_LINK (6) - A link at the end of the chatlist, if present the UI should show the button "Archived chats"-
*
* "Normal" chat IDs are larger than these special IDs (larger than DC_CHAT_ID_LAST_SPECIAL).
*
* @memberof dc_chat_t
*
* @param chat The chat object.
*
* @return Chat ID. 0 on errors.
*/
uint32_t dc_chat_get_id(dc_chat_t* chat)
{
if( chat == NULL || chat->m_magic != MR_CHAT_MAGIC ) {
return 0;
}
return chat->m_id;
}
/**
* Get chat type.
*
* Currently, there are two chat types:
*
* - DC_CHAT_TYPE_SINGLE (100) - a normal chat is a chat with a single contact,
* chats_contacts contains one record for the user. DC_CONTACT_ID_SELF
* (see dc_contact_t::m_id) is added _only_ for a self talk.
*
* - DC_CHAT_TYPE_GROUP (120) - a group chat, chats_contacts conain all group
* members, incl. DC_CONTACT_ID_SELF
*
* - DC_CHAT_TYPE_VERIFIED_GROUP (130) - a verified group chat. In verified groups,
* all members are verified and encryption is always active and cannot be disabled.
*
* @memberof dc_chat_t
*
* @param chat The chat object.
*
* @return Chat type.
*/
int dc_chat_get_type(dc_chat_t* chat)
{
if( chat == NULL || chat->m_magic != MR_CHAT_MAGIC ) {
return DC_CHAT_TYPE_UNDEFINED;
}
return chat->m_type;
}
/**
* Get name of a chat. For one-to-one chats, this is the name of the contact.
* For group chats, this is the name given eg. to dc_create_group_chat() or
* received by a group-creation message.
*
* To change the name, use dc_set_chat_name()
*
* See also: dc_chat_get_subtitle()
*
* @memberof dc_chat_t
*
* @param chat The chat object.
*
* @return Chat name as a string. Must be free()'d after usage. Never NULL.
*/
char* dc_chat_get_name(dc_chat_t* chat)
{
if( chat == NULL || chat->m_magic != MR_CHAT_MAGIC ) {
return safe_strdup("Err");
}
return safe_strdup(chat->m_name);
}
/**
* Get a subtitle for a chat. The subtitle is eg. the email-address or the
* number of group members.
*
* See also: dc_chat_get_name()
*
* @memberof dc_chat_t
*
* @param chat The chat object to calulate the subtitle for.
*
* @return Subtitle as a string. Must be free()'d after usage. Never NULL.
*/
char* dc_chat_get_subtitle(dc_chat_t* chat)
{
/* returns either the address or the number of chat members */
char* ret = NULL;
sqlite3_stmt* stmt;
if( chat == NULL || chat->m_magic != MR_CHAT_MAGIC ) {
return safe_strdup("Err");
}
if( chat->m_type == DC_CHAT_TYPE_SINGLE && dc_param_exists(chat->m_param, DC_PARAM_SELFTALK) )
{
ret = dc_stock_str(DC_STR_SELFTALK_SUBTITLE);
}
else if( chat->m_type == DC_CHAT_TYPE_SINGLE )
{
int r;
dc_sqlite3_lock(chat->m_context->m_sql);
stmt = dc_sqlite3_predefine__(chat->m_context->m_sql, SELECT_a_FROM_chats_contacts_WHERE_i,
"SELECT c.addr FROM chats_contacts cc "
" LEFT JOIN contacts c ON c.id=cc.contact_id "
" WHERE cc.chat_id=?;");
sqlite3_bind_int(stmt, 1, chat->m_id);
r = sqlite3_step(stmt);
if( r == SQLITE_ROW ) {
ret = safe_strdup((const char*)sqlite3_column_text(stmt, 0));
}
dc_sqlite3_unlock(chat->m_context->m_sql);
}
else if( DC_CHAT_TYPE_IS_MULTI(chat->m_type) )
{
int cnt = 0;
if( chat->m_id == DC_CHAT_ID_DEADDROP )
{
ret = dc_stock_str(DC_STR_DEADDROP); /* typically, the subtitle for the deaddropn is not displayed at all */
}
else
{
dc_sqlite3_lock(chat->m_context->m_sql);
cnt = dc_get_chat_contact_count__(chat->m_context, chat->m_id);
ret = dc_stock_str_repl_pl(DC_STR_MEMBER, cnt /*SELF is included in group chats (if not removed)*/);
dc_sqlite3_unlock(chat->m_context->m_sql);
}
}
return ret? ret : safe_strdup("Err");
}
/**
* Get the chat's profile image.
* The profile image is set using dc_set_chat_profile_image() for groups.
* For normal chats, the profile image is set using dc_set_contact_profile_image() (not yet implemented).
*
* @memberof dc_chat_t
*
* @param chat The chat object.
*
* @return Path and file if the profile image, if any. NULL otherwise.
* Must be free()'d after usage.
*/
char* dc_chat_get_profile_image(dc_chat_t* chat)
{
if( chat == NULL || chat->m_magic != MR_CHAT_MAGIC ) {
return NULL;
}
return dc_param_get(chat->m_param, DC_PARAM_PROFILE_IMAGE, NULL);
}
/**
* Get draft for the chat, if any. A draft is a message that the user started to
* compose but that is not sent yet. You can save a draft for a chat using dc_set_draft().
*
* Drafts are considered when sorting messages and are also returned eg.
* by dc_chatlist_get_summary().
*
* @memberof dc_chat_t
*
* @param chat The chat object.
*
* @return Draft text, must be free()'d. Returns NULL if there is no draft.
*/
char* dc_chat_get_draft(dc_chat_t* chat)
{
if( chat == NULL || chat->m_magic != MR_CHAT_MAGIC ) {
return NULL;
}
return strdup_keep_null(chat->m_draft_text); /* may be NULL */
}
/**
* Get timestamp of the draft.
*
* The draft itself can be get using dc_chat_get_draft().
*
* @memberof dc_chat_t
*
* @param chat The chat object.
*
* @return Timestamp of the draft. 0 if there is no draft.
*/
time_t dc_chat_get_draft_timestamp(dc_chat_t* chat)
{
if( chat == NULL || chat->m_magic != MR_CHAT_MAGIC ) {
return 0;
}
return chat->m_draft_timestamp;
}
/**
* Get archived state.
*
* - 0 = normal chat, not archived, not sticky.
* - 1 = chat archived
* - 2 = chat sticky (reserved for future use, if you do not support this value, just treat the chat as a normal one)
*
* To archive or unarchive chats, use dc_archive_chat().
* If chats are archived, this should be shown in the UI by a little icon or text,
* eg. the search will also return archived chats.
*
* @memberof dc_chat_t
*
* @param chat The chat object.
*
* @return Archived state.
*/
int dc_chat_get_archived(dc_chat_t* chat)
{
if( chat == NULL || chat->m_magic != MR_CHAT_MAGIC ) {
return 0;
}
return chat->m_archived;
}
/**
* Check if a chat is still unpromoted. Chats are unpromoted until the first
* message is sent. With unpromoted chats, members can be sent, settings can be
* modified without the need of special status messages being sent.
*
* After the creation with dc_create_group_chat() the chat is usuall unpromoted
* until the first call to dc_send_msg() or dc_send_text_msg().
*
* @memberof dc_chat_t
*
* @param chat The chat object.
*
* @return 1=chat is still unpromoted, no message was ever send to the chat,
* 0=chat is not unpromoted, messages were send and/or received
*/
int dc_chat_is_unpromoted(dc_chat_t* chat)
{
if( chat == NULL || chat->m_magic != MR_CHAT_MAGIC ) {
return 0;
}
return dc_param_get_int(chat->m_param, DC_PARAM_UNPROMOTED, 0);
}
/**
* Check if a chat is verified. Verified chats contain only verified members
* and encryption is alwasy enabled. Verified chats are created using
* dc_create_group_chat() by setting the 'verified' parameter to true.
*
* @memberof dc_chat_t
*
* @param chat The chat object.
*
* @return 1=chat verified, 0=chat is not verified
*/
int dc_chat_is_verified(dc_chat_t* chat)
{
if( chat == NULL || chat->m_magic != MR_CHAT_MAGIC ) {
return 0;
}
return (chat->m_type==DC_CHAT_TYPE_VERIFIED_GROUP);
}
int dc_chat_are_all_members_verified__(dc_chat_t* chat)
{
int chat_verified = 0;
sqlite3_stmt* stmt;
if( chat == NULL || chat->m_magic != MR_CHAT_MAGIC ) {
goto cleanup;
}
if( chat->m_id == DC_CHAT_ID_DEADDROP || chat->m_id == DC_CHAT_ID_STARRED ) {
goto cleanup; // deaddrop & co. are never verified
}
stmt = dc_sqlite3_predefine__(chat->m_context->m_sql, SELECT_verified_FROM_chats_contacts_WHERE_chat_id,
"SELECT c.id, LENGTH(ps.verified_key_fingerprint) "
" FROM chats_contacts cc"
" LEFT JOIN contacts c ON c.id=cc.contact_id"
" LEFT JOIN acpeerstates ps ON c.addr=ps.addr "
" WHERE cc.chat_id=?;");
sqlite3_bind_int(stmt, 1, chat->m_id);
while( sqlite3_step(stmt) == SQLITE_ROW )
{
uint32_t contact_id = sqlite3_column_int(stmt, 0);
int has_verified_key = sqlite3_column_int(stmt, 1);
if( contact_id != DC_CONTACT_ID_SELF
&& !has_verified_key )
{
goto cleanup; // a single unverified contact results in an unverified chat
}
}
chat_verified = 1;
cleanup:
return chat_verified;
}
/**
* Check if a chat is a self talk. Self talks are normal chats with
* the only contact DC_CONTACT_ID_SELF.
*
* @memberof dc_chat_t
*
* @param chat The chat object.
*
* @return 1=chat is self talk, 0=chat is no self talk
*/
int dc_chat_is_self_talk(dc_chat_t* chat)
{
if( chat == NULL || chat->m_magic != MR_CHAT_MAGIC ) {
return 0;
}
return dc_param_exists(chat->m_param, DC_PARAM_SELFTALK);
}
/*******************************************************************************
* Misc.
******************************************************************************/
int dc_chat_update_param__(dc_chat_t* ths)
{
int success = 0;
sqlite3_stmt* stmt = dc_sqlite3_prepare_v2_(ths->m_context->m_sql, "UPDATE chats SET param=? WHERE id=?");
sqlite3_bind_text(stmt, 1, ths->m_param->m_packed, -1, SQLITE_STATIC);
sqlite3_bind_int (stmt, 2, ths->m_id);
success = sqlite3_step(stmt)==SQLITE_DONE? 1 : 0;
sqlite3_finalize(stmt);
return success;
}
static int dc_chat_set_from_stmt__(dc_chat_t* ths, sqlite3_stmt* row)
{
int row_offset = 0;
const char* draft_text;
if( ths == NULL || ths->m_magic != MR_CHAT_MAGIC || row == NULL ) {
return 0;
}
dc_chat_empty(ths);
#define MR_CHAT_FIELDS " c.id,c.type,c.name, c.draft_timestamp,c.draft_txt,c.grpid,c.param,c.archived, c.blocked "
ths->m_id = sqlite3_column_int (row, row_offset++); /* the columns are defined in MR_CHAT_FIELDS */
ths->m_type = sqlite3_column_int (row, row_offset++);
ths->m_name = safe_strdup((char*)sqlite3_column_text (row, row_offset++));
ths->m_draft_timestamp = sqlite3_column_int64(row, row_offset++);
draft_text = (const char*)sqlite3_column_text (row, row_offset++);
ths->m_grpid = safe_strdup((char*)sqlite3_column_text (row, row_offset++));
dc_param_set_packed(ths->m_param, (char*)sqlite3_column_text (row, row_offset++));
ths->m_archived = sqlite3_column_int (row, row_offset++);
ths->m_blocked = sqlite3_column_int (row, row_offset++);
/* We leave a NULL-pointer for the very usual situation of "no draft".
Also make sure, m_draft_text and m_draft_timestamp are set together */
if( ths->m_draft_timestamp && draft_text && draft_text[0] ) {
ths->m_draft_text = safe_strdup(draft_text);
}
else {
ths->m_draft_timestamp = 0;
}
/* correct the title of some special groups */
if( ths->m_id == DC_CHAT_ID_DEADDROP ) {
free(ths->m_name);
ths->m_name = dc_stock_str(DC_STR_DEADDROP);
}
else if( ths->m_id == DC_CHAT_ID_ARCHIVED_LINK ) {
free(ths->m_name);
char* tempname = dc_stock_str(DC_STR_ARCHIVEDCHATS);
ths->m_name = dc_mprintf("%s (%i)", tempname, dc_get_archived_count__(ths->m_context));
free(tempname);
}
else if( ths->m_id == DC_CHAT_ID_STARRED ) {
free(ths->m_name);
ths->m_name = dc_stock_str(DC_STR_STARREDMSGS);
}
else if( dc_param_exists(ths->m_param, DC_PARAM_SELFTALK) ) {
free(ths->m_name);
ths->m_name = dc_stock_str(DC_STR_SELF);
}
return row_offset; /* success, return the next row offset */
}
/**
* Library-internal.
*
* Calling this function is not thread-safe, locking is up to the caller.
*
* @private @memberof dc_chat_t
*
* @param chat The chat object that should be filled with the data from the database.
* Existing data are free()'d before using dc_chat_empty().
*
* @param chat_id Chat ID that should be loaded from the database.
*
* @return 1=success, 0=error.
*/
int dc_chat_load_from_db__(dc_chat_t* chat, uint32_t chat_id)
{
sqlite3_stmt* stmt;
if( chat==NULL || chat->m_magic != MR_CHAT_MAGIC ) {
return 0;
}
dc_chat_empty(chat);
stmt = dc_sqlite3_predefine__(chat->m_context->m_sql, SELECT_itndd_FROM_chats_WHERE_i,
"SELECT " MR_CHAT_FIELDS " FROM chats c WHERE c.id=?;");
sqlite3_bind_int(stmt, 1, chat_id);
if( sqlite3_step(stmt) != SQLITE_ROW ) {
return 0;
}
if( !dc_chat_set_from_stmt__(chat, stmt) ) {
return 0;
}
return 1;
}