/* * chat-message.cpp * Copyright (C) 2010-2018 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 2 * 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, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "object/object-p.h" #include "linphone/core.h" #include "linphone/lpconfig.h" #include "linphone/utils/utils.h" #include "address/address.h" #include "c-wrapper/c-wrapper.h" #include "call/call-p.h" #include "chat/chat-message/chat-message-p.h" #include "chat/chat-room/chat-room-p.h" #include "chat/chat-room/client-group-to-basic-chat-room.h" #include "chat/chat-room/real-time-text-chat-room.h" #include "chat/modifier/cpim-chat-message-modifier.h" #include "chat/modifier/encryption-chat-message-modifier.h" #include "chat/modifier/file-transfer-chat-message-modifier.h" #include "chat/modifier/multipart-chat-message-modifier.h" #include "conference/participant.h" #include "content/file-content.h" #include "content/content.h" #include "core/core.h" #include "core/core-p.h" #include "logger/logger.h" #include "chat/notification/imdn.h" #include "sip-tools/sip-headers.h" #include "ortp/b64.h" // ============================================================================= using namespace std; using namespace B64_NAMESPACE; LINPHONE_BEGIN_NAMESPACE ChatMessagePrivate::ChatMessagePrivate(const std::shared_ptr &cr, ChatMessage::Direction dir):fileTransferChatMessageModifier(cr->getCore()->getCCore()->http_provider) { direction = dir; setChatRoom(cr); } void ChatMessagePrivate::setDirection (ChatMessage::Direction dir) { direction = dir; } void ChatMessagePrivate::setTime (time_t t) { time = t; } void ChatMessagePrivate::setIsReadOnly (bool readOnly) { isReadOnly = readOnly; } void ChatMessagePrivate::setParticipantState (const IdentityAddress &participantAddress, ChatMessage::State newState) { L_Q(); if (!(q->getChatRoom()->getCapabilities() & AbstractChatRoom::Capabilities::Conference) || (linphone_config_get_bool(linphone_core_get_config(q->getChatRoom()->getCore()->getCCore()), "misc", "enable_simple_group_chat_message_state", TRUE )) ) { setState(newState); return; } if (!dbKey.isValid()) return; unique_ptr &mainDb = q->getChatRoom()->getCore()->getPrivate()->mainDb; shared_ptr eventLog = mainDb->getEventFromKey(dbKey); ChatMessage::State currentState = mainDb->getChatMessageParticipantState(eventLog, participantAddress); if (!validStateTransition(currentState, newState)) return; lInfo() << "Chat message " << this << ": moving participant '" << participantAddress.asString() << "' state to " << Utils::toString(newState); mainDb->setChatMessageParticipantState(eventLog, participantAddress, newState); list states = mainDb->getChatMessageParticipantStates(eventLog); size_t nbDisplayedStates = 0; size_t nbDeliveredToUserStates = 0; size_t nbNotDeliveredStates = 0; for (const auto &state : states) { switch (state) { case ChatMessage::State::Displayed: nbDisplayedStates++; break; case ChatMessage::State::DeliveredToUser: nbDeliveredToUserStates++; break; case ChatMessage::State::NotDelivered: nbNotDeliveredStates++; break; default: break; } } if (nbNotDeliveredStates > 0) setState(ChatMessage::State::NotDelivered); else if (nbDisplayedStates == states.size()) setState(ChatMessage::State::Displayed); else if ((nbDisplayedStates + nbDeliveredToUserStates) == states.size()) setState(ChatMessage::State::DeliveredToUser); } list> ChatMessagePrivate::getParticipantsInState (const ChatMessage::State state) const { L_Q(); list> participantsInState; if (!(q->getChatRoom()->getCapabilities() & AbstractChatRoom::Capabilities::Conference) || !dbKey.isValid()) { return participantsInState; } unique_ptr &mainDb = q->getChatRoom()->getCore()->getPrivate()->mainDb; shared_ptr eventLog = mainDb->getEventFromKey(dbKey); list addressesInState = mainDb->getChatMessageParticipantsInState(eventLog, state); const list> &participants = q->getChatRoom()->getParticipants(); for (IdentityAddress addr : addressesInState) { for (const auto &participant : participants) { if (participant->getAddress() == addr) { participantsInState.push_back(participant); } } } return participantsInState; } void ChatMessagePrivate::setState (ChatMessage::State newState, bool force) { L_Q(); if (force) state = newState; if (!validStateTransition(state, newState)) return; lInfo() << "Chat message " << this << ": moving from " << Utils::toString(state) << " to " << Utils::toString(newState); state = newState; LinphoneChatMessage *msg = L_GET_C_BACK_PTR(q); if (linphone_chat_message_get_message_state_changed_cb(msg)) linphone_chat_message_get_message_state_changed_cb(msg)( msg, (LinphoneChatMessageState)state, linphone_chat_message_get_message_state_changed_cb_user_data(msg) ); LinphoneChatMessageCbs *cbs = linphone_chat_message_get_callbacks(msg); if (cbs && linphone_chat_message_cbs_get_msg_state_changed(cbs)) linphone_chat_message_cbs_get_msg_state_changed(cbs)(msg, (LinphoneChatMessageState)state); if (state == ChatMessage::State::FileTransferDone && !hasFileTransferContent()) { // We wait until the file has been downloaded to send the displayed IMDN q->sendDisplayNotification(); setState(ChatMessage::State::Displayed); } else { updateInDb(); } } belle_http_request_t *ChatMessagePrivate::getHttpRequest () const { return fileTransferChatMessageModifier.getHttpRequest(); } void ChatMessagePrivate::setHttpRequest (belle_http_request_t *request) { fileTransferChatMessageModifier.setHttpRequest(request); } SalOp *ChatMessagePrivate::getSalOp () const { return salOp; } void ChatMessagePrivate::setSalOp (SalOp *op) { salOp = op; } SalCustomHeader *ChatMessagePrivate::getSalCustomHeaders () const { return salCustomHeaders; } void ChatMessagePrivate::setSalCustomHeaders (SalCustomHeader *headers) { salCustomHeaders = headers; } void ChatMessagePrivate::addSalCustomHeader (const string &name, const string &value) { salCustomHeaders = sal_custom_header_append(salCustomHeaders, name.c_str(), value.c_str()); } void ChatMessagePrivate::removeSalCustomHeader (const string &name) { salCustomHeaders = sal_custom_header_remove(salCustomHeaders, name.c_str()); } string ChatMessagePrivate::getSalCustomHeaderValue (const string &name) { return L_C_TO_STRING(sal_custom_header_find(salCustomHeaders, name.c_str())); } // ----------------------------------------------------------------------------- // Below methods are only for C API backward compatibility... // ----------------------------------------------------------------------------- bool ChatMessagePrivate::hasTextContent() const { for (const Content *c : getContents()) { if (c->getContentType() == ContentType::PlainText) { return true; } } return false; } const Content* ChatMessagePrivate::getTextContent() const { for (const Content *c : getContents()) { if (c->getContentType() == ContentType::PlainText) { return c; } } return &Utils::getEmptyConstRefObject(); } bool ChatMessagePrivate::hasFileTransferContent() const { for (const Content *c : getContents()) { if (c->getContentType() == ContentType::FileTransfer) { return true; } } return false; } const Content* ChatMessagePrivate::getFileTransferContent() const { for (const Content *c : getContents()) { if (c->getContentType() == ContentType::FileTransfer) { return c; } } return &Utils::getEmptyConstRefObject(); } const string &ChatMessagePrivate::getFileTransferFilepath () const { return fileTransferFilePath; } void ChatMessagePrivate::setFileTransferFilepath (const string &path) { fileTransferFilePath = path; } const string &ChatMessagePrivate::getAppdata () const { for (const Content *c : getContents()) { if (!c->getAppData("legacy").empty()) { return c->getAppData("legacy"); } } return Utils::getEmptyConstRefObject(); } void ChatMessagePrivate::setAppdata (const string &data) { bool contentFound = false; for (Content *c : getContents()) { c->setAppData("legacy", data); contentFound = true; break; } if (contentFound) { updateInDb(); } } const string &ChatMessagePrivate::getExternalBodyUrl () const { if (!externalBodyUrl.empty()) { return externalBodyUrl; } if (hasFileTransferContent()) { FileTransferContent *content = (FileTransferContent*) getFileTransferContent(); return content->getFileUrl(); } return Utils::getEmptyConstRefObject(); } void ChatMessagePrivate::setExternalBodyUrl (const string &url) { externalBodyUrl = url; } const ContentType &ChatMessagePrivate::getContentType () { loadContentsFromDatabase(); if (direction == ChatMessage::Direction::Incoming) { if (contents.size() > 0) { Content *content = contents.front(); cContentType = content->getContentType(); } else { cContentType = internalContent.getContentType(); } } else { if (internalContent.getContentType().isValid()) { cContentType = internalContent.getContentType(); } else { if (contents.size() > 0) { Content *content = contents.front(); cContentType = content->getContentType(); } } } return cContentType; } void ChatMessagePrivate::setContentType (const ContentType &contentType) { loadContentsFromDatabase(); if (contents.size() > 0 && internalContent.getContentType().isEmpty() && internalContent.isEmpty()) { internalContent.setBody(contents.front()->getBody()); } internalContent.setContentType(contentType); if ((currentSendStep &ChatMessagePrivate::Step::Started) != ChatMessagePrivate::Step::Started) { // if not started yet the sending also alter the first content if (contents.size() > 0) contents.front()->setContentType(contentType); } } const string &ChatMessagePrivate::getText () { loadContentsFromDatabase(); if (direction == ChatMessage::Direction::Incoming) { if (hasTextContent()) { cText = getTextContent()->getBodyAsString(); } else if (contents.size() > 0) { Content *content = contents.front(); cText = content->getBodyAsString(); } else { cText = internalContent.getBodyAsString(); } } else { if (!internalContent.isEmpty()) { cText = internalContent.getBodyAsString(); } else { if (contents.size() > 0) { Content *content = contents.front(); cText = content->getBodyAsString(); } } } return cText; } void ChatMessagePrivate::setText (const string &text) { loadContentsFromDatabase(); if (contents.size() > 0 && internalContent.getContentType().isEmpty() && internalContent.isEmpty()) { internalContent.setContentType(contents.front()->getContentType()); } internalContent.setBody(text); if ((currentSendStep &ChatMessagePrivate::Step::Started) != ChatMessagePrivate::Step::Started) { // if not started yet the sending also alter the first content if (contents.size() > 0) contents.front()->setBody(text); } } LinphoneContent *ChatMessagePrivate::getFileTransferInformation () const { if (hasFileTransferContent()) { return getFileTransferContent()->toLinphoneContent(); } for (const Content *c : getContents()) { if (c->isFile()) { FileContent *fileContent = (FileContent *)c; return fileContent->toLinphoneContent(); } } return nullptr; } void ChatMessagePrivate::setFileTransferInformation (const LinphoneContent *c_content) { L_Q(); // Create a FileContent, it will create the FileTransferContent at upload time FileContent *fileContent = new FileContent(); ContentType contentType(linphone_content_get_type(c_content), linphone_content_get_subtype(c_content)); fileContent->setContentType(contentType); fileContent->setFileSize(linphone_content_get_size(c_content)); fileContent->setFileName(linphone_content_get_name(c_content)); if (linphone_content_get_string_buffer(c_content)) { fileContent->setBody(linphone_content_get_string_buffer(c_content)); } q->addContent(*fileContent); } bool ChatMessagePrivate::downloadFile () { L_Q(); for (auto &content : getContents()) if (content->getContentType() == ContentType::FileTransfer) return q->downloadFile(*static_cast(content)); return false; } void ChatMessagePrivate::addContent (Content &content) { getContents().push_back(&content); } void ChatMessagePrivate::removeContent (const Content &content) { getContents().remove(&const_cast(content)); } void ChatMessagePrivate::loadFileTransferUrlFromBodyToContent() { L_Q(); int errorCode = 0; fileTransferChatMessageModifier.decode(q->getSharedFromThis(), errorCode); } std::string ChatMessagePrivate::createFakeFileTransferFromUrl(const std::string &url) { return fileTransferChatMessageModifier.createFakeFileTransferFromUrl(url); } void ChatMessagePrivate::setChatRoom (const shared_ptr &cr) { chatRoom = cr; chatRoomId = cr->getChatRoomId(); if (direction == ChatMessage::Direction::Outgoing) { fromAddress = chatRoomId.getLocalAddress(); toAddress = chatRoomId.getPeerAddress(); } else { fromAddress = chatRoomId.getPeerAddress(); toAddress = chatRoomId.getLocalAddress(); } } // ----------------------------------------------------------------------------- void ChatMessagePrivate::sendImdn (Imdn::Type imdnType, LinphoneReason reason) { L_Q(); shared_ptr msg = q->getChatRoom()->createChatMessage(); Content *content = new Content(); content->setContentType(ContentType::Imdn); content->setBody(Imdn::createXml(imdnId, time, imdnType, reason)); msg->addContent(*content); if (reason != LinphoneReasonNone) msg->getPrivate()->setEncryptionPrevented(true); msg->setToBeStored(false); msg->getPrivate()->addSalCustomHeader(PriorityHeader::HeaderName, PriorityHeader::NonUrgent); msg->getPrivate()->send(); } static void forceUtf8Content (Content &content) { // TODO: Deal with other content type in the future. ContentType contentType = content.getContentType(); if (contentType != ContentType::PlainText) return; string charset = contentType.getParameter(); if (charset.empty()) return; size_t n = charset.find("charset="); if (n == string::npos) return; L_BEGIN_LOG_EXCEPTION size_t begin = n + sizeof("charset"); size_t end = charset.find(";", begin); charset = charset.substr(begin, end - begin); if (Utils::stringToLower(charset) != "utf-8") { string utf8Body = Utils::convertString(content.getBodyAsUtf8String(), charset, "UTF-8"); if (!utf8Body.empty()) { // TODO: use move operator if possible in the future! content.setBodyFromUtf8(utf8Body); contentType.setParameter(string(contentType.getParameter()).replace(begin, end - begin, "UTF-8")); content.setContentType(contentType); } } L_END_LOG_EXCEPTION } void ChatMessagePrivate::notifyReceiving () { L_Q(); LinphoneChatRoom *chatRoom = L_GET_C_BACK_PTR(q->getChatRoom()); if ((getContentType() != ContentType::Imdn) && (getContentType() != ContentType::ImIsComposing)) { _linphone_chat_room_notify_chat_message_should_be_stored(chatRoom, L_GET_C_BACK_PTR(q->getSharedFromThis())); if (toBeStored) storeInDb(); } shared_ptr event = make_shared( ::time(nullptr), q->getSharedFromThis() ); _linphone_chat_room_notify_chat_message_received(chatRoom, L_GET_C_BACK_PTR(event)); // Legacy q->getChatRoom()->getPrivate()->notifyChatMessageReceived(q->getSharedFromThis()); if ((getContentType() != ContentType::Imdn) && (getContentType() != ContentType::ImIsComposing)) { q->sendDeliveryNotification(LinphoneReasonNone); } } LinphoneReason ChatMessagePrivate::receive () { L_Q(); int errorCode = 0; LinphoneReason reason = LinphoneReasonNone; shared_ptr core = q->getCore(); shared_ptr chatRoom = q->getChatRoom(); // --------------------------------------- // Start of message modification // --------------------------------------- if ((currentRecvStep &ChatMessagePrivate::Step::Encryption) == ChatMessagePrivate::Step::Encryption) { lInfo() << "Encryption step already done, skipping"; } else { EncryptionChatMessageModifier ecmm; ChatMessageModifier::Result result = ecmm.decode(q->getSharedFromThis(), errorCode); if (result == ChatMessageModifier::Result::Error) { /* Unable to decrypt message */ chatRoom->getPrivate()->notifyUndecryptableChatMessageReceived(q->getSharedFromThis()); reason = linphone_error_code_to_reason(errorCode); q->sendDeliveryNotification(reason); return reason; } else if (result == ChatMessageModifier::Result::Suspended) { currentRecvStep |= ChatMessagePrivate::Step::Encryption; return LinphoneReasonNone; } currentRecvStep |= ChatMessagePrivate::Step::Encryption; } if ((currentRecvStep &ChatMessagePrivate::Step::Cpim) == ChatMessagePrivate::Step::Cpim) { lInfo() << "Cpim step already done, skipping"; } else { if (internalContent.getContentType() == ContentType::Cpim) { CpimChatMessageModifier ccmm; ccmm.decode(q->getSharedFromThis(), errorCode); } currentRecvStep |= ChatMessagePrivate::Step::Cpim; } if ((currentRecvStep &ChatMessagePrivate::Step::Multipart) == ChatMessagePrivate::Step::Multipart) { lInfo() << "Multipart step already done, skipping"; } else { MultipartChatMessageModifier mcmm; mcmm.decode(q->getSharedFromThis(), errorCode); currentRecvStep |= ChatMessagePrivate::Step::Multipart; } if ((currentRecvStep & ChatMessagePrivate::Step::FileUpload) == ChatMessagePrivate::Step::FileUpload) { lInfo() << "File download step already done, skipping"; } else { // This will check if internal content is FileTransfer and make the appropriate changes loadFileTransferUrlFromBodyToContent(); currentRecvStep |= ChatMessagePrivate::Step::FileUpload; } if (contents.size() == 0) { // All previous modifiers only altered the internal content, let's fill the content list contents.push_back(new Content(internalContent)); } for (auto &content : contents) forceUtf8Content(*content); // --------------------------------------- // End of message modification // --------------------------------------- // Remove internal content as it is not needed anymore and will confuse some old methods like getText() internalContent.setBody(""); internalContent.setContentType(ContentType("")); // Also remove current step so we go through all modifiers if message is re-sent currentRecvStep = ChatMessagePrivate::Step::None; setState(ChatMessage::State::Delivered); if (errorCode <= 0) { bool foundSupportContentType = false; for (Content *c : contents) { if (linphone_core_is_content_type_supported(core->getCCore(), c->getContentType().asString().c_str())) { foundSupportContentType = true; break; } else lError() << "Unsupported content-type: " << c->getContentType().asString(); } if (!foundSupportContentType) { errorCode = 415; lError() << "No content-type in the contents list is supported..."; } } // Check if this is in fact an outgoing message (case where this is a message sent by us from an other device). Address me(linphone_core_get_identity(core->getCCore())); if (me.weakEqual(q->getFromAddress())) setDirection(ChatMessage::Direction::Outgoing); // Check if this is a duplicate message. if (chatRoom->findChatMessage(imdnId, direction)) return core->getCCore()->chat_deny_code; if (errorCode > 0) { reason = linphone_error_code_to_reason(errorCode); q->sendDeliveryNotification(reason); return reason; } if ((getContentType() == ContentType::ImIsComposing) || (getContentType() == ContentType::Imdn)) toBeStored = false; return reason; } void ChatMessagePrivate::send () { L_Q(); SalOp *op = salOp; LinphoneCall *lcall = nullptr; int errorCode = 0; currentSendStep |= ChatMessagePrivate::Step::Started; if (toBeStored && currentSendStep == (ChatMessagePrivate::Step::Started | ChatMessagePrivate::Step::None)) storeInDb(); if ((currentSendStep & ChatMessagePrivate::Step::FileUpload) == ChatMessagePrivate::Step::FileUpload) { lInfo() << "File upload step already done, skipping"; } else { ChatMessageModifier::Result result = fileTransferChatMessageModifier.encode(q->getSharedFromThis(), errorCode); if (result == ChatMessageModifier::Result::Error) { setState(ChatMessage::State::NotDelivered); return; } else if (result == ChatMessageModifier::Result::Suspended) { setState(ChatMessage::State::InProgress); return; } currentSendStep |= ChatMessagePrivate::Step::FileUpload; } shared_ptr core = q->getCore(); if (lp_config_get_int(core->getCCore()->config, "sip", "chat_use_call_dialogs", 0) != 0) { lcall = linphone_core_get_call_by_remote_address(core->getCCore(), q->getToAddress().asString().c_str()); if (lcall) { shared_ptr call = L_GET_CPP_PTR_FROM_C_OBJECT(lcall); if ((call->getState() == CallSession::State::Connected) || (call->getState() == CallSession::State::StreamsRunning) || (call->getState() == CallSession::State::Paused) || (call->getState() == CallSession::State::Pausing) || (call->getState() == CallSession::State::PausedByRemote) ) { lInfo() << "Send SIP msg through the existing call"; op = call->getPrivate()->getOp(); string identity = linphone_core_find_best_identity(core->getCCore(), linphone_call_get_remote_address(lcall)); if (identity.empty()) { LinphoneAddress *addr = linphone_address_new(q->getToAddress().asString().c_str()); LinphoneProxyConfig *proxy = linphone_core_lookup_known_proxy(core->getCCore(), addr); 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(core->getCCore()); } linphone_address_unref(addr); } } } } if (!op) { LinphoneAddress *peer = linphone_address_new(q->getToAddress().asString().c_str()); /* Sending out of call */ salOp = op = new SalMessageOp(core->getCCore()->sal); linphone_configure_op( core->getCCore(), op, peer, getSalCustomHeaders(), !!lp_config_get_int(core->getCCore()->config, "sip", "chat_msg_with_contact", 0) ); op->set_user_pointer(q); /* If out of call, directly store msg */ linphone_address_unref(peer); } op->set_from(q->getFromAddress().asString().c_str()); op->set_to(q->getToAddress().asString().c_str()); // --------------------------------------- // Start of message modification // --------------------------------------- if (applyModifiers) { // Do not multipart or encapsulate with CPIM in an old ChatRoom to maintain backward compatibility if (q->getChatRoom()->canHandleMultipart()) { if ((currentSendStep &ChatMessagePrivate::Step::Multipart) == ChatMessagePrivate::Step::Multipart) { lInfo() << "Multipart step already done, skipping"; } else { if (contents.size() > 1) { MultipartChatMessageModifier mcmm; mcmm.encode(q->getSharedFromThis(), errorCode); } currentSendStep |= ChatMessagePrivate::Step::Multipart; } } if (q->getChatRoom()->canHandleCpim()) { if ((currentSendStep &ChatMessagePrivate::Step::Cpim) == ChatMessagePrivate::Step::Cpim) { lInfo() << "Cpim step already done, skipping"; } else { CpimChatMessageModifier ccmm; ccmm.encode(q->getSharedFromThis(), errorCode); currentSendStep |= ChatMessagePrivate::Step::Cpim; } } if ((currentSendStep &ChatMessagePrivate::Step::Encryption) == ChatMessagePrivate::Step::Encryption) { lInfo() << "Encryption step already done, skipping"; } else { if (!encryptionPrevented) { EncryptionChatMessageModifier ecmm; ChatMessageModifier::Result result = ecmm.encode(q->getSharedFromThis(), errorCode); if (result == ChatMessageModifier::Result::Error) { sal_error_info_set((SalErrorInfo *)op->get_error_info(), SalReasonNotAcceptable, "SIP", errorCode, "Unable to encrypt IM", nullptr); setState(ChatMessage::State::NotDelivered); return; } else if (result == ChatMessageModifier::Result::Suspended) { currentSendStep |= ChatMessagePrivate::Step::Encryption; return; } } currentSendStep |= ChatMessagePrivate::Step::Encryption; } } // --------------------------------------- // End of message modification // --------------------------------------- if (internalContent.isEmpty()) { if (contents.size() > 0) { internalContent = *(contents.front()); } else if (externalBodyUrl.empty()) { // When using external body url, there is no content lError() << "Trying to send a message without any content !"; return; } } auto msgOp = dynamic_cast(op); if (!externalBodyUrl.empty()) { char *content_type = ms_strdup_printf("message/external-body; access-type=URL; URL=\"%s\"", externalBodyUrl.c_str()); msgOp->send_message(content_type, NULL); ms_free(content_type); } else if (internalContent.getContentType().isValid()) { msgOp->send_message(internalContent.getContentType().asString().c_str(), internalContent.getBodyAsUtf8String().c_str()); } else { msgOp->send_message(ContentType::PlainText.asString().c_str(), internalContent.getBodyAsUtf8String().c_str()); } // Restore FileContents and remove FileTransferContents list::iterator it = contents.begin(); while (it != contents.end()) { Content *content = *it; if (content->getContentType() == ContentType::FileTransfer) { FileTransferContent *fileTransferContent = (FileTransferContent *)content; it = contents.erase(it); addContent(*fileTransferContent->getFileContent()); delete fileTransferContent; } else { it++; } } // Remove internal content as it is not needed anymore and will confuse some old methods like getContentType() internalContent.setBody(""); internalContent.setContentType(ContentType("")); // Also remove current step so we go through all modifiers if message is re-sent currentSendStep = ChatMessagePrivate::Step::None; if (imdnId.empty()) setImdnMessageId(op->get_call_id()); /* must be known at that time */ if (lcall && linphone_call_get_op(lcall) == 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 */ return; } /* If operation failed, we should not change message state */ if (direction == ChatMessage::Direction::Outgoing) { setIsReadOnly(true); setState(ChatMessage::State::InProgress); } } void ChatMessagePrivate::storeInDb () { L_Q(); // TODO: store message in the future if (linphone_core_conference_server_enabled(q->getCore()->getCCore())) return; if (dbKey.isValid()) { updateInDb(); } else { shared_ptr eventLog = make_shared(time, q->getSharedFromThis()); // Avoid transaction in transaction if contents are not loaded. loadContentsFromDatabase(); q->getChatRoom()->getPrivate()->addEvent(eventLog); if (direction == ChatMessage::Direction::Incoming) { if (hasFileTransferContent()) { // Keep the event in the transient list, message storage can be updated in near future q->getChatRoom()->getPrivate()->addTransientEvent(eventLog); } } else { // Keep event in transient to be able to store in database state changes q->getChatRoom()->getPrivate()->addTransientEvent(eventLog); } } } void ChatMessagePrivate::updateInDb () { L_Q(); if (!dbKey.isValid()) return; unique_ptr &mainDb = q->getChatRoom()->getCore()->getPrivate()->mainDb; shared_ptr eventLog = mainDb->getEventFromKey(dbKey); // Avoid transaction in transaction if contents are not loaded. loadContentsFromDatabase(); mainDb->updateEvent(eventLog); if (direction == ChatMessage::Direction::Incoming) { if (!hasFileTransferContent()) { // Incoming message doesn't have any download waiting anymore, we can remove it's event from the transients q->getChatRoom()->getPrivate()->removeTransientEvent(eventLog); } } else { if (state == ChatMessage::State::Delivered || state == ChatMessage::State::NotDelivered) { // Once message has reached this state it won't change anymore so we can remove the event from the transients q->getChatRoom()->getPrivate()->removeTransientEvent(eventLog); } } } // ----------------------------------------------------------------------------- bool ChatMessagePrivate::validStateTransition (ChatMessage::State currentState, ChatMessage::State newState) { if (newState == currentState) return false; if ( (currentState == ChatMessage::State::Displayed || currentState == ChatMessage::State::DeliveredToUser) && ( newState == ChatMessage::State::DeliveredToUser || newState == ChatMessage::State::Delivered || newState == ChatMessage::State::NotDelivered ) ) return false; return true; } // ----------------------------------------------------------------------------- ChatMessage::ChatMessage (const shared_ptr &chatRoom, ChatMessage::Direction direction) : Object(*new ChatMessagePrivate(chatRoom,direction)), CoreAccessor(chatRoom->getCore()) { } ChatMessage::~ChatMessage () { L_D(); for (Content *content : d->contents) delete content; if (d->salOp) { d->salOp->set_user_pointer(nullptr); d->salOp->unref(); } if (d->salCustomHeaders) sal_custom_header_unref(d->salCustomHeaders); } shared_ptr ChatMessage::getChatRoom () const { L_D(); shared_ptr chatRoom = d->chatRoom.lock(); if (!chatRoom) { chatRoom = getCore()->getOrCreateBasicChatRoom(d->chatRoomId); if (!chatRoom) { lError() << "Unable to get valid chat room instance."; throw logic_error("Unable to get chat room of chat message."); } } return chatRoom; } // ----------------------------------------------------------------------------- time_t ChatMessage::getTime () const { L_D(); return d->time; } bool ChatMessage::isSecured () const { L_D(); return d->isSecured; } void ChatMessage::setIsSecured (bool isSecured) { L_D(); d->isSecured = isSecured; } ChatMessage::Direction ChatMessage::getDirection () const { L_D(); return d->direction; } ChatMessage::State ChatMessage::getState () const { L_D(); return d->state; } const string &ChatMessage::getImdnMessageId () const { L_D(); return d->imdnId; } void ChatMessagePrivate::setImdnMessageId (const string &id) { imdnId = id; } void ChatMessagePrivate::loadContentsFromDatabase () const { L_Q(); if (contentsNotLoadedFromDatabase) { isReadOnly = false; contentsNotLoadedFromDatabase = false; q->getChatRoom()->getCore()->getPrivate()->mainDb->loadChatMessageContents( const_pointer_cast(q->getSharedFromThis()) ); isReadOnly = true; } } bool ChatMessage::isRead () const { L_D(); LinphoneImNotifPolicy *policy = linphone_core_get_im_notif_policy(getCore()->getCCore()); if (linphone_im_notif_policy_get_recv_imdn_displayed(policy) && d->state == State::Displayed) return true; if ( linphone_im_notif_policy_get_recv_imdn_delivered(policy) && (d->state == State::DeliveredToUser || d->state == State::Displayed) ) return true; return d->state == State::Delivered || d->state == State::Displayed || d->state == State::DeliveredToUser; } const IdentityAddress &ChatMessage::getFromAddress () const { L_D(); return d->fromAddress; } const IdentityAddress &ChatMessage::getToAddress () const { L_D(); return d->toAddress; } bool ChatMessage::getToBeStored () const { L_D(); return d->toBeStored; } void ChatMessage::setToBeStored (bool value) { L_D(); d->toBeStored = value; } // ----------------------------------------------------------------------------- const LinphoneErrorInfo *ChatMessage::getErrorInfo () const { L_D(); if (!d->errorInfo) d->errorInfo = linphone_error_info_new(); // let's do it mutable linphone_error_info_from_sal_op(d->errorInfo, d->salOp); return d->errorInfo; } bool ChatMessage::isReadOnly () const { L_D(); return d->isReadOnly; } const list &ChatMessage::getContents () const { L_D(); return d->getContents(); } void ChatMessage::addContent (Content &content) { L_D(); if (!d->isReadOnly) d->addContent(content); } void ChatMessage::removeContent (const Content &content) { L_D(); if (!d->isReadOnly) d->removeContent(content); } const Content &ChatMessage::getInternalContent () const { L_D(); return d->internalContent; } void ChatMessage::setInternalContent (const Content &content) { L_D(); d->internalContent = content; } string ChatMessage::getCustomHeaderValue (const string &headerName) const { L_D(); try { return d->customHeaders.at(headerName); } catch (const exception &) { // Key doesn't exist. } return nullptr; } void ChatMessage::addCustomHeader (const string &headerName, const string &headerValue) { L_D(); if (d->isReadOnly) return; d->customHeaders[headerName] = headerValue; } void ChatMessage::removeCustomHeader (const string &headerName) { L_D(); if (d->isReadOnly) return; d->customHeaders.erase(headerName); } void ChatMessage::send () { L_D(); // Do not allow sending a message that is already being sent or that has been correctly delivered/displayed if ((d->state == State::InProgress) || (d->state == State::Delivered) || (d->state == State::FileTransferDone) || (d->state == State::DeliveredToUser) || (d->state == State::Displayed)) { lWarning() << "Cannot send chat message in state " << Utils::toString(d->state); return; } d->loadContentsFromDatabase(); getChatRoom()->getPrivate()->sendChatMessage(getSharedFromThis()); } void ChatMessage::sendDeliveryNotification (LinphoneReason reason) { L_D(); LinphoneImNotifPolicy *policy = linphone_core_get_im_notif_policy(getCore()->getCCore()); if (linphone_im_notif_policy_get_send_imdn_delivered(policy)) d->sendImdn(Imdn::Type::Delivery, reason); } void ChatMessage::sendDisplayNotification () { L_D(); LinphoneImNotifPolicy *policy = linphone_core_get_im_notif_policy(getCore()->getCCore()); if (linphone_im_notif_policy_get_send_imdn_displayed(policy)) d->sendImdn(Imdn::Type::Display, LinphoneReasonNone); } bool ChatMessage::downloadFile(FileTransferContent &fileTransferContent) { L_D(); return d->fileTransferChatMessageModifier.downloadFile(getSharedFromThis(), &fileTransferContent); } bool ChatMessage::isFileTransferInProgress() { L_D(); return d->fileTransferChatMessageModifier.isFileTransferInProgressAndValid(); } void ChatMessage::cancelFileTransfer () { L_D(); if (d->fileTransferChatMessageModifier.isFileTransferInProgressAndValid()) { if (d->state == State::InProgress) { d->setState(State::NotDelivered); } d->fileTransferChatMessageModifier.cancelFileTransfer(); } else { lInfo() << "No existing file transfer - nothing to cancel"; } } int ChatMessage::putCharacter (uint32_t character) { L_D(); constexpr uint32_t newLine = 0x2028; constexpr uint32_t crlf = 0x0D0A; constexpr uint32_t lf = 0x0A; shared_ptr chatRoom = getChatRoom(); if (!(chatRoom->getCapabilities() & LinphonePrivate::ChatRoom::Capabilities::RealTimeText)) return -1; shared_ptr rttcr = static_pointer_cast(chatRoom); if (!rttcr) return -1; shared_ptr call = rttcr->getCall(); if (!call || !call->getPrivate()->getMediaStream(LinphoneStreamTypeText)) return -1; if (character == newLine || character == crlf || character == lf) { shared_ptr core = getCore(); if (lp_config_get_int(core->getCCore()->config, "misc", "store_rtt_messages", 1) == 1) { // TODO: History. lDebug() << "New line sent, forge a message with content " << d->rttMessage.c_str(); d->setTime(ms_time(0)); d->state = State::Displayed; // d->direction = Direction::Outgoing; // setFromAddress(Address( // linphone_address_as_string(linphone_address_new(linphone_core_get_identity(core->getCCore()))) // )); // linphone_chat_message_store(L_GET_C_BACK_PTR(this)); d->rttMessage = ""; } } else { char *value = LinphonePrivate::Utils::utf8ToChar(character); d->rttMessage = d->rttMessage + string(value); lDebug() << "Sent RTT character: " << value << "(" << (unsigned long)character << "), pending text is " << d->rttMessage.c_str(); delete[] value; } text_stream_putchar32( reinterpret_cast(call->getPrivate()->getMediaStream(LinphoneStreamTypeText)), character ); return 0; } LINPHONE_END_NAMESPACE