diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index afefefd42..432f30d12 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -30,6 +30,8 @@ set(LINPHONE_CXX_OBJECTS_PRIVATE_HEADER_FILES chat/chat-room.h content/content.h core/core.h + chat/imdn.h + chat/is-composing.h cpim/cpim.h cpim/header/cpim-core-headers.h cpim/header/cpim-generic-header.h @@ -73,6 +75,8 @@ set(LINPHONE_CXX_OBJECTS_SOURCE_FILES chat/chat-room.cpp content/content.cpp core/core.cpp + chat/imdn.cpp + chat/is-composing.cpp cpim/header/cpim-core-headers.cpp cpim/header/cpim-generic-header.cpp cpim/header/cpim-header.cpp diff --git a/src/chat/chat-room-p.h b/src/chat/chat-room-p.h index 9e6de5c25..4f7e492d9 100644 --- a/src/chat/chat-room-p.h +++ b/src/chat/chat-room-p.h @@ -23,21 +23,20 @@ #include "private.h" #include "chat-room.h" +#include "is-composing.h" +#include "is-composing-listener.h" #include "object/object-p.h" // ============================================================================= LINPHONE_BEGIN_NAMESPACE -class ChatRoomPrivate : public ObjectPrivate { +class ChatRoomPrivate : public ObjectPrivate, public IsComposingListener { public: + ChatRoomPrivate (LinphoneCore *core); virtual ~ChatRoomPrivate (); private: - static int refreshComposing (void *data, unsigned int revents); - static int remoteRefreshComposing (void *data, unsigned int revents); - static int stopComposing (void *data, unsigned int revents); - static int createChatMessageFromDb (void *data, int argc, char **argv, char **colName); static void onWeakMessageDestroyed (void *obj, belle_sip_object_t *messageBeingDestroyed); @@ -64,16 +63,7 @@ public: } private: - std::string createIsComposingXml () const; - void deleteComposingIdleTimer (); - void deleteComposingRefreshTimer (); - void deleteRemoteComposingRefreshTimer (); - int refreshComposing (unsigned int revents); - int remoteRefreshComposing (unsigned int revents); void sendIsComposingNotification (); - int stopComposing (unsigned int revents); - void processImdn (xmlparsing_context_t *xmlCtx); - void processIsComposingNotification (xmlparsing_context_t *xmlCtx); int createChatMessageFromDb (int argc, char **argv, char **colName); void onWeakMessageDestroyed (LinphoneChatMessage *messageBeingDestroyed); @@ -82,7 +72,6 @@ private: int sqlRequest (sqlite3 *db, const std::string &stmt); void sqlRequestMessage (sqlite3 *db, const std::string &stmt); std::list findMessages (const std::string &messageId); - LinphoneChatMessage *findMessageWithDirection (const std::string &messageId, LinphoneChatMessageDir direction); void storeOrUpdateMessage (LinphoneChatMessage *msg); public: @@ -94,13 +83,12 @@ private: void imdnReceived (const std::string &text); void isComposingReceived (const std::string &text); -public: - static const int composingDefaultIdleTimeout = 15; - static const int composingDefaultRefreshTimeout = 60; - static const int composingDefaultRemoteRefreshTimeout = 120; - static const std::string imdnPrefix; - static const std::string isComposingPrefix; +private: + void isComposingStateChanged (bool isComposing); + void isRemoteComposingStateChanged (bool isComposing); + void isComposingRefreshNeeded (); +public: LinphoneChatRoom *cBackPointer = nullptr; LinphoneCore *core = nullptr; LinphoneCall *call = nullptr; @@ -109,14 +97,12 @@ public: int unreadCount = -1; bool isComposing = false; bool remoteIsComposing = false; - belle_sip_source_t *remoteComposingRefreshTimer = nullptr; - belle_sip_source_t *composingIdleTimer = nullptr; - belle_sip_source_t *composingRefreshTimer = nullptr; std::list messages; std::list transientMessages; std::list weakMessages; std::list receivedRttCharacters; LinphoneChatMessage *pendingMessage = nullptr; + IsComposing isComposingHandler; private: L_DECLARE_PUBLIC(ChatRoom); diff --git a/src/chat/chat-room.cpp b/src/chat/chat-room.cpp index b6dcb41d2..1001a04bc 100644 --- a/src/chat/chat-room.cpp +++ b/src/chat/chat-room.cpp @@ -19,6 +19,9 @@ #include #include "chat-room-p.h" + +#include "chat-room.h" +#include "imdn.h" #include "logger/logger.h" #include "utils/content-type.h" #include "utils/utils.h" @@ -31,15 +34,12 @@ using namespace std; LINPHONE_BEGIN_NAMESPACE -const string ChatRoomPrivate::imdnPrefix = "/imdn:imdn"; -const string ChatRoomPrivate::isComposingPrefix = "/xsi:isComposing"; +// ============================================================================= -// ----------------------------------------------------------------------------- +ChatRoomPrivate::ChatRoomPrivate (LinphoneCore *core) + : core(core), isComposingHandler(core, this) {} ChatRoomPrivate::~ChatRoomPrivate () { - deleteComposingIdleTimer(); - deleteComposingRefreshTimer(); - deleteRemoteComposingRefreshTimer(); for (auto it = transientMessages.begin(); it != transientMessages.end(); it++) { linphone_chat_message_release(*it); } @@ -62,23 +62,6 @@ ChatRoomPrivate::~ChatRoomPrivate () { // ----------------------------------------------------------------------------- -int ChatRoomPrivate::refreshComposing (void *data, unsigned int revents) { - ChatRoomPrivate *d = reinterpret_cast(data); - return d->refreshComposing(revents); -} - -int ChatRoomPrivate::remoteRefreshComposing (void *data, unsigned int revents) { - ChatRoomPrivate *d = reinterpret_cast(data); - return d->remoteRefreshComposing(revents); -} - -int ChatRoomPrivate::stopComposing (void *data, unsigned int revents) { - ChatRoomPrivate *d = reinterpret_cast(data); - return d->stopComposing(revents); -} - -// ----------------------------------------------------------------------------- - int ChatRoomPrivate::createChatMessageFromDb (void *data, int argc, char **argv, char **colName) { ChatRoomPrivate *d = reinterpret_cast(data); return d->createChatMessageFromDb(argc, argv, colName); @@ -125,9 +108,7 @@ void ChatRoomPrivate::removeTransientMessage (LinphoneChatMessage *msg) { // ----------------------------------------------------------------------------- void ChatRoomPrivate::release () { - deleteComposingIdleTimer(); - deleteComposingRefreshTimer(); - deleteRemoteComposingRefreshTimer(); + isComposingHandler.stopTimers(); for (auto it = weakMessages.begin(); it != weakMessages.end(); it++) { linphone_chat_message_deactivate(*it); } @@ -214,97 +195,6 @@ int ChatRoomPrivate::getMessagesCount (bool unreadOnly) { // ----------------------------------------------------------------------------- -string ChatRoomPrivate::createIsComposingXml () const { - string content; - - xmlBufferPtr buf = xmlBufferCreate(); - if (!buf) { - lError() << "Error creating the XML buffer"; - return content; - } - xmlTextWriterPtr writer = xmlNewTextWriterMemory(buf, 0); - if (!writer) { - lError() << "Error creating the XML writer"; - return content; - } - - int err = xmlTextWriterStartDocument(writer, "1.0", "UTF-8", nullptr); - if (err >= 0) { - err = xmlTextWriterStartElementNS(writer, nullptr, (const xmlChar *)"isComposing", - (const xmlChar *)"urn:ietf:params:xml:ns:im-iscomposing"); - } - if (err >= 0) { - err = xmlTextWriterWriteAttributeNS(writer, (const xmlChar *)"xmlns", (const xmlChar *)"xsi", nullptr, - (const xmlChar *)"http://www.w3.org/2001/XMLSchema-instance"); - } - if (err >= 0) { - err = xmlTextWriterWriteAttributeNS(writer, (const xmlChar *)"xsi", (const xmlChar *)"schemaLocation", nullptr, - (const xmlChar *)"urn:ietf:params:xml:ns:im-composing iscomposing.xsd"); - } - if (err >= 0) { - err = xmlTextWriterWriteElement(writer, (const xmlChar *)"state", - isComposing ? (const xmlChar *)"active" : (const xmlChar *)"idle"); - } - if ((err >= 0) && isComposing) { - int refreshTimeout = lp_config_get_int(core->config, "sip", "composing_refresh_timeout", composingDefaultRefreshTimeout); - err = xmlTextWriterWriteElement(writer, (const xmlChar *)"refresh", (const xmlChar *)Utils::toString(refreshTimeout).c_str()); - } - if (err >= 0) { - /* Close the "isComposing" element. */ - err = xmlTextWriterEndElement(writer); - } - if (err >= 0) { - err = xmlTextWriterEndDocument(writer); - } - if (err > 0) { - /* xmlTextWriterEndDocument returns the size of the content. */ - content = (char *)buf->content; - } - xmlFreeTextWriter(writer); - xmlBufferFree(buf); - return content; -} - -void ChatRoomPrivate::deleteComposingIdleTimer () { - if (composingIdleTimer) { - if (core && core->sal) - sal_cancel_timer(core->sal, composingIdleTimer); - belle_sip_object_unref(composingIdleTimer); - composingIdleTimer = nullptr; - } -} - -void ChatRoomPrivate::deleteComposingRefreshTimer () { - if (composingRefreshTimer) { - if (core && core->sal) - sal_cancel_timer(core->sal, composingRefreshTimer); - belle_sip_object_unref(composingRefreshTimer); - composingRefreshTimer = nullptr; - } -} - -void ChatRoomPrivate::deleteRemoteComposingRefreshTimer () { - if (remoteComposingRefreshTimer) { - if (core && core->sal) - sal_cancel_timer(core->sal, remoteComposingRefreshTimer); - belle_sip_object_unref(remoteComposingRefreshTimer); - remoteComposingRefreshTimer = nullptr; - } -} - -int ChatRoomPrivate::refreshComposing (unsigned int revents) { - sendIsComposingNotification(); - return BELLE_SIP_CONTINUE; -} - -int ChatRoomPrivate::remoteRefreshComposing (unsigned int revents) { - belle_sip_object_unref(remoteComposingRefreshTimer); - remoteComposingRefreshTimer = nullptr; - remoteIsComposing = false; - linphone_core_notify_is_composing_received(core, cBackPointer); - return BELLE_SIP_STOP; -} - void ChatRoomPrivate::sendIsComposingNotification () { L_Q(ChatRoom); LinphoneImNotifPolicy *policy = linphone_core_get_im_notif_policy(core); @@ -320,7 +210,7 @@ void ChatRoomPrivate::sendIsComposingNotification () { /* Sending out of call */ SalOp *op = sal_op_new(core->sal); linphone_configure_op(core, op, peerAddress, nullptr, lp_config_get_int(core->config, "sip", "chat_msg_with_contact", 0)); - string content = createIsComposingXml(); + string content = isComposingHandler.marshal(isComposing); if (!content.empty()) { int retval = -1; LinphoneAddress *fromAddr = linphone_address_new(identity); @@ -349,127 +239,6 @@ void ChatRoomPrivate::sendIsComposingNotification () { } } -int ChatRoomPrivate::stopComposing (unsigned int revents) { - isComposing = false; - sendIsComposingNotification(); - deleteComposingRefreshTimer(); - belle_sip_object_unref(composingIdleTimer); - composingIdleTimer = nullptr; - return BELLE_SIP_STOP; -} - -void ChatRoomPrivate::processImdn (xmlparsing_context_t *xmlCtx) { - char xpathStr[MAX_XPATH_LENGTH]; - char *messageIdStr = nullptr; - char *datetimeStr = nullptr; - if (linphone_create_xml_xpath_context(xmlCtx) < 0) - return; - - xmlXPathRegisterNs(xmlCtx->xpath_ctx, (const xmlChar *)"imdn", (const xmlChar *)"urn:ietf:params:xml:ns:imdn"); - xmlXPathObjectPtr imdnObject = linphone_get_xml_xpath_object_for_node_list(xmlCtx, imdnPrefix.c_str()); - if (imdnObject) { - if (imdnObject->nodesetval && (imdnObject->nodesetval->nodeNr >= 1)) { - snprintf(xpathStr, sizeof(xpathStr), "%s[1]/imdn:message-id", imdnPrefix.c_str()); - messageIdStr = linphone_get_xml_text_content(xmlCtx, xpathStr); - snprintf(xpathStr, sizeof(xpathStr), "%s[1]/imdn:datetime", imdnPrefix.c_str()); - datetimeStr = linphone_get_xml_text_content(xmlCtx, xpathStr); - } - xmlXPathFreeObject(imdnObject); - } - - if (messageIdStr && datetimeStr) { - LinphoneChatMessage *cm = findMessageWithDirection(messageIdStr, LinphoneChatMessageOutgoing); - if (!cm) { - lWarning() << "Received IMDN for unknown message " << messageIdStr; - } else { - LinphoneImNotifPolicy *policy = linphone_core_get_im_notif_policy(core); - snprintf(xpathStr, sizeof(xpathStr), "%s[1]/imdn:delivery-notification/imdn:status", imdnPrefix.c_str()); - xmlXPathObjectPtr deliveryStatusObject = linphone_get_xml_xpath_object_for_node_list(xmlCtx, xpathStr); - snprintf(xpathStr, sizeof(xpathStr), "%s[1]/imdn:display-notification/imdn:status", imdnPrefix.c_str()); - xmlXPathObjectPtr displayStatusObject = linphone_get_xml_xpath_object_for_node_list(xmlCtx, xpathStr); - if (deliveryStatusObject && linphone_im_notif_policy_get_recv_imdn_delivered(policy)) { - if (deliveryStatusObject->nodesetval && (deliveryStatusObject->nodesetval->nodeNr >= 1)) { - xmlNodePtr node = deliveryStatusObject->nodesetval->nodeTab[0]; - if (node->children && node->children->name) { - if (strcmp((const char *)node->children->name, "delivered") == 0) { - linphone_chat_message_update_state(cm, LinphoneChatMessageStateDeliveredToUser); - } else if (strcmp((const char *)node->children->name, "error") == 0) { - linphone_chat_message_update_state(cm, LinphoneChatMessageStateNotDelivered); - } - } - } - xmlXPathFreeObject(deliveryStatusObject); - } - if (displayStatusObject && linphone_im_notif_policy_get_recv_imdn_displayed(policy)) { - if (displayStatusObject->nodesetval && (displayStatusObject->nodesetval->nodeNr >= 1)) { - xmlNodePtr node = displayStatusObject->nodesetval->nodeTab[0]; - if (node->children && node->children->name) { - if (strcmp((const char *)node->children->name, "displayed") == 0) { - linphone_chat_message_update_state(cm, LinphoneChatMessageStateDisplayed); - } - } - } - xmlXPathFreeObject(displayStatusObject); - } - linphone_chat_message_unref(cm); - } - } - if (messageIdStr) - linphone_free_xml_text_content(messageIdStr); - if (datetimeStr) - linphone_free_xml_text_content(datetimeStr); -} - -void ChatRoomPrivate::processIsComposingNotification (xmlparsing_context_t *xmlCtx) { - char xpathStr[MAX_XPATH_LENGTH]; - char *stateStr = nullptr; - char *refreshStr = nullptr; - int i; - bool state = false; - - if (linphone_create_xml_xpath_context(xmlCtx) < 0) - return; - - xmlXPathRegisterNs(xmlCtx->xpath_ctx, (const xmlChar *)"xsi", (const xmlChar *)"urn:ietf:params:xml:ns:im-iscomposing"); - xmlXPathObjectPtr isComposingObject = linphone_get_xml_xpath_object_for_node_list(xmlCtx, isComposingPrefix.c_str()); - if (isComposingObject) { - if (isComposingObject->nodesetval) { - for (i = 1; i <= isComposingObject->nodesetval->nodeNr; i++) { - snprintf(xpathStr, sizeof(xpathStr), "%s[%i]/xsi:state", isComposingPrefix.c_str(), i); - stateStr = linphone_get_xml_text_content(xmlCtx, xpathStr); - if (!stateStr) - continue; - snprintf(xpathStr, sizeof(xpathStr), "%s[%i]/xsi:refresh", isComposingPrefix.c_str(), i); - refreshStr = linphone_get_xml_text_content(xmlCtx, xpathStr); - } - } - xmlXPathFreeObject(isComposingObject); - } - - if (stateStr) { - if (strcmp(stateStr, "active") == 0) { - int refreshDuration = lp_config_get_int(core->config, "sip", "composing_remote_refresh_timeout", composingDefaultRemoteRefreshTimeout); - state = true; - if (refreshStr) - refreshDuration = atoi(refreshStr); - if (!remoteComposingRefreshTimer) { - remoteComposingRefreshTimer = sal_create_timer(core->sal, remoteRefreshComposing, this, - refreshDuration * 1000, "composing remote refresh timeout"); - } else { - belle_sip_source_set_timeout(remoteComposingRefreshTimer, refreshDuration * 1000); - } - } else { - deleteRemoteComposingRefreshTimer(); - } - - remoteIsComposing = state; - linphone_core_notify_is_composing_received(core, cBackPointer); - linphone_free_xml_text_content(stateStr); - } - if (refreshStr) - linphone_free_xml_text_content(refreshStr); -} - // ----------------------------------------------------------------------------- /** @@ -603,24 +372,6 @@ list ChatRoomPrivate::findMessages (const string &message return result; } -LinphoneChatMessage *ChatRoomPrivate::findMessageWithDirection (const string &messageId, LinphoneChatMessageDir direction) { - LinphoneChatMessage *ret = nullptr; - list l = findMessages(messageId); - for (auto it = l.begin(); it != l.end(); it++) { - LinphoneChatMessage *cm = *it; - if (cm->dir == direction) { - linphone_chat_message_ref(cm); - ret = cm; - break; - } - } - if (!l.empty()) { - for (auto it = l.begin(); it != l.end(); it++) - linphone_chat_message_unref(*it); - } - return ret; -} - /** * TODO: Should be handled directly by the LinphoneChatMessage object! */ @@ -644,7 +395,7 @@ LinphoneReason ChatRoomPrivate::messageReceived (SalOp *op, const SalMessage *sa LinphoneChatMessage *msg; /* Check if this is a duplicate message */ - if ((msg = findMessageWithDirection(sal_op_get_call_id(op), LinphoneChatMessageIncoming))) { + if ((msg = q->findMessageWithDirection(sal_op_get_call_id(op), LinphoneChatMessageIncoming))) { reason = core->chat_deny_code; if (msg) linphone_chat_message_unref(msg); @@ -815,32 +566,34 @@ void ChatRoomPrivate::chatMessageReceived (LinphoneChatMessage *msg) { } void ChatRoomPrivate::imdnReceived (const string &text) { - xmlparsing_context_t *xmlCtx = linphone_xmlparsing_context_new(); - xmlSetGenericErrorFunc(xmlCtx, linphone_xmlparsing_genericxml_error); - xmlCtx->doc = xmlReadDoc((const unsigned char *)text.c_str(), 0, nullptr, 0); - if (xmlCtx->doc) - processImdn(xmlCtx); - else - lWarning() << "Wrongly formatted IMDN XML: " << xmlCtx->errorBuffer; - linphone_xmlparsing_context_destroy(xmlCtx); + L_Q(ChatRoom); + Imdn::parse(*q, text); } void ChatRoomPrivate::isComposingReceived (const string &text) { - xmlparsing_context_t *xmlCtx = linphone_xmlparsing_context_new(); - xmlSetGenericErrorFunc(xmlCtx, linphone_xmlparsing_genericxml_error); - xmlCtx->doc = xmlReadDoc((const unsigned char *)text.c_str(), 0, nullptr, 0); - if (xmlCtx->doc) - processIsComposingNotification(xmlCtx); - else - lWarning() << "Wrongly formatted presence XML: " << xmlCtx->errorBuffer; - linphone_xmlparsing_context_destroy(xmlCtx); + isComposingHandler.parse(text); +} + +// ----------------------------------------------------------------------------- + +void ChatRoomPrivate::isComposingStateChanged (bool isComposing) { + this->isComposing = isComposing; + sendIsComposingNotification(); +} + +void ChatRoomPrivate::isRemoteComposingStateChanged (bool isComposing) { + remoteIsComposing = isComposing; + linphone_core_notify_is_composing_received(core, cBackPointer); +} + +void ChatRoomPrivate::isComposingRefreshNeeded () { + sendIsComposingNotification(); } // ============================================================================= -ChatRoom::ChatRoom (LinphoneCore *core, LinphoneAddress *peerAddress) : Object(*new ChatRoomPrivate) { +ChatRoom::ChatRoom (LinphoneCore *core, LinphoneAddress *peerAddress) : Object(*new ChatRoomPrivate(core)) { L_D(ChatRoom); - d->core = core; d->peerAddress = peerAddress; char *peerStr = linphone_address_as_string(d->peerAddress); d->peer = peerStr; @@ -851,23 +604,12 @@ ChatRoom::ChatRoom (LinphoneCore *core, LinphoneAddress *peerAddress) : Object(* void ChatRoom::compose () { L_D(ChatRoom); - int idleTimeout = lp_config_get_int(getCore()->config, "sip", "composing_idle_timeout", ChatRoomPrivate::composingDefaultIdleTimeout); - int refreshTimeout = lp_config_get_int(getCore()->config, "sip", "composing_refresh_timeout", ChatRoomPrivate::composingDefaultRefreshTimeout); if (!d->isComposing) { d->isComposing = true; d->sendIsComposingNotification(); - if (!d->composingRefreshTimer) { - d->composingRefreshTimer = sal_create_timer(getCore()->sal, ChatRoomPrivate::refreshComposing, d, - refreshTimeout * 1000, "composing refresh timeout"); - } else { - belle_sip_source_set_timeout(d->composingRefreshTimer, refreshTimeout * 1000); - } - if (!d->composingIdleTimer) { - d->composingIdleTimer = sal_create_timer(getCore()->sal, ChatRoomPrivate::stopComposing, d, - idleTimeout * 1000, "composing idle timeout"); - } + d->isComposingHandler.startRefreshTimer(); } - belle_sip_source_set_timeout(d->composingIdleTimer, idleTimeout * 1000); + d->isComposingHandler.startIdleTimer(); } LinphoneChatMessage *ChatRoom::createFileTransferMessage (const LinphoneContent *initialContent) { @@ -939,6 +681,25 @@ LinphoneChatMessage *ChatRoom::findMessage (const string &messageId) { return cm; } +LinphoneChatMessage * ChatRoom::findMessageWithDirection (const string &messageId, LinphoneChatMessageDir direction) { + L_D(ChatRoom); + LinphoneChatMessage *ret = nullptr; + list l = d->findMessages(messageId); + for (auto it = l.begin(); it != l.end(); it++) { + LinphoneChatMessage *cm = *it; + if (cm->dir == direction) { + linphone_chat_message_ref(cm); + ret = cm; + break; + } + } + if (!l.empty()) { + for (auto it = l.begin(); it != l.end(); it++) + linphone_chat_message_unref(*it); + } + return ret; +} + uint32_t ChatRoom::getChar () const { L_D(const ChatRoom); if (!d->receivedRttCharacters.empty()) { @@ -1188,8 +949,8 @@ void ChatRoom::sendMessage (LinphoneChatMessage *msg) { if (d->isComposing) d->isComposing = false; - d->deleteComposingIdleTimer(); - d->deleteComposingRefreshTimer(); + d->isComposingHandler.stopIdleTimer(); + d->isComposingHandler.stopRefreshTimer(); if (clearTextMessage) { ms_free(clearTextMessage); diff --git a/src/chat/chat-room.h b/src/chat/chat-room.h index 801896cf7..9584b3304 100644 --- a/src/chat/chat-room.h +++ b/src/chat/chat-room.h @@ -19,6 +19,9 @@ #ifndef _CHAT_ROOM_H_ #define _CHAT_ROOM_H_ +// From coreapi +#include "private.h" + #include #include "object/object.h" @@ -41,7 +44,8 @@ public: LinphoneChatMessage *createMessage (const std::string &msg); void deleteHistory (); void deleteMessage (LinphoneChatMessage *msg); - LinphoneChatMessage *findMessage (const std::string &messageId); + LinphoneChatMessage * findMessage (const std::string& messageId); + LinphoneChatMessage * findMessageWithDirection (const std::string &messageId, LinphoneChatMessageDir direction); uint32_t getChar () const; std::list getHistory (int nbMessages); int getHistorySize (); diff --git a/src/chat/imdn.cpp b/src/chat/imdn.cpp new file mode 100644 index 000000000..79b953cbe --- /dev/null +++ b/src/chat/imdn.cpp @@ -0,0 +1,104 @@ +/* + * imdn.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 "imdn.h" + +#include "logger/logger.h" + +using namespace std; + +LINPHONE_BEGIN_NAMESPACE + +// ============================================================================= + +const string Imdn::imdnPrefix = "/imdn:imdn"; + +void Imdn::parse (ChatRoom &cr, const string &text) { + xmlparsing_context_t *xmlCtx = linphone_xmlparsing_context_new(); + xmlSetGenericErrorFunc(xmlCtx, linphone_xmlparsing_genericxml_error); + xmlCtx->doc = xmlReadDoc((const unsigned char *)text.c_str(), 0, nullptr, 0); + if (xmlCtx->doc) + parse(cr, xmlCtx); + else + lWarning() << "Wrongly formatted IMDN XML: " << xmlCtx->errorBuffer; + linphone_xmlparsing_context_destroy(xmlCtx); +} + +void Imdn::parse (ChatRoom &cr, xmlparsing_context_t *xmlCtx) { + char xpathStr[MAX_XPATH_LENGTH]; + char *messageIdStr = nullptr; + char *datetimeStr = nullptr; + if (linphone_create_xml_xpath_context(xmlCtx) < 0) + return; + + xmlXPathRegisterNs(xmlCtx->xpath_ctx, (const xmlChar *)"imdn", (const xmlChar *)"urn:ietf:params:xml:ns:imdn"); + xmlXPathObjectPtr imdnObject = linphone_get_xml_xpath_object_for_node_list(xmlCtx, imdnPrefix.c_str()); + if (imdnObject) { + if (imdnObject->nodesetval && (imdnObject->nodesetval->nodeNr >= 1)) { + snprintf(xpathStr, sizeof(xpathStr), "%s[1]/imdn:message-id", imdnPrefix.c_str()); + messageIdStr = linphone_get_xml_text_content(xmlCtx, xpathStr); + snprintf(xpathStr, sizeof(xpathStr), "%s[1]/imdn:datetime", imdnPrefix.c_str()); + datetimeStr = linphone_get_xml_text_content(xmlCtx, xpathStr); + } + xmlXPathFreeObject(imdnObject); + } + + if (messageIdStr && datetimeStr) { + LinphoneChatMessage *cm = cr.findMessageWithDirection(messageIdStr, LinphoneChatMessageOutgoing); + if (!cm) { + lWarning() << "Received IMDN for unknown message " << messageIdStr; + } else { + LinphoneImNotifPolicy *policy = linphone_core_get_im_notif_policy(cr.getCore()); + snprintf(xpathStr, sizeof(xpathStr), "%s[1]/imdn:delivery-notification/imdn:status", imdnPrefix.c_str()); + xmlXPathObjectPtr deliveryStatusObject = linphone_get_xml_xpath_object_for_node_list(xmlCtx, xpathStr); + snprintf(xpathStr, sizeof(xpathStr), "%s[1]/imdn:display-notification/imdn:status", imdnPrefix.c_str()); + xmlXPathObjectPtr displayStatusObject = linphone_get_xml_xpath_object_for_node_list(xmlCtx, xpathStr); + if (deliveryStatusObject && linphone_im_notif_policy_get_recv_imdn_delivered(policy)) { + if (deliveryStatusObject->nodesetval && (deliveryStatusObject->nodesetval->nodeNr >= 1)) { + xmlNodePtr node = deliveryStatusObject->nodesetval->nodeTab[0]; + if (node->children && node->children->name) { + if (strcmp((const char *)node->children->name, "delivered") == 0) { + linphone_chat_message_update_state(cm, LinphoneChatMessageStateDeliveredToUser); + } else if (strcmp((const char *)node->children->name, "error") == 0) { + linphone_chat_message_update_state(cm, LinphoneChatMessageStateNotDelivered); + } + } + } + xmlXPathFreeObject(deliveryStatusObject); + } + if (displayStatusObject && linphone_im_notif_policy_get_recv_imdn_displayed(policy)) { + if (displayStatusObject->nodesetval && (displayStatusObject->nodesetval->nodeNr >= 1)) { + xmlNodePtr node = displayStatusObject->nodesetval->nodeTab[0]; + if (node->children && node->children->name) { + if (strcmp((const char *)node->children->name, "displayed") == 0) { + linphone_chat_message_update_state(cm, LinphoneChatMessageStateDisplayed); + } + } + } + xmlXPathFreeObject(displayStatusObject); + } + linphone_chat_message_unref(cm); + } + } + if (messageIdStr) + linphone_free_xml_text_content(messageIdStr); + if (datetimeStr) + linphone_free_xml_text_content(datetimeStr); +} + +LINPHONE_END_NAMESPACE diff --git a/src/chat/imdn.h b/src/chat/imdn.h new file mode 100644 index 000000000..d23ba0245 --- /dev/null +++ b/src/chat/imdn.h @@ -0,0 +1,46 @@ +/* + * imdn.h + * 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 . + */ + +#ifndef _IMDN_H_ +#define _IMDN_H_ + +#include + +#include "chat-room.h" +#include "utils/general.h" + +#include "private.h" + +// ============================================================================= + +LINPHONE_BEGIN_NAMESPACE + +class Imdn { +public: + static void parse (ChatRoom &cr, const std::string &content); + +private: + static void parse (ChatRoom &cr, xmlparsing_context_t *xmlCtx); + +private: + static const std::string imdnPrefix; +}; + +LINPHONE_END_NAMESPACE + +#endif // ifndef _IMDN_H_ diff --git a/src/chat/is-composing-listener.h b/src/chat/is-composing-listener.h new file mode 100644 index 000000000..dbb612825 --- /dev/null +++ b/src/chat/is-composing-listener.h @@ -0,0 +1,37 @@ +/* + * is-composing-listener.h + * 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 . + */ + +#ifndef _IS_COMPOSING_LISTENER_H_ +#define _IS_COMPOSING_LISTENER_H_ + +#include "utils/general.h" + +// ============================================================================= + +LINPHONE_BEGIN_NAMESPACE + +class IsComposingListener { +public: + virtual void isComposingStateChanged (bool isComposing) = 0; + virtual void isRemoteComposingStateChanged (bool isComposing) = 0; + virtual void isComposingRefreshNeeded () = 0; +}; + +LINPHONE_END_NAMESPACE + +#endif // ifndef _IS_COMPOSING_LISTENER_H_ diff --git a/src/chat/is-composing.cpp b/src/chat/is-composing.cpp new file mode 100644 index 000000000..02515652e --- /dev/null +++ b/src/chat/is-composing.cpp @@ -0,0 +1,267 @@ +/* + * is-composing.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 "is-composing.h" + +#include "chat-room-p.h" + +#include "logger/logger.h" + +using namespace std; + +LINPHONE_BEGIN_NAMESPACE + +// ============================================================================= + +const string IsComposing::isComposingPrefix = "/xsi:isComposing"; + +// ----------------------------------------------------------------------------- + +IsComposing::IsComposing (LinphoneCore *core, IsComposingListener *listener) + : core(core), listener(listener) {} + +IsComposing::~IsComposing () { + stopTimers(); +} + +// ----------------------------------------------------------------------------- + +std::string IsComposing::marshal (bool isComposing) { + string content; + + xmlBufferPtr buf = xmlBufferCreate(); + if (!buf) { + lError() << "Error creating the XML buffer"; + return content; + } + xmlTextWriterPtr writer = xmlNewTextWriterMemory(buf, 0); + if (!writer) { + lError() << "Error creating the XML writer"; + return content; + } + + int err = xmlTextWriterStartDocument(writer, "1.0", "UTF-8", nullptr); + if (err >= 0) { + err = xmlTextWriterStartElementNS(writer, nullptr, (const xmlChar *)"isComposing", + (const xmlChar *)"urn:ietf:params:xml:ns:im-iscomposing"); + } + if (err >= 0) { + err = xmlTextWriterWriteAttributeNS(writer, (const xmlChar *)"xmlns", (const xmlChar *)"xsi", nullptr, + (const xmlChar *)"http://www.w3.org/2001/XMLSchema-instance"); + } + if (err >= 0) { + err = xmlTextWriterWriteAttributeNS(writer, (const xmlChar *)"xsi", (const xmlChar *)"schemaLocation", nullptr, + (const xmlChar *)"urn:ietf:params:xml:ns:im-composing iscomposing.xsd"); + } + if (err >= 0) { + err = xmlTextWriterWriteElement(writer, (const xmlChar *)"state", + isComposing ? (const xmlChar *)"active" : (const xmlChar *)"idle"); + } + if ((err >= 0) && isComposing) { + int refreshTimeout = lp_config_get_int(core->config, "sip", "composing_refresh_timeout", defaultRefreshTimeout); + err = xmlTextWriterWriteElement(writer, (const xmlChar *)"refresh", (const xmlChar *)to_string(refreshTimeout).c_str()); + } + if (err >= 0) { + /* Close the "isComposing" element. */ + err = xmlTextWriterEndElement(writer); + } + if (err >= 0) { + err = xmlTextWriterEndDocument(writer); + } + if (err > 0) { + /* xmlTextWriterEndDocument returns the size of the content. */ + content = (char *)buf->content; + } + xmlFreeTextWriter(writer); + xmlBufferFree(buf); + return content; +} + +void IsComposing::parse (const string &text) { + xmlparsing_context_t *xmlCtx = linphone_xmlparsing_context_new(); + xmlSetGenericErrorFunc(xmlCtx, linphone_xmlparsing_genericxml_error); + xmlCtx->doc = xmlReadDoc((const unsigned char *)text.c_str(), 0, nullptr, 0); + if (xmlCtx->doc) + parse(xmlCtx); + else + lWarning() << "Wrongly formatted presence XML: " << xmlCtx->errorBuffer; + linphone_xmlparsing_context_destroy(xmlCtx); +} + +void IsComposing::startIdleTimer () { + int duration = getIdleTimerDuration(); + if (!idleTimer) { + idleTimer = sal_create_timer(core->sal, idleTimerExpired, this, + duration * 1000, "composing idle timeout"); + } else { + belle_sip_source_set_timeout(idleTimer, duration * 1000); + } +} + +void IsComposing::startRefreshTimer () { + int duration = getRefreshTimerDuration(); + if (!refreshTimer) { + refreshTimer = sal_create_timer(core->sal, refreshTimerExpired, this, + duration * 1000, "composing refresh timeout"); + } else { + belle_sip_source_set_timeout(refreshTimer, duration * 1000); + } +} + +void IsComposing::startRemoteRefreshTimer () { + int duration = getRemoteRefreshTimerDuration(); + if (!remoteRefreshTimer) { + remoteRefreshTimer = sal_create_timer(core->sal, remoteRefreshTimerExpired, this, + duration * 1000, "composing remote refresh timeout"); + } else { + belle_sip_source_set_timeout(remoteRefreshTimer, duration * 1000); + } +} + +#if 0 +void IsComposing::idleTimerExpired () { + stopRefreshTimer(); + stopIdleTimer(); +} +#endif + +void IsComposing::stopTimers () { + stopIdleTimer(); + stopRefreshTimer(); + stopRemoteRefreshTimer(); +} + +// ----------------------------------------------------------------------------- + +void IsComposing::stopIdleTimer () { + if (idleTimer) { + if (core && core->sal) + sal_cancel_timer(core->sal, idleTimer); + belle_sip_object_unref(idleTimer); + idleTimer = nullptr; + } +} + +void IsComposing::stopRefreshTimer () { + if (refreshTimer) { + if (core && core->sal) + sal_cancel_timer(core->sal, refreshTimer); + belle_sip_object_unref(refreshTimer); + refreshTimer = nullptr; + } +} + +void IsComposing::stopRemoteRefreshTimer () { + if (remoteRefreshTimer) { + if (core && core->sal) + sal_cancel_timer(core->sal, remoteRefreshTimer); + belle_sip_object_unref(remoteRefreshTimer); + remoteRefreshTimer = nullptr; + } +} + +// ----------------------------------------------------------------------------- + +int IsComposing::getIdleTimerDuration () { + return lp_config_get_int(core->config, "sip", "composing_idle_timeout", defaultIdleTimeout); +} + +int IsComposing::getRefreshTimerDuration () { + return lp_config_get_int(core->config, "sip", "composing_refresh_timeout", defaultRefreshTimeout); +} + +int IsComposing::getRemoteRefreshTimerDuration () { + return lp_config_get_int(core->config, "sip", "composing_remote_refresh_timeout", defaultRemoteRefreshTimeout); +} + +void IsComposing::parse (xmlparsing_context_t *xmlCtx) { + char xpathStr[MAX_XPATH_LENGTH]; + char *stateStr = nullptr; + char *refreshStr = nullptr; + int i; + bool state = false; + + if (linphone_create_xml_xpath_context(xmlCtx) < 0) + return; + + xmlXPathRegisterNs(xmlCtx->xpath_ctx, (const xmlChar *)"xsi", (const xmlChar *)"urn:ietf:params:xml:ns:im-iscomposing"); + xmlXPathObjectPtr isComposingObject = linphone_get_xml_xpath_object_for_node_list(xmlCtx, isComposingPrefix.c_str()); + if (isComposingObject) { + if (isComposingObject->nodesetval) { + for (i = 1; i <= isComposingObject->nodesetval->nodeNr; i++) { + snprintf(xpathStr, sizeof(xpathStr), "%s[%i]/xsi:state", isComposingPrefix.c_str(), i); + stateStr = linphone_get_xml_text_content(xmlCtx, xpathStr); + if (!stateStr) + continue; + snprintf(xpathStr, sizeof(xpathStr), "%s[%i]/xsi:refresh", isComposingPrefix.c_str(), i); + refreshStr = linphone_get_xml_text_content(xmlCtx, xpathStr); + } + } + xmlXPathFreeObject(isComposingObject); + } + + if (stateStr) { + if (strcmp(stateStr, "active") == 0) { + int refreshDuration = getRefreshTimerDuration(); + state = true; + if (refreshStr) + refreshDuration = atoi(refreshStr); + startRemoteRefreshTimer(); + } else { + stopRemoteRefreshTimer(); + } + + listener->isRemoteComposingStateChanged(state); + linphone_free_xml_text_content(stateStr); + } + if (refreshStr) + linphone_free_xml_text_content(refreshStr); +} + +int IsComposing::idleTimerExpired (unsigned int revents) { + listener->isComposingStateChanged(false); + return BELLE_SIP_STOP; +} + +int IsComposing::refreshTimerExpired (unsigned int revents) { + listener->isComposingRefreshNeeded(); + return BELLE_SIP_CONTINUE; +} + +int IsComposing::remoteRefreshTimerExpired (unsigned int revents) { + stopRemoteRefreshTimer(); + listener->isRemoteComposingStateChanged(false); + return BELLE_SIP_STOP; +} + +int IsComposing::idleTimerExpired (void *data, unsigned int revents) { + IsComposing *d = reinterpret_cast(data); + return d->idleTimerExpired(revents); +} + +int IsComposing::refreshTimerExpired (void *data, unsigned int revents) { + IsComposing *d = reinterpret_cast(data); + return d->refreshTimerExpired(revents); +} + +int IsComposing::remoteRefreshTimerExpired (void *data, unsigned int revents) { + IsComposing *d = reinterpret_cast(data); + return d->remoteRefreshTimerExpired(revents); +} + +LINPHONE_END_NAMESPACE diff --git a/src/chat/is-composing.h b/src/chat/is-composing.h new file mode 100644 index 000000000..4315d230f --- /dev/null +++ b/src/chat/is-composing.h @@ -0,0 +1,77 @@ +/* + * is-composing.h + * 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 . + */ + +#ifndef _IS_COMPOSING_H_ +#define _IS_COMPOSING_H_ + +#include + +#include "is-composing-listener.h" +#include "utils/general.h" + +#include "private.h" + +// ============================================================================= + +LINPHONE_BEGIN_NAMESPACE + +class IsComposing { +public: + IsComposing (LinphoneCore *core, IsComposingListener *listener); + ~IsComposing (); + + std::string marshal (bool isComposing); + void parse (const std::string &content); + void startIdleTimer (); + void startRefreshTimer (); + void startRemoteRefreshTimer (); + void stopComposing (); + void stopIdleTimer (); + void stopRefreshTimer (); + void stopRemoteRefreshTimer (); + void stopTimers (); + +private: + int getIdleTimerDuration (); + int getRefreshTimerDuration (); + int getRemoteRefreshTimerDuration (); + void parse (xmlparsing_context_t *xmlCtx); + int idleTimerExpired (unsigned int revents); + int refreshTimerExpired (unsigned int revents); + int remoteRefreshTimerExpired (unsigned int revents); + + static int idleTimerExpired (void *data, unsigned int revents); + static int refreshTimerExpired (void *data, unsigned int revents); + static int remoteRefreshTimerExpired (void *data, unsigned int revents); + +private: + static const int defaultIdleTimeout = 15; + static const int defaultRefreshTimeout = 60; + static const int defaultRemoteRefreshTimeout = 120; + static const std::string isComposingPrefix; + + LinphoneCore *core = nullptr; + IsComposingListener *listener = nullptr; + belle_sip_source_t *remoteRefreshTimer = nullptr; + belle_sip_source_t *idleTimer = nullptr; + belle_sip_source_t *refreshTimer = nullptr; +}; + +LINPHONE_END_NAMESPACE + +#endif // ifndef _IS_COMPOSING_H_