linphone-iphone/src/chat/chat-message/chat-message.cpp
Jehan Monnier 2686dca63a Revert "Merge branch 'dev_content_cpp' into dev_refactor_cpp"
This reverts commit d7890dbe74, reversing
changes made to e6158d8da5.
2018-04-05 12:08:44 +02:00

1164 lines
36 KiB
C++

/*
* 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<AbstractChatRoom> &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> &mainDb = q->getChatRoom()->getCore()->getPrivate()->mainDb;
shared_ptr<EventLog> 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<ChatMessage::State> 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<shared_ptr<Participant>> ChatMessagePrivate::getParticipantsInState (const ChatMessage::State state) const {
L_Q();
list<shared_ptr<Participant>> participantsInState;
if (!(q->getChatRoom()->getCapabilities() & AbstractChatRoom::Capabilities::Conference) || !dbKey.isValid()) {
return participantsInState;
}
unique_ptr<MainDb> &mainDb = q->getChatRoom()->getCore()->getPrivate()->mainDb;
shared_ptr<EventLog> eventLog = mainDb->getEventFromKey(dbKey);
list<IdentityAddress> addressesInState = mainDb->getChatMessageParticipantsInState(eventLog, state);
const list<shared_ptr<Participant>> &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<Content>();
}
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<Content>();
}
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<string>();
}
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<string>();
}
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<FileTransferContent *>(content));
return false;
}
void ChatMessagePrivate::addContent (Content &content) {
getContents().push_back(&content);
}
void ChatMessagePrivate::removeContent (const Content &content) {
getContents().remove(&const_cast<Content &>(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<AbstractChatRoom> &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<ChatMessage> 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<ConferenceChatMessageEvent> event = make_shared<ConferenceChatMessageEvent>(
::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> core = q->getCore();
shared_ptr<AbstractChatRoom> 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> 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> 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<SalMessageOpInterface *>(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<Content*>::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> eventLog = make_shared<ConferenceChatMessageEvent>(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> &mainDb = q->getChatRoom()->getCore()->getPrivate()->mainDb;
shared_ptr<EventLog> 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<AbstractChatRoom> &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<AbstractChatRoom> ChatMessage::getChatRoom () const {
L_D();
shared_ptr<AbstractChatRoom> 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<ChatMessage>(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<Content *> &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<AbstractChatRoom> chatRoom = getChatRoom();
if (!(chatRoom->getCapabilities() & LinphonePrivate::ChatRoom::Capabilities::RealTimeText))
return -1;
shared_ptr<LinphonePrivate::RealTimeTextChatRoom> rttcr =
static_pointer_cast<LinphonePrivate::RealTimeTextChatRoom>(chatRoom);
if (!rttcr)
return -1;
shared_ptr<Call> call = rttcr->getCall();
if (!call || !call->getPrivate()->getMediaStream(LinphoneStreamTypeText))
return -1;
if (character == newLine || character == crlf || character == lf) {
shared_ptr<Core> 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<TextStream *>(call->getPrivate()->getMediaStream(LinphoneStreamTypeText)),
character
);
return 0;
}
LINPHONE_END_NAMESPACE