/* * chat-room.cpp * Copyright (C) 2017 Belledonne Communications SARL * * 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 . */ #include #include "linphone/api/c-chat-message.h" #include "linphone/utils/utils.h" #include "c-wrapper/c-wrapper.h" #include "chat-room-p.h" #include "modifier/multipart-chat-message-modifier.h" #include "modifier/cpim-chat-message-modifier.h" #include "imdn.h" #include "content/content.h" #include "chat-message-p.h" #include "chat-room.h" #include "sal/message_op.h" #include "logger/logger.h" // ============================================================================= using namespace std; LINPHONE_BEGIN_NAMESPACE ChatRoomPrivate::ChatRoomPrivate (LinphoneCore *core) : core(core), isComposingHandler(core, this) {} ChatRoomPrivate::~ChatRoomPrivate () { /*for (auto &message : transientMessages) linphone_chat_message_release(message); if (pendingMessage) linphone_chat_message_unref(pendingMessage);*/ } // ----------------------------------------------------------------------------- int ChatRoomPrivate::createChatMessageFromDb (void *data, int argc, char **argv, char **colName) { ChatRoomPrivate *d = reinterpret_cast(data); return d->createChatMessageFromDb(argc, argv, colName); } // ----------------------------------------------------------------------------- void ChatRoomPrivate::addTransientMessage (shared_ptr msg) { auto iter = find(transientMessages.begin(), transientMessages.end(), msg); if (iter == transientMessages.end()) transientMessages.push_back(msg); } void ChatRoomPrivate::addWeakMessage (shared_ptr msg) { weak_ptr weakptr(msg); weakMessages.push_back(weakptr); } void ChatRoomPrivate::moveTransientMessageToWeakMessages (shared_ptr msg) { auto iter = find(transientMessages.begin(), transientMessages.end(), msg); if (iter != transientMessages.end()) { /* msg is not transient anymore, we can remove it from our transient list and unref it */ addWeakMessage(msg); removeTransientMessage(msg); } else { /* msg has already been removed from the transient messages, do nothing */ } } void ChatRoomPrivate::removeTransientMessage (shared_ptr msg) { auto iter = find(transientMessages.begin(), transientMessages.end(), msg); if (iter != transientMessages.end()) { transientMessages.erase(iter); } } // ----------------------------------------------------------------------------- void ChatRoomPrivate::release () { L_Q(); isComposingHandler.stopTimers(); /*for (auto &message : weakMessages) linphone_chat_message_deactivate(message); for (auto &message : transientMessages) linphone_chat_message_deactivate(message);*/ core = nullptr; linphone_chat_room_unref(L_GET_C_BACK_PTR(q)); } void ChatRoomPrivate::sendImdn (const string &payload, LinphoneReason reason) { L_Q(); const char *identity = nullptr; LinphoneAddress *peer = linphone_address_new(peerAddress.asString().c_str()); LinphoneProxyConfig *proxy = linphone_core_lookup_known_proxy(core, peer); if (proxy) identity = linphone_address_as_string(linphone_proxy_config_get_identity_address(proxy)); else identity = linphone_core_get_primary_contact(core); /* Sending out of call */ SalMessageOp *op = new SalMessageOp(core->sal); linphone_configure_op(core, op, peer, nullptr, !!lp_config_get_int(core->config, "sip", "chat_msg_with_contact", 0)); shared_ptr msg = q->createMessage(); msg->setFromAddress(identity); msg->setToAddress(peerAddress.asString()); shared_ptr content = make_shared(); content->setContentType("message/imdn+xml"); content->setBody(payload); msg->addContent(content); /* Do not try to encrypt the notification when it is reporting an error (maybe it should be bypassed only for some reasons). */ int retval = -1; LinphoneImEncryptionEngine *imee = linphone_core_get_im_encryption_engine(core); if (imee && (reason == LinphoneReasonNone)) { LinphoneImEncryptionEngineCbs *imeeCbs = linphone_im_encryption_engine_get_callbacks(imee); LinphoneImEncryptionEngineCbsOutgoingMessageCb cbProcessOutgoingMessage = linphone_im_encryption_engine_cbs_get_process_outgoing_message(imeeCbs); if (cbProcessOutgoingMessage) { retval = cbProcessOutgoingMessage(imee, L_GET_C_BACK_PTR(q), L_GET_C_BACK_PTR(msg)); } } if (retval <= 0) { op->send_message(identity, peerAddress.asString().c_str(), msg->getPrivate()->getContentType().c_str(), msg->getPrivate()->getText().c_str(), nullptr); } linphone_address_unref(peer); op->unref(); } // ----------------------------------------------------------------------------- int ChatRoomPrivate::getMessagesCount (bool unreadOnly) { if (!core->db) return 0; /* Optimization: do not read database if the count is already available in memory */ if (unreadOnly && unreadCount >= 0) return unreadCount; string peer = peerAddress.asStringUriOnly(); char *option = nullptr; if (unreadOnly) option = bctbx_strdup_printf("AND status!=%i AND direction=%i", LinphoneChatMessageStateDisplayed, LinphoneChatMessageIncoming); char *buf = sqlite3_mprintf("SELECT count(*) FROM history WHERE remoteContact = %Q %s;", peer.c_str(), unreadOnly ? option : ""); sqlite3_stmt *selectStatement; int numrows = 0; int returnValue = sqlite3_prepare_v2(core->db, buf, -1, &selectStatement, nullptr); if (returnValue == SQLITE_OK) { if (sqlite3_step(selectStatement) == SQLITE_ROW) { numrows = sqlite3_column_int(selectStatement, 0); } } sqlite3_finalize(selectStatement); sqlite3_free(buf); /* No need to test the sign of unreadCount here because it has been tested above */ if (unreadOnly) { unreadCount = numrows; } if (option) bctbx_free(option); return numrows; } void ChatRoomPrivate::setState (ChatRoom::State newState) { L_Q(); if (newState != state) { state = newState; if (state == ChatRoom::State::Instantiated) linphone_core_notify_chat_room_instantiated(core, L_GET_C_BACK_PTR(q)); notifyStateChanged(); } } // ----------------------------------------------------------------------------- void ChatRoomPrivate::sendIsComposingNotification () { L_Q(); LinphoneImNotifPolicy *policy = linphone_core_get_im_notif_policy(core); if (linphone_im_notif_policy_get_send_is_composing(policy)) { LinphoneAddress *peer = linphone_address_new(peerAddress.asString().c_str()); LinphoneProxyConfig *proxy = linphone_core_lookup_known_proxy(core, peer); const char *identity = nullptr; if (proxy) identity = linphone_address_as_string(linphone_proxy_config_get_identity_address(proxy)); else identity = linphone_core_get_primary_contact(core); /* Sending out of call */ SalMessageOp *op = new SalMessageOp(core->sal); linphone_configure_op(core, op, peer, nullptr, !!lp_config_get_int(core->config, "sip", "chat_msg_with_contact", 0)); string payload = isComposingHandler.marshal(isComposing); if (!payload.empty()) { int retval = -1; shared_ptr msg = q->createMessage(); msg->setFromAddress(identity); msg->setToAddress(peerAddress.asString()); shared_ptr content = make_shared(); content->setContentType("application/im-iscomposing+xml"); content->setBody(payload); msg->addContent(content); LinphoneImEncryptionEngine *imee = linphone_core_get_im_encryption_engine(core); if (imee) { LinphoneImEncryptionEngineCbs *imeeCbs = linphone_im_encryption_engine_get_callbacks(imee); LinphoneImEncryptionEngineCbsOutgoingMessageCb cbProcessOutgoingMessage = linphone_im_encryption_engine_cbs_get_process_outgoing_message(imeeCbs); if (cbProcessOutgoingMessage) { retval = cbProcessOutgoingMessage(imee, L_GET_C_BACK_PTR(q), L_GET_C_BACK_PTR(msg)); } } if (retval <= 0) { op->send_message(identity, peerAddress.asString().c_str(), msg->getPrivate()->getContentType().c_str(), msg->getPrivate()->getText().c_str(), nullptr); } op->unref(); } linphone_address_unref(peer); } } // ----------------------------------------------------------------------------- /** * DB layout: * * | 0 | storage_id * | 1 | localContact * | 2 | remoteContact * | 3 | direction flag (LinphoneChatMessageDir) * | 4 | message (text content of the message) * | 5 | time (unused now, used to be string-based timestamp, replaced by the utc timestamp) * | 6 | read flag (no longer used, replaced by the LinphoneChatMessageStateDisplayed state) * | 7 | status (LinphoneChatMessageState) * | 8 | external body url (deprecated file transfer system) * | 9 | utc timestamp * | 10 | app data text * | 11 | linphone content id (LinphoneContent describing a file transfer) * | 12 | message id (used for IMDN) * | 13 | content type (of the message field [must be text representable]) * | 14 | secured flag */ int ChatRoomPrivate::createChatMessageFromDb (int argc, char **argv, char **colName) { L_Q(); unsigned int storageId = (unsigned int)atoi(argv[0]); /* Check if the message exists in the weak messages list, in which case we should return that one. */ shared_ptr message = getWeakMessage(storageId); if (!message) { /* Check if the message exists in the transient list, in which case we should return that one. */ message = getTransientMessage(storageId); } if (!message) { message = q->createMessage(); shared_ptr content = make_shared(); message->addContent(content); if (argv[4]) { content->setBody(argv[4]); } if (argv[13]) { content->setContentType(argv[13]); } Address peer(peerAddress.asString()); if (atoi(argv[3]) == ChatMessage::Direction::Incoming) { message->getPrivate()->setDirection(ChatMessage::Direction::Incoming); message->setFromAddress(peer); } else { message->getPrivate()->setDirection(ChatMessage::Direction::Outgoing); message->setToAddress(peer); } message->getPrivate()->setTime((time_t)atol(argv[9])); message->getPrivate()->setState((ChatMessage::State)atoi(argv[7])); message->getPrivate()->setStorageId(storageId); if (argv[8]) { message->setExternalBodyUrl(argv[8]); } if (argv[10]) { message->setAppdata(argv[10]); } message->setId(argv[12]); message->setIsSecured((bool)atoi(argv[14])); if (argv[11]) { int id = atoi(argv[11]); if (id >= 0) linphone_chat_message_fetch_content_from_database(core->db, L_GET_C_BACK_PTR(message), id); } /* Fix content type for old messages that were stored without it */ /* To keep ? if (!linphone_chat_message_get_content_type(newMessage)) { if (linphone_chat_message_get_file_transfer_information(newMessage)) { linphone_chat_message_set_content_type(newMessage, ms_strdup("application/vnd.gsma.rcs-ft-http+xml")); } else if (linphone_chat_message_get_external_body_url(newMessage)) { linphone_chat_message_set_content_type(newMessage, ms_strdup("message/external-body")); } else { linphone_chat_message_set_content_type(newMessage, ms_strdup("text/plain")); } }*/ /* Add the new message to the weak messages list. */ addWeakMessage(message); } messages.push_front(message); return 0; } shared_ptr ChatRoomPrivate::getTransientMessage (unsigned int storageId) const { for (auto &message : transientMessages) { if (message->getPrivate()->getStorageId() == storageId) return message; } return nullptr; } std::shared_ptr ChatRoomPrivate::getWeakMessage (unsigned int storageId) const { for (auto &message : weakMessages) { shared_ptr msg(message); if (msg->getPrivate()->getStorageId() == storageId) return msg; } return nullptr; } int ChatRoomPrivate::sqlRequest (sqlite3 *db, const string &stmt) { char *errmsg = nullptr; int ret = sqlite3_exec(db, stmt.c_str(), nullptr, nullptr, &errmsg); if (ret != SQLITE_OK) { lError() << "ChatRoomPrivate::sqlRequest: statement " << stmt << " -> error sqlite3_exec(): " << errmsg; sqlite3_free(errmsg); } return ret; } void ChatRoomPrivate::sqlRequestMessage (sqlite3 *db, const string &stmt) { char *errmsg = nullptr; int ret = sqlite3_exec(db, stmt.c_str(), createChatMessageFromDb, this, &errmsg); if (ret != SQLITE_OK) { lError() << "Error in creation: " << errmsg; sqlite3_free(errmsg); } } list > ChatRoomPrivate::findMessages (const string &messageId) { if (!core->db) return list >(); string peer = peerAddress.asStringUriOnly(); char *buf = sqlite3_mprintf("SELECT * FROM history WHERE remoteContact = %Q AND messageId = %Q", peer.c_str(), messageId.c_str()); messages.clear(); sqlRequestMessage(core->db, buf); sqlite3_free(buf); list > result = messages; messages.clear(); return result; } void ChatRoomPrivate::storeOrUpdateMessage (shared_ptr msg) { msg->store(); } // ----------------------------------------------------------------------------- LinphoneReason ChatRoomPrivate::messageReceived (SalOp *op, const SalMessage *salMsg) { L_Q(); bool increaseMsgCount = true; LinphoneReason reason = LinphoneReasonNone; shared_ptr msg; /* Check if this is a duplicate message */ if ((msg = q->findMessageWithDirection(op->get_call_id(), ChatMessage::Direction::Incoming))) { reason = core->chat_deny_code; return reason; } msg = q->createMessage(); shared_ptr content = make_shared(); content->setContentType(salMsg->content_type); content->setBody(salMsg->text ? salMsg->text : ""); msg->addContent(content); msg->setToAddress( op->get_to() ? op->get_to() : linphone_core_get_identity(core)); msg->getPrivate()->setTime(salMsg->time); msg->getPrivate()->setState(ChatMessage::State::Delivered); msg->getPrivate()->setDirection(ChatMessage::Direction::Incoming); msg->setId(op->get_call_id()); const SalCustomHeader *ch = op->get_recv_custom_header(); if (ch) msg->getPrivate()->setSalCustomHeaders(sal_custom_header_clone(ch)); if (salMsg->url) msg->setExternalBodyUrl(salMsg->url); int retval = -1; LinphoneImEncryptionEngine *imee = core->im_encryption_engine; if (imee) { LinphoneImEncryptionEngineCbs *imeeCbs = linphone_im_encryption_engine_get_callbacks(imee); LinphoneImEncryptionEngineCbsIncomingMessageCb cbProcessIncomingMessage = linphone_im_encryption_engine_cbs_get_process_incoming_message(imeeCbs); if (cbProcessIncomingMessage) { retval = cbProcessIncomingMessage(imee, L_GET_C_BACK_PTR(q), L_GET_C_BACK_PTR(msg)); if (retval == 0) { msg->setIsSecured(true); } else if (retval > 0) { /* Unable to decrypt message */ notifyUndecryptableMessageReceived(msg); reason = linphone_error_code_to_reason(retval); msg->sendDeliveryNotification(reason); /* Return LinphoneReasonNone to avoid flexisip resending us a message we can't decrypt */ reason = LinphoneReasonNone; goto end; } } } if ((retval <= 0) && (linphone_core_is_content_type_supported(core, msg->getPrivate()->getContentType().c_str()) == FALSE)) { retval = 415; lError() << "Unsupported MESSAGE (content-type " << msg->getPrivate()->getContentType() << " not recognized)"; } if (retval > 0) { reason = linphone_error_code_to_reason(retval); msg->sendDeliveryNotification(reason); goto end; } if (ContentType::isFileTransfer(msg->getPrivate()->getContentType())) { create_file_transfer_information_from_vnd_gsma_rcs_ft_http_xml(L_GET_C_BACK_PTR(msg)); msg->setIsToBeStored(true); } else if (ContentType::isImIsComposing(msg->getPrivate()->getContentType())) { isComposingReceived(msg->getPrivate()->getText()); msg->setIsToBeStored(false); increaseMsgCount = FALSE; if (lp_config_get_int(core->config, "sip", "deliver_imdn", 0) != 1) { goto end; } } else if (ContentType::isImdn(msg->getPrivate()->getContentType())) { imdnReceived(msg->getPrivate()->getText()); msg->setIsToBeStored(false); increaseMsgCount = FALSE; if (lp_config_get_int(core->config, "sip", "deliver_imdn", 0) != 1) { goto end; } } else if (ContentType::isText(msg->getPrivate()->getContentType())) { msg->setIsToBeStored(true); } if (increaseMsgCount) { if (unreadCount < 0) unreadCount = 1; else unreadCount++; /* Mark the message as pending so that if ChatRoom::markAsRead() is called in the * ChatRoomPrivate::chatMessageReceived() callback, it will effectively be marked as * being read before being stored. */ pendingMessage = msg; } chatMessageReceived(msg); if (msg->isToBeStored()) { msg->store(); } pendingMessage = nullptr; end: return reason; } // ----------------------------------------------------------------------------- void ChatRoomPrivate::chatMessageReceived (shared_ptr msg) { L_Q(); if (!ContentType::isImdn(msg->getPrivate()->getContentType()) && !ContentType::isImIsComposing(msg->getPrivate()->getContentType())) { notifyChatMessageReceived(msg); remoteIsComposing = false; linphone_core_notify_is_composing_received(core, L_GET_C_BACK_PTR(q)); msg->sendDeliveryNotification(LinphoneReasonNone); } } void ChatRoomPrivate::imdnReceived (const string &text) { L_Q(); Imdn::parse(*q, text); } void ChatRoomPrivate::isComposingReceived (const string &text) { isComposingHandler.parse(text); } // ----------------------------------------------------------------------------- void ChatRoomPrivate::notifyChatMessageReceived (shared_ptr msg) { L_Q(); LinphoneChatRoom *cr = L_GET_C_BACK_PTR(q); if (!msg->getPrivate()->getText().empty()) { /* Legacy API */ linphone_core_notify_text_message_received(core, cr, L_GET_C_BACK_PTR(&msg->getFromAddress()), msg->getPrivate()->getText().c_str()); } LinphoneChatRoomCbs *cbs = linphone_chat_room_get_callbacks(cr); LinphoneChatRoomCbsMessageReceivedCb cb = linphone_chat_room_cbs_get_message_received(cbs); if (cb) cb(cr, L_GET_C_BACK_PTR(msg)); linphone_core_notify_message_received(core, cr, L_GET_C_BACK_PTR(msg)); } void ChatRoomPrivate::notifyStateChanged () { L_Q(); LinphoneChatRoom *cr = L_GET_C_BACK_PTR(q); LinphoneChatRoomCbs *cbs = linphone_chat_room_get_callbacks(cr); LinphoneChatRoomCbsStateChangedCb cb = linphone_chat_room_cbs_get_state_changed(cbs); if (cb) cb(cr, (LinphoneChatRoomState)state); } void ChatRoomPrivate::notifyUndecryptableMessageReceived (shared_ptr msg) { L_Q(); LinphoneChatRoom *cr = L_GET_C_BACK_PTR(q); LinphoneChatRoomCbs *cbs = linphone_chat_room_get_callbacks(cr); LinphoneChatRoomCbsUndecryptableMessageReceivedCb cb = linphone_chat_room_cbs_get_undecryptable_message_received(cbs); if (cb) cb(cr, L_GET_C_BACK_PTR(msg)); linphone_core_notify_message_received_unable_decrypt(core, cr, L_GET_C_BACK_PTR(msg)); } // ----------------------------------------------------------------------------- void ChatRoomPrivate::onIsComposingStateChanged (bool isComposing) { this->isComposing = isComposing; sendIsComposingNotification(); } void ChatRoomPrivate::onIsRemoteComposingStateChanged (bool isComposing) { L_Q(); remoteIsComposing = isComposing; linphone_core_notify_is_composing_received(core, L_GET_C_BACK_PTR(q)); } void ChatRoomPrivate::onIsComposingRefreshNeeded () { sendIsComposingNotification(); } // ============================================================================= ChatRoom::ChatRoom (LinphoneCore *core) : Object(*new ChatRoomPrivate(core)) {} ChatRoom::ChatRoom (ChatRoomPrivate &p) : Object(p) {} // ----------------------------------------------------------------------------- void ChatRoom::compose () { L_D(); if (!d->isComposing) { d->isComposing = true; d->sendIsComposingNotification(); d->isComposingHandler.startRefreshTimer(); } d->isComposingHandler.startIdleTimer(); } shared_ptr ChatRoom::createFileTransferMessage (const LinphoneContent *initialContent) { shared_ptr chatMessage = createMessage(); chatMessage->getPrivate()->setDirection(ChatMessage::Direction::Outgoing); chatMessage->getPrivate()->setFileTransferInformation(linphone_content_copy(initialContent)); return chatMessage; } shared_ptr ChatRoom::createMessage (const string &message) { shared_ptr chatMessage = createMessage(); shared_ptr content = make_shared(); content->setContentType("text/plain"); content->setBody(message); chatMessage->addContent(content); return chatMessage; } shared_ptr ChatRoom::createMessage () { L_D(); shared_ptr chatMessage = make_shared(static_pointer_cast(shared_from_this())); chatMessage->getPrivate()->setTime(ms_time(0)); chatMessage->setToAddress(d->peerAddress); chatMessage->setFromAddress(linphone_core_get_identity(d->core)); return chatMessage; } void ChatRoom::deleteHistory () { L_D(); if (!d->core->db) return; string peer = d->peerAddress.asStringUriOnly(); char *buf = sqlite3_mprintf("DELETE FROM history WHERE remoteContact = %Q;", peer.c_str()); d->sqlRequest(d->core->db, buf); sqlite3_free(buf); if (d->unreadCount > 0) d->unreadCount = 0; } void ChatRoom::deleteMessage (shared_ptr msg) { L_D(); if (!d->core->db) return; char *buf = sqlite3_mprintf("DELETE FROM history WHERE id = %u;", msg->getPrivate()->getStorageId()); d->sqlRequest(d->core->db, buf); sqlite3_free(buf); /* Invalidate unread_count when we modify the database, so that next time we need it it will be recomputed from latest database state */ d->unreadCount = -1; } shared_ptr ChatRoom::findMessage (const string &messageId) { L_D(); shared_ptr cm = nullptr; list > l = d->findMessages(messageId); if (!l.empty()) { cm = l.front(); } return cm; } shared_ptr ChatRoom::findMessageWithDirection (const string &messageId, ChatMessage::Direction direction) { L_D(); shared_ptr ret = nullptr; list > l = d->findMessages(messageId); for (auto &message : l) { if (message->getDirection() == direction) { ret = message; break; } } return ret; } list > ChatRoom::getHistory (int nbMessages) { return getHistoryRange(0, nbMessages - 1); } int ChatRoom::getHistorySize () { L_D(); return d->getMessagesCount(false); } list > ChatRoom::getHistoryRange (int startm, int endm) { L_D(); if (!d->core->db) return list >(); string peer = d->peerAddress.asStringUriOnly(); d->messages.clear(); /* Since we want to append query parameters depending on arguments given, we use malloc instead of sqlite3_mprintf */ const int bufMaxSize = 512; char *buf = reinterpret_cast(ms_malloc(bufMaxSize)); buf = sqlite3_snprintf(bufMaxSize - 1, buf, "SELECT * FROM history WHERE remoteContact = %Q ORDER BY id DESC", peer.c_str()); if (startm < 0) startm = 0; if (((endm > 0) && (endm >= startm)) || ((startm == 0) && (endm == 0))) { char *buf2 = ms_strdup_printf("%s LIMIT %i ", buf, endm + 1 - startm); ms_free(buf); buf = buf2; } else if (startm > 0) { ms_message("%s(): end is lower than start (%d < %d). Assuming no end limit.", __FUNCTION__, endm, startm); char *buf2 = ms_strdup_printf("%s LIMIT -1", buf); ms_free(buf); buf = buf2; } if (startm > 0) { char *buf2 = ms_strdup_printf("%s OFFSET %i ", buf, startm); ms_free(buf); buf = buf2; } uint64_t begin = ortp_get_cur_time_ms(); d->sqlRequestMessage(d->core->db, buf); uint64_t end = ortp_get_cur_time_ms(); if ((endm + 1 - startm) > 1) { /* Display message only if at least 2 messages are loaded */ ms_message("%s(): completed in %i ms", __FUNCTION__, (int)(end - begin)); } ms_free(buf); if (!d->messages.empty()) { /* Fill local addr with core identity instead of per message */ for (auto &message : d->messages) { if (message->isOutgoing()) { message->setFromAddress(linphone_core_get_identity(d->core)); } else { message->setToAddress(linphone_core_get_identity(d->core)); } } } list > result = d->messages; d->messages.clear(); return result; } int ChatRoom::getUnreadMessagesCount () { L_D(); return d->getMessagesCount(true); } bool ChatRoom::isRemoteComposing () const { L_D(); return d->remoteIsComposing; } void ChatRoom::markAsRead () { L_D(); if (!d->core->db) return; /* Optimization: do not modify the database if no message is marked as unread */ if (getUnreadMessagesCount() == 0) return; string peer = d->peerAddress.asStringUriOnly(); char *buf = sqlite3_mprintf("SELECT * FROM history WHERE remoteContact = %Q AND direction = %i AND status != %i", peer.c_str(), ChatMessage::Direction::Incoming, ChatMessage::State::Displayed); d->sqlRequestMessage(d->core->db, buf); sqlite3_free(buf); for (auto &message : d->messages) { message->sendDisplayNotification(); } d->messages.clear(); buf = sqlite3_mprintf("UPDATE history SET status=%i WHERE remoteContact=%Q AND direction=%i;", ChatMessage::State::Displayed, peer.c_str(), ChatMessage::Direction::Incoming); d->sqlRequest(d->core->db, buf); sqlite3_free(buf); if (d->pendingMessage) { d->pendingMessage->getPrivate()->setState(ChatMessage::State::Displayed); d->pendingMessage->sendDisplayNotification(); } d->unreadCount = 0; } void ChatRoom::sendMessage (shared_ptr msg) { L_D(); msg->getPrivate()->setDirection(ChatMessage::Direction::Outgoing); /* Check if we shall upload a file to a server */ if (msg->getPrivate()->getFileTransferInformation() && msg->getPrivate()->getContentType().empty()) { /* Open a transaction with the server and send an empty request(RCS5.1 section 3.5.4.8.3.1) */ if (msg->uploadFile() == 0) { /* Add to transient list only if message is going out */ d->addTransientMessage(msg); /* Store the message so that even if the upload is stopped, it can be done again */ d->storeOrUpdateMessage(msg); } else { return; } } else { SalOp *op = msg->getPrivate()->getSalOp(); LinphoneCall *call = nullptr; string identity; char *clearTextMessage = nullptr; char *clearTextContentType = nullptr; LinphoneAddress *peer = linphone_address_new(d->peerAddress.asString().c_str()); if (!msg->getPrivate()->getText().empty()) { clearTextMessage = ms_strdup(msg->getPrivate()->getText().c_str()); } if (!msg->getPrivate()->getContentType().empty()) { clearTextContentType = ms_strdup(msg->getPrivate()->getContentType().c_str()); } /* Add to transient list */ d->addTransientMessage(msg); msg->getPrivate()->setTime(ms_time(0)); if (lp_config_get_int(d->core->config, "sip", "chat_use_call_dialogs", 0) != 0) { call = linphone_core_get_call_by_remote_address(d->core, d->peerAddress.asString().c_str()); if (call) { if (linphone_call_get_state(call) == LinphoneCallConnected || linphone_call_get_state(call) == LinphoneCallStreamsRunning || linphone_call_get_state(call) == LinphoneCallPaused || linphone_call_get_state(call) == LinphoneCallPausing || linphone_call_get_state(call) == LinphoneCallPausedByRemote) { ms_message("send SIP msg through the existing call."); op = linphone_call_get_op(call); identity = linphone_core_find_best_identity(d->core, linphone_call_get_remote_address(call)); } } } if (identity.empty()) { LinphoneProxyConfig *proxy = linphone_core_lookup_known_proxy(d->core, peer); if (proxy) { identity = L_GET_CPP_PTR_FROM_C_OBJECT(linphone_proxy_config_get_identity_address(proxy))->asString(); } else { identity = linphone_core_get_primary_contact(d->core); } } msg->setFromAddress(identity); // --------------------------------------- // Start of message modification // --------------------------------------- int retval = -1; LinphoneImEncryptionEngine *imee = d->core->im_encryption_engine; if (imee) { LinphoneImEncryptionEngineCbs *imeeCbs = linphone_im_encryption_engine_get_callbacks(imee); LinphoneImEncryptionEngineCbsOutgoingMessageCb cbProcessOutgoingMessage = linphone_im_encryption_engine_cbs_get_process_outgoing_message(imeeCbs); if (cbProcessOutgoingMessage) { retval = cbProcessOutgoingMessage(imee, L_GET_C_BACK_PTR(this), L_GET_C_BACK_PTR(msg)); if (retval == 0) { msg->setIsSecured(true); } } } if (msg->getContents().size() > 1) { MultipartChatMessageModifier mcmm; mcmm.encode(msg->getPrivate()); } if (lp_config_get_int(d->core->config, "sip", "use_cpim", 0) == 1) { CpimChatMessageModifier ccmm; ccmm.encode(msg->getPrivate()); } // --------------------------------------- // End of message modification // --------------------------------------- if (!op) { /* Sending out of call */ msg->getPrivate()->setSalOp(op = new SalMessageOp(d->core->sal)); linphone_configure_op( d->core, op, peer, msg->getPrivate()->getSalCustomHeaders(), !!lp_config_get_int(d->core->config, "sip", "chat_msg_with_contact", 0) ); op->set_user_pointer(L_GET_C_BACK_PTR(msg)); /* If out of call, directly store msg */ } if (retval > 0) { sal_error_info_set((SalErrorInfo *)op->get_error_info(), SalReasonNotAcceptable, "SIP", retval, "Unable to encrypt IM", nullptr); d->storeOrUpdateMessage(msg); msg->updateState(ChatMessage::State::NotDelivered); linphone_address_unref(peer); return; } if (!msg->getExternalBodyUrl().empty()) { char *content_type = ms_strdup_printf("message/external-body; access-type=URL; URL=\"%s\"", msg->getExternalBodyUrl().c_str()); auto msgOp = dynamic_cast(op); msgOp->send_message(identity.c_str(), d->peerAddress.asString().c_str(), content_type, nullptr, nullptr); ms_free(content_type); } else { auto msgOp = dynamic_cast(op); if (!msg->getPrivate()->getContentType().empty()) { msgOp->send_message(identity.c_str(), d->peerAddress.asString().c_str(), msg->getPrivate()->getContentType().c_str(), msg->getPrivate()->getText().c_str(), d->peerAddress.asStringUriOnly().c_str()); } else { msgOp->send_message(identity.c_str(), d->peerAddress.asString().c_str(), msg->getPrivate()->getText().c_str()); } } if (!msg->getPrivate()->getText().empty() && clearTextMessage && strcmp(msg->getPrivate()->getText().c_str(), clearTextMessage) != 0) { /* We replace the encrypted message by the original one so it can be correctly stored and displayed by the application */ msg->getPrivate()->setText(clearTextMessage); } if (!msg->getPrivate()->getContentType().empty() && clearTextContentType && (strcmp(msg->getPrivate()->getContentType().c_str(), clearTextContentType) != 0)) { /* We replace the encrypted content type by the original one */ msg->getPrivate()->setContentType(clearTextContentType); } msg->setId(op->get_call_id()); /* must be known at that time */ d->storeOrUpdateMessage(msg); if (d->isComposing) d->isComposing = false; d->isComposingHandler.stopIdleTimer(); d->isComposingHandler.stopRefreshTimer(); if (clearTextMessage) { ms_free(clearTextMessage); } if (clearTextContentType) { ms_free(clearTextContentType); } linphone_address_unref(peer); if (call && linphone_call_get_op(call) == op) { /* In this case, chat delivery status is not notified, so unrefing chat message right now */ /* Might be better fixed by delivering status, but too costly for now */ d->removeTransientMessage(msg); return; } } /* If operation failed, we should not change message state */ if (msg->isOutgoing()) { msg->getPrivate()->setIsReadOnly(true); msg->getPrivate()->setState(ChatMessage::State::InProgress); } } // ----------------------------------------------------------------------------- LinphoneCore *ChatRoom::getCore () const { L_D(); return d->core; } // ----------------------------------------------------------------------------- const Address& ChatRoom::getPeerAddress () const { L_D(); return d->peerAddress; } ChatRoom::State ChatRoom::getState () const { L_D(); return d->state; } LINPHONE_END_NAMESPACE