diff options
Diffstat (limited to 'Swift/Controllers/Chat')
-rw-r--r-- | Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h | 53 | ||||
-rw-r--r-- | Swift/Controllers/Chat/ChatController.cpp | 224 | ||||
-rw-r--r-- | Swift/Controllers/Chat/ChatController.h | 39 | ||||
-rw-r--r-- | Swift/Controllers/Chat/ChatControllerBase.cpp | 164 | ||||
-rw-r--r-- | Swift/Controllers/Chat/ChatControllerBase.h | 76 | ||||
-rw-r--r-- | Swift/Controllers/Chat/ChatMessageParser.cpp | 195 | ||||
-rw-r--r-- | Swift/Controllers/Chat/ChatMessageParser.h | 26 | ||||
-rw-r--r-- | Swift/Controllers/Chat/ChatsManager.cpp | 499 | ||||
-rw-r--r-- | Swift/Controllers/Chat/ChatsManager.h | 72 | ||||
-rw-r--r-- | Swift/Controllers/Chat/MUCController.cpp | 475 | ||||
-rw-r--r-- | Swift/Controllers/Chat/MUCController.h | 56 | ||||
-rw-r--r-- | Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp | 267 | ||||
-rw-r--r-- | Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp | 115 | ||||
-rw-r--r-- | Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp | 165 | ||||
-rw-r--r-- | Swift/Controllers/Chat/UnitTest/MockChatListWindow.h | 9 | ||||
-rw-r--r-- | Swift/Controllers/Chat/UserSearchController.cpp | 170 | ||||
-rw-r--r-- | Swift/Controllers/Chat/UserSearchController.h | 33 |
17 files changed, 2287 insertions, 351 deletions
diff --git a/Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h b/Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h new file mode 100644 index 0000000..2265c3b --- /dev/null +++ b/Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2013 Tobias Markmann + * Licensed under the simplified BSD license. + * See Documentation/Licenses/BSD-simplified.txt for more information. + */ + +/* + * Copyright (c) 2014 Kevin Smith and Remko Tronçon + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include <Swiften/Roster/XMPPRoster.h> +#include <Swiften/Elements/MUCInvitationPayload.h> +#include <Swift/Controllers/Settings/SettingsProvider.h> +#include <Swift/Controllers/SettingConstants.h> + +namespace Swift { + class AutoAcceptMUCInviteDecider { + public: + AutoAcceptMUCInviteDecider(const JID& domain, XMPPRoster* roster, SettingsProvider* settings) : domain_(domain), roster_(roster), settings_(settings) { + } + + bool isAutoAcceptedInvite(const JID& from, MUCInvitationPayload::ref invite) { + if (!invite->getIsImpromptu()) { + return false; /* always ask the user for normal MUC invites */ + } + + if (invite->getIsContinuation()) { + return true; + } + + std::string auto_accept_mode = settings_->getSetting(SettingConstants::INVITE_AUTO_ACCEPT_MODE); + if (auto_accept_mode == "no") { + return false; + } else if (auto_accept_mode == "presence") { + return roster_->getSubscriptionStateForJID(from) == RosterItemPayload::From || roster_->getSubscriptionStateForJID(from) == RosterItemPayload::Both; + } else if (auto_accept_mode == "domain") { + return roster_->getSubscriptionStateForJID(from) == RosterItemPayload::From || roster_->getSubscriptionStateForJID(from) == RosterItemPayload::Both || from.getDomain() == domain_; + } else { + assert(false); + return false; + } + } + + private: + JID domain_; + XMPPRoster* roster_; + SettingsProvider* settings_; + }; +} diff --git a/Swift/Controllers/Chat/ChatController.cpp b/Swift/Controllers/Chat/ChatController.cpp index 5a18a98..0b34681 100644 --- a/Swift/Controllers/Chat/ChatController.cpp +++ b/Swift/Controllers/Chat/ChatController.cpp @@ -1,9 +1,9 @@ /* - * Copyright (c) 2010-2012 Kevin Smith + * Copyright (c) 2010-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ -#include "Swift/Controllers/Chat/ChatController.h" +#include <Swift/Controllers/Chat/ChatController.h> #include <boost/bind.hpp> @@ -11,25 +11,37 @@ #include <stdio.h> -#include <Swift/Controllers/Intl.h> -#include <Swiften/Base/format.h> -#include <Swiften/Base/Algorithm.h> #include <Swiften/Avatars/AvatarManager.h> +#include <Swiften/Base/Algorithm.h> +#include <Swiften/Base/DateTime.h> +#include <Swiften/Base/foreach.h> +#include <Swiften/Base/format.h> +#include <Swiften/Base/Log.h> #include <Swiften/Chat/ChatStateNotifier.h> #include <Swiften/Chat/ChatStateTracker.h> +#include <Swiften/Client/ClientBlockListManager.h> +#include <Swiften/Client/NickResolver.h> #include <Swiften/Client/StanzaChannel.h> +#include <Swiften/Disco/EntityCapsProvider.h> +#include <Swiften/Elements/DeliveryReceipt.h> +#include <Swiften/Elements/DeliveryReceiptRequest.h> +#include <Swiften/Elements/Idle.h> +#include <Swiften/FileTransfer/FileTransferManager.h> + +#include <Swift/Controllers/Intl.h> #include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> -#include <Swiften/Client/NickResolver.h> #include <Swift/Controllers/XMPPEvents/EventController.h> #include <Swift/Controllers/FileTransfer/FileTransferController.h> #include <Swift/Controllers/StatusUtil.h> -#include <Swiften/Disco/EntityCapsProvider.h> -#include <Swiften/Base/foreach.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/UIEvents/SendFileUIEvent.h> -#include <Swiften/Elements/DeliveryReceipt.h> -#include <Swiften/Elements/DeliveryReceiptRequest.h> +#include <Swift/Controllers/UIEvents/AcceptWhiteboardSessionUIEvent.h> +#include <Swift/Controllers/UIEvents/CancelWhiteboardSessionUIEvent.h> +#include <Swift/Controllers/UIEvents/ShowWhiteboardUIEvent.h> +#include <Swift/Controllers/UIEvents/RequestChangeBlockStateUIEvent.h> +#include <Swift/Controllers/UIEvents/InviteToMUCUIEvent.h> +#include <Swift/Controllers/UIEvents/RequestInviteToMUCUIEvent.h> #include <Swift/Controllers/SettingConstants.h> - -#include <Swiften/Base/Log.h> +#include <Swift/Controllers/Highlighter.h> +#include <Swift/Controllers/Chat/ChatMessageParser.h> namespace Swift { @@ -38,6 +50,6 @@ namespace Swift { * The controller does not gain ownership of the stanzaChannel, nor the factory. */ -ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, SettingsProvider* settings) - : ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, contact, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController, timerFactory, entityCapsProvider), eventStream_(eventStream), userWantsReceipts_(userWantsReceipts), settings_(settings) { +ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, SettingsProvider* settings, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, boost::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider) + : ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, contact, presenceOracle, avatarManager, useDelayForLatency, eventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser, autoAcceptMUCInviteDecider), eventStream_(eventStream), userWantsReceipts_(userWantsReceipts), settings_(settings), clientBlockListManager_(clientBlockListManager) { isInMUC_ = isInMUC; lastWasPresence_ = false; @@ -60,4 +72,8 @@ ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQ theirPresence = contact.isBare() ? presenceOracle->getHighestPriorityPresence(contact.toBare()) : presenceOracle->getLastPresence(contact); } + Idle::ref idle; + if (theirPresence && (idle = theirPresence->getPayload<Idle>())) { + startMessage += str(format(QT_TRANSLATE_NOOP("", ", who has been idle since %1%")) % dateTimeToLocalString(idle->getSince())); + } startMessage += ": " + statusShowTypeToFriendlyName(theirPresence ? theirPresence->getShow() : StatusShow::None); if (theirPresence && !theirPresence->getStatus().empty()) { @@ -67,5 +83,5 @@ ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQ chatStateNotifier_->setContactIsOnline(theirPresence && theirPresence->getType() == Presence::Available); startMessage += "."; - chatWindow_->addSystemMessage(startMessage); + chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(startMessage), ChatWindow::DefaultDirection); chatWindow_->onUserTyping.connect(boost::bind(&ChatStateNotifier::setUserIsTyping, chatStateNotifier_)); chatWindow_->onUserCancelsTyping.connect(boost::bind(&ChatStateNotifier::userCancelledNewMessage, chatStateNotifier_)); @@ -74,7 +90,15 @@ ChatController::ChatController(const JID& self, StanzaChannel* stanzaChannel, IQ chatWindow_->onFileTransferCancel.connect(boost::bind(&ChatController::handleFileTransferCancel, this, _1)); chatWindow_->onSendFileRequest.connect(boost::bind(&ChatController::handleSendFileRequest, this, _1)); + chatWindow_->onWhiteboardSessionAccept.connect(boost::bind(&ChatController::handleWhiteboardSessionAccept, this)); + chatWindow_->onWhiteboardSessionCancel.connect(boost::bind(&ChatController::handleWhiteboardSessionCancel, this)); + chatWindow_->onWhiteboardWindowShow.connect(boost::bind(&ChatController::handleWhiteboardWindowShow, this)); + chatWindow_->onBlockUserRequest.connect(boost::bind(&ChatController::handleBlockUserRequest, this)); + chatWindow_->onUnblockUserRequest.connect(boost::bind(&ChatController::handleUnblockUserRequest, this)); + chatWindow_->onInviteToChat.connect(boost::bind(&ChatController::handleInviteToChat, this, _1)); + chatWindow_->onClosed.connect(boost::bind(&ChatController::handleWindowClosed, this)); handleBareJIDCapsChanged(toJID_); settings_->onSettingChanged.connect(boost::bind(&ChatController::handleSettingChanged, this, _1)); + eventStream_->onUIEvent.connect(boost::bind(&ChatController::handleUIEvent, this, _1)); } @@ -86,4 +110,5 @@ void ChatController::handleContactNickChanged(const JID& jid, const std::string& ChatController::~ChatController() { + eventStream_->onUIEvent.disconnect(boost::bind(&ChatController::handleUIEvent, this, _1)); settings_->onSettingChanged.disconnect(boost::bind(&ChatController::handleSettingChanged, this, _1)); nickResolver_->onNickChanged.disconnect(boost::bind(&ChatController::handleContactNickChanged, this, _1, _2)); @@ -92,4 +117,8 @@ ChatController::~ChatController() { } +JID ChatController::getBaseJID() { + return isInMUC_ ? toJID_ : ChatControllerBase::getBaseJID(); +} + void ChatController::cancelReplaces() { lastWasPresence_ = false; @@ -109,4 +138,9 @@ void ChatController::handleBareJIDCapsChanged(const JID& /*jid*/) { contactSupportsReceipts_ = ChatWindow::No; } + if (FileTransferManager::isSupportedBy(disco)) { + chatWindow_->setFileTransferEnabled(ChatWindow::Yes); + } else { + chatWindow_->setFileTransferEnabled(ChatWindow::No); + } } else { SWIFT_LOG(debug) << "No disco info :(" << std::endl; @@ -130,4 +164,17 @@ void ChatController::setToJID(const JID& jid) { } +void ChatController::setAvailableServerFeatures(boost::shared_ptr<DiscoInfo> info) { + ChatControllerBase::setAvailableServerFeatures(info); + if (iqRouter_->isAvailable() && info->hasFeature(DiscoInfo::BlockingCommandFeature)) { + boost::shared_ptr<BlockList> blockList = clientBlockListManager_->getBlockList(); + + blockingOnStateChangedConnection_ = blockList->onStateChanged.connect(boost::bind(&ChatController::handleBlockingStateChanged, this)); + blockingOnItemAddedConnection_ = blockList->onItemAdded.connect(boost::bind(&ChatController::handleBlockingStateChanged, this)); + blockingOnItemRemovedConnection_ = blockList->onItemRemoved.connect(boost::bind(&ChatController::handleBlockingStateChanged, this)); + + handleBlockingStateChanged(); + } +} + bool ChatController::isIncomingMessageFromMe(boost::shared_ptr<Message>) { return false; @@ -147,6 +194,8 @@ void ChatController::preHandleIncomingMessage(boost::shared_ptr<MessageEvent> me } chatStateTracker_->handleMessageReceived(message); - chatStateNotifier_->receivedMessageFromContact(message->getPayload<ChatState>()); + chatStateNotifier_->receivedMessageFromContact(!!message->getPayload<ChatState>()); + // handle XEP-0184 Message Receipts + // incomming receipts if (boost::shared_ptr<DeliveryReceipt> receipt = message->getPayload<DeliveryReceipt>()) { SWIFT_LOG(debug) << "received receipt for id: " << receipt->getReceivedID() << std::endl; @@ -155,4 +204,11 @@ void ChatController::preHandleIncomingMessage(boost::shared_ptr<MessageEvent> me requestedReceipts_.erase(receipt->getReceivedID()); } + // incomming errors in response to send out receipts + } else if (message->getPayload<DeliveryReceiptRequest>() && (message->getType() == Message::Error)) { + if (requestedReceipts_.find(message->getID()) != requestedReceipts_.end()) { + chatWindow_->setMessageReceiptState(requestedReceipts_[message->getID()], ChatWindow::ReceiptFailed); + requestedReceipts_.erase(message->getID()); + } + // incoming receipt requests } else if (message->getPayload<DeliveryReceiptRequest>()) { if (receivingPresenceFromUs_) { @@ -165,6 +221,9 @@ void ChatController::preHandleIncomingMessage(boost::shared_ptr<MessageEvent> me } -void ChatController::postHandleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent) { +void ChatController::postHandleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent, const HighlightAction& highlight) { eventController_->handleIncomingEvent(messageEvent); + if (!messageEvent->getConcluded()) { + highlighter_->handleHighlightAction(highlight); + } } @@ -189,20 +248,90 @@ void ChatController::handleSettingChanged(const std::string& settingPath) { void ChatController::checkForDisplayingDisplayReceiptsAlert() { + boost::optional<ChatWindow::AlertID> newDeliverReceiptAlert; if (userWantsReceipts_ && (contactSupportsReceipts_ == ChatWindow::No)) { - chatWindow_->setAlert(QT_TRANSLATE_NOOP("", "This chat doesn't support delivery receipts.")); + newDeliverReceiptAlert = chatWindow_->addAlert(QT_TRANSLATE_NOOP("", "This chat doesn't support delivery receipts.")); } else if (userWantsReceipts_ && (contactSupportsReceipts_ == ChatWindow::Maybe)) { - chatWindow_->setAlert(QT_TRANSLATE_NOOP("", "This chat may not support delivery receipts. You might not receive delivery receipts for the messages you sent.")); + newDeliverReceiptAlert = chatWindow_->addAlert(QT_TRANSLATE_NOOP("", "This chat may not support delivery receipts. You might not receive delivery receipts for the messages you sent.")); + } else { + if (deliveryReceiptAlert_) { + chatWindow_->removeAlert(*deliveryReceiptAlert_); + deliveryReceiptAlert_.reset(); + } + } + if (newDeliverReceiptAlert) { + if (deliveryReceiptAlert_) { + chatWindow_->removeAlert(*deliveryReceiptAlert_); + } + deliveryReceiptAlert_ = newDeliverReceiptAlert; + } +} + +void ChatController::handleBlockingStateChanged() { + boost::shared_ptr<BlockList> blockList = clientBlockListManager_->getBlockList(); + if (blockList->getState() == BlockList::Available) { + if (isInMUC_ ? blockList->isBlocked(toJID_) : blockList->isBlocked(toJID_.toBare())) { + if (!blockedContactAlert_) { + blockedContactAlert_ = chatWindow_->addAlert(QT_TRANSLATE_NOOP("", "You've currently blocked this contact. To continue your conversation you have to unblock the contact first.")); + } + chatWindow_->setInputEnabled(false); + chatWindow_->setBlockingState(ChatWindow::IsBlocked); + + // disconnect typing events to prevent chat state notifciations to blocked contacts + chatWindow_->onUserTyping.disconnect(boost::bind(&ChatStateNotifier::setUserIsTyping, chatStateNotifier_)); + chatWindow_->onUserCancelsTyping.disconnect(boost::bind(&ChatStateNotifier::userCancelledNewMessage, chatStateNotifier_)); + } else { + if (blockedContactAlert_) { + chatWindow_->removeAlert(*blockedContactAlert_); + blockedContactAlert_.reset(); + } + chatWindow_->setInputEnabled(true); + chatWindow_->setBlockingState(ChatWindow::IsUnblocked); + + chatWindow_->onUserTyping.connect(boost::bind(&ChatStateNotifier::setUserIsTyping, chatStateNotifier_)); + chatWindow_->onUserCancelsTyping.connect(boost::bind(&ChatStateNotifier::userCancelledNewMessage, chatStateNotifier_)); + } + } +} + +void ChatController::handleBlockUserRequest() { + if (isInMUC_) { + eventStream_->send(boost::make_shared<RequestChangeBlockStateUIEvent>(RequestChangeBlockStateUIEvent::Blocked, toJID_)); } else { - chatWindow_->cancelAlert(); + eventStream_->send(boost::make_shared<RequestChangeBlockStateUIEvent>(RequestChangeBlockStateUIEvent::Blocked, toJID_.toBare())); + } +} + +void ChatController::handleUnblockUserRequest() { + if (isInMUC_) { + eventStream_->send(boost::make_shared<RequestChangeBlockStateUIEvent>(RequestChangeBlockStateUIEvent::Unblocked, toJID_)); + } else { + eventStream_->send(boost::make_shared<RequestChangeBlockStateUIEvent>(RequestChangeBlockStateUIEvent::Unblocked, toJID_.toBare())); + } +} + +void ChatController::handleInviteToChat(const std::vector<JID>& droppedJIDs) { + boost::shared_ptr<UIEvent> event(new RequestInviteToMUCUIEvent(toJID_.toBare(), droppedJIDs, RequestInviteToMUCUIEvent::Impromptu)); + eventStream_->send(event); +} + +void ChatController::handleWindowClosed() { + onWindowClosed(); +} + +void ChatController::handleUIEvent(boost::shared_ptr<UIEvent> event) { + boost::shared_ptr<InviteToMUCUIEvent> inviteEvent = boost::dynamic_pointer_cast<InviteToMUCUIEvent>(event); + if (inviteEvent && inviteEvent->getRoom() == toJID_.toBare()) { + onConvertToMUC(detachChatWindow(), inviteEvent->getInvites(), inviteEvent->getReason()); } } + void ChatController::postSendMessage(const std::string& body, boost::shared_ptr<Stanza> sentStanza) { boost::shared_ptr<Replace> replace = sentStanza->getPayload<Replace>(); if (replace) { eraseIf(unackedStanzas_, PairSecondEquals<boost::shared_ptr<Stanza>, std::string>(myLastMessageUIID_)); - replaceMessage(body, myLastMessageUIID_, boost::posix_time::microsec_clock::universal_time()); + replaceMessage(body, myLastMessageUIID_, true, boost::posix_time::microsec_clock::universal_time(), HighlightAction()); } else { - myLastMessageUIID_ = addMessage(body, QT_TRANSLATE_NOOP("", "me"), true, labelsEnabled_ ? chatWindow_->getSelectedSecurityLabel().getLabel() : boost::shared_ptr<SecurityLabel>(), std::string(avatarManager_->getAvatarPath(selfJID_).string()), boost::posix_time::microsec_clock::universal_time()); + myLastMessageUIID_ = addMessage(body, QT_TRANSLATE_NOOP("", "me"), true, labelsEnabled_ ? chatWindow_->getSelectedSecurityLabel().getLabel() : boost::shared_ptr<SecurityLabel>(), avatarManager_->getAvatarPath(selfJID_), boost::posix_time::microsec_clock::universal_time(), HighlightAction()); } @@ -252,4 +381,12 @@ void ChatController::handleNewFileTransferController(FileTransferController* ftc } +void ChatController::handleWhiteboardSessionRequest(bool senderIsSelf) { + lastWbID_ = chatWindow_->addWhiteboardRequest(senderIsSelf); +} + +void ChatController::handleWhiteboardStateChange(const ChatWindow::WhiteboardSessionState state) { + chatWindow_->setWhiteboardSessionStatus(lastWbID_, state); +} + void ChatController::handleFileTransferCancel(std::string id) { SWIFT_LOG(debug) << "handleFileTransferCancel(" << id << ")" << std::endl; @@ -271,5 +408,5 @@ void ChatController::handleFileTransferStart(std::string id, std::string descrip void ChatController::handleFileTransferAccept(std::string id, std::string filename) { - SWIFT_LOG(debug) "handleFileTransferAccept(" << id << ", " << filename << ")" << std::endl; + SWIFT_LOG(debug) << "handleFileTransferAccept(" << id << ", " << filename << ")" << std::endl; if (ftControllers.find(id) != ftControllers.end()) { ftControllers[id]->accept(filename); @@ -284,4 +421,16 @@ void ChatController::handleSendFileRequest(std::string filename) { } +void ChatController::handleWhiteboardSessionAccept() { + eventStream_->send(boost::make_shared<AcceptWhiteboardSessionUIEvent>(toJID_)); +} + +void ChatController::handleWhiteboardSessionCancel() { + eventStream_->send(boost::make_shared<CancelWhiteboardSessionUIEvent>(toJID_)); +} + +void ChatController::handleWhiteboardWindowShow() { + eventStream_->send(boost::make_shared<ShowWhiteboardUIEvent>(toJID_)); +} + std::string ChatController::senderDisplayNameFromMessage(const JID& from) { return nickResolver_->jidToNick(from); @@ -303,4 +452,9 @@ std::string ChatController::getStatusChangeString(boost::shared_ptr<Presence> pr } } + Idle::ref idle; + if ((idle = presence->getPayload<Idle>())) { + response += str(format(QT_TRANSLATE_NOOP("", " and has been idle since %1%")) % dateTimeToLocalString(idle->getSince())); + } + if (!response.empty()) { response = str(format(response) % nick); @@ -337,7 +491,7 @@ void ChatController::handlePresenceChange(boost::shared_ptr<Presence> newPresenc if (newStatusChangeString != lastStatusChangeString_) { if (lastWasPresence_) { - chatWindow_->replaceLastMessage(newStatusChangeString); + chatWindow_->replaceLastMessage(chatMessageParser_->parseMessageBody(newStatusChangeString), ChatWindow::UpdateTimestamp); } else { - chatWindow_->addPresenceMessage(newStatusChangeString); + chatWindow_->addPresenceMessage(chatMessageParser_->parseMessageBody(newStatusChangeString), ChatWindow::DefaultDirection); } lastStatusChangeString_ = newStatusChangeString; @@ -350,3 +504,23 @@ boost::optional<boost::posix_time::ptime> ChatController::getMessageTimestamp(bo } +void ChatController::logMessage(const std::string& message, const JID& fromJID, const JID& toJID, const boost::posix_time::ptime& timeStamp, bool /* isIncoming */) { + HistoryMessage::Type type; + if (mucRegistry_->isMUC(fromJID.toBare()) || mucRegistry_->isMUC(toJID.toBare())) { + type = HistoryMessage::PrivateMessage; + } + else { + type = HistoryMessage::Chat; + } + + if (historyController_) { + historyController_->addMessage(message, fromJID, toJID, type, timeStamp); + } +} + +ChatWindow* ChatController::detachChatWindow() { + chatWindow_->onUserTyping.disconnect(boost::bind(&ChatStateNotifier::setUserIsTyping, chatStateNotifier_)); + chatWindow_->onUserCancelsTyping.disconnect(boost::bind(&ChatStateNotifier::userCancelledNewMessage, chatStateNotifier_)); + return ChatControllerBase::detachChatWindow(); +} + } diff --git a/Swift/Controllers/Chat/ChatController.h b/Swift/Controllers/Chat/ChatController.h index 00d167e..998b437 100644 --- a/Swift/Controllers/Chat/ChatController.h +++ b/Swift/Controllers/Chat/ChatController.h @@ -1,4 +1,4 @@ /* - * Copyright (c) 2010-2012 Kevin Smith + * Copyright (c) 2010-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. @@ -22,16 +22,26 @@ namespace Swift { class FileTransferController; class SettingsProvider; + class HistoryController; + class HighlightManager; + class ClientBlockListManager; + class UIEvent; class ChatController : public ChatControllerBase { public: - ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, SettingsProvider* settings); + ChatController(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &contact, NickResolver* nickResolver, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool isInMUC, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, bool userWantsReceipts, SettingsProvider* settings, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, boost::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider); virtual ~ChatController(); virtual void setToJID(const JID& jid); + virtual void setAvailableServerFeatures(boost::shared_ptr<DiscoInfo> info); virtual void setOnline(bool online); virtual void handleNewFileTransferController(FileTransferController* ftc); + virtual void handleWhiteboardSessionRequest(bool senderIsSelf); + virtual void handleWhiteboardStateChange(const ChatWindow::WhiteboardSessionState state); virtual void setContactIsReceivingPresence(bool /*isReceivingPresence*/); + virtual ChatWindow* detachChatWindow(); protected: void cancelReplaces(); + JID getBaseJID(); + void logMessage(const std::string& message, const JID& fromJID, const JID& toJID, const boost::posix_time::ptime& timeStamp, bool isIncoming); private: @@ -41,5 +51,5 @@ namespace Swift { void postSendMessage(const std::string &body, boost::shared_ptr<Stanza> sentStanza); void preHandleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent); - void postHandleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent); + void postHandleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent, const HighlightAction&); void preSendMessageRequest(boost::shared_ptr<Message>); std::string senderDisplayNameFromMessage(const JID& from); @@ -55,7 +65,21 @@ namespace Swift { void handleSendFileRequest(std::string filename); + void handleWhiteboardSessionAccept(); + void handleWhiteboardSessionCancel(); + void handleWhiteboardWindowShow(); + void handleSettingChanged(const std::string& settingPath); void checkForDisplayingDisplayReceiptsAlert(); + void handleBlockingStateChanged(); + void handleBlockUserRequest(); + void handleUnblockUserRequest(); + + void handleInviteToChat(const std::vector<JID>& droppedJIDs); + + void handleWindowClosed(); + + void handleUIEvent(boost::shared_ptr<UIEvent> event); + private: NickResolver* nickResolver_; @@ -76,4 +100,13 @@ namespace Swift { std::map<std::string, FileTransferController*> ftControllers; SettingsProvider* settings_; + std::string lastWbID_; + + ClientBlockListManager* clientBlockListManager_; + boost::bsignals::scoped_connection blockingOnStateChangedConnection_; + boost::bsignals::scoped_connection blockingOnItemAddedConnection_; + boost::bsignals::scoped_connection blockingOnItemRemovedConnection_; + + boost::optional<ChatWindow::AlertID> deliveryReceiptAlert_; + boost::optional<ChatWindow::AlertID> blockedContactAlert_; }; } diff --git a/Swift/Controllers/Chat/ChatControllerBase.cpp b/Swift/Controllers/Chat/ChatControllerBase.cpp index 3ff52a6..2c2540c 100644 --- a/Swift/Controllers/Chat/ChatControllerBase.cpp +++ b/Swift/Controllers/Chat/ChatControllerBase.cpp @@ -1,9 +1,9 @@ /* - * Copyright (c) 2010-2011 Kevin Smith + * Copyright (c) 2010-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ -#include "Swift/Controllers/Chat/ChatControllerBase.h" +#include <Swift/Controllers/Chat/ChatControllerBase.h> #include <sstream> @@ -14,8 +14,9 @@ #include <boost/smart_ptr/make_shared.hpp> #include <boost/date_time/posix_time/posix_time.hpp> +#include <boost/numeric/conversion/cast.hpp> #include <boost/algorithm/string.hpp> -#include <Swift/Controllers/Intl.h> #include <Swiften/Base/format.h> +#include <Swiften/Base/Path.h> #include <Swiften/Base/String.h> #include <Swiften/Client/StanzaChannel.h> @@ -24,15 +25,23 @@ #include <Swiften/Elements/MUCUserPayload.h> #include <Swiften/Base/foreach.h> -#include <Swift/Controllers/XMPPEvents/EventController.h> #include <Swiften/Disco/EntityCapsProvider.h> -#include <Swift/Controllers/UIInterfaces/ChatWindow.h> -#include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> #include <Swiften/Queries/Requests/GetSecurityLabelsCatalogRequest.h> #include <Swiften/Avatars/AvatarManager.h> + +#include <Swift/Controllers/Intl.h> +#include <Swift/Controllers/XMPPEvents/EventController.h> +#include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> +#include <Swift/Controllers/UIEvents/UIEventStream.h> +#include <Swift/Controllers/UIInterfaces/ChatWindow.h> +#include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> #include <Swift/Controllers/XMPPEvents/MUCInviteEvent.h> +#include <Swift/Controllers/HighlightManager.h> +#include <Swift/Controllers/Highlighter.h> +#include <Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h> +#include <Swift/Controllers/Chat/ChatMessageParser.h> namespace Swift { -ChatControllerBase::ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider) : selfJID_(self), stanzaChannel_(stanzaChannel), iqRouter_(iqRouter), chatWindowFactory_(chatWindowFactory), toJID_(toJID), labelsEnabled_(false), presenceOracle_(presenceOracle), avatarManager_(avatarManager), useDelayForLatency_(useDelayForLatency), eventController_(eventController), timerFactory_(timerFactory), entityCapsProvider_(entityCapsProvider) { +ChatControllerBase::ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, boost::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider) : selfJID_(self), stanzaChannel_(stanzaChannel), iqRouter_(iqRouter), chatWindowFactory_(chatWindowFactory), toJID_(toJID), labelsEnabled_(false), presenceOracle_(presenceOracle), avatarManager_(avatarManager), useDelayForLatency_(useDelayForLatency), eventController_(eventController), timerFactory_(timerFactory), entityCapsProvider_(entityCapsProvider), historyController_(historyController), mucRegistry_(mucRegistry), chatMessageParser_(chatMessageParser), autoAcceptMUCInviteDecider_(autoAcceptMUCInviteDecider), eventStream_(eventStream) { chatWindow_ = chatWindowFactory_->createChatWindow(toJID, eventStream); chatWindow_->onAllMessagesRead.connect(boost::bind(&ChatControllerBase::handleAllMessagesRead, this)); @@ -40,4 +49,5 @@ ChatControllerBase::ChatControllerBase(const JID& self, StanzaChannel* stanzaCha chatWindow_->onLogCleared.connect(boost::bind(&ChatControllerBase::handleLogCleared, this)); entityCapsProvider_->onCapsChanged.connect(boost::bind(&ChatControllerBase::handleCapsChanged, this, _1)); + highlighter_ = highlightManager->createHighlighter(); setOnline(stanzaChannel->isAvailable() && iqRouter->isAvailable()); createDayChangeTimer(); @@ -45,4 +55,5 @@ ChatControllerBase::ChatControllerBase(const JID& self, StanzaChannel* stanzaCha ChatControllerBase::~ChatControllerBase() { + delete highlighter_; delete chatWindow_; } @@ -52,4 +63,10 @@ void ChatControllerBase::handleLogCleared() { } +ChatWindow* ChatControllerBase::detachChatWindow() { + ChatWindow* chatWindow = chatWindow_; + chatWindow_ = NULL; + return chatWindow; +} + void ChatControllerBase::handleCapsChanged(const JID& jid) { if (jid.compare(toJID_, JID::WithoutResource) == 0) { @@ -58,9 +75,15 @@ void ChatControllerBase::handleCapsChanged(const JID& jid) { } +void ChatControllerBase::setCanStartImpromptuChats(bool supportsImpromptu) { + if (chatWindow_) { + chatWindow_->setCanInitiateImpromptuChats(supportsImpromptu); + } +} + void ChatControllerBase::createDayChangeTimer() { if (timerFactory_) { boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); boost::posix_time::ptime midnight(now.date() + boost::gregorian::days(1)); - long millisecondsUntilMidnight = (midnight - now).total_milliseconds(); + int millisecondsUntilMidnight = boost::numeric_cast<int>((midnight - now).total_milliseconds()); dateChangeTimer_ = boost::shared_ptr<Timer>(timerFactory_->createTimer(millisecondsUntilMidnight)); dateChangeTimer_->onTick.connect(boost::bind(&ChatControllerBase::handleDayChangeTick, this)); @@ -72,5 +95,5 @@ void ChatControllerBase::handleDayChangeTick() { dateChangeTimer_->stop(); boost::posix_time::ptime now = boost::posix_time::second_clock::local_time(); - chatWindow_->addSystemMessage(str(format(QT_TRANSLATE_NOOP("", "The day is now %1%")) % std::string(boost::posix_time::to_iso_extended_string(now)).substr(0,10))); + chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(str(format(QT_TRANSLATE_NOOP("", "The day is now %1%")) % std::string(boost::posix_time::to_iso_extended_string(now)).substr(0,10))), ChatWindow::DefaultDirection); dayTicked(); createDayChangeTimer(); @@ -85,7 +108,11 @@ void ChatControllerBase::setOnline(bool online) { } +JID ChatControllerBase::getBaseJID() { + return JID(toJID_.toBare()); +} + void ChatControllerBase::setAvailableServerFeatures(boost::shared_ptr<DiscoInfo> info) { if (iqRouter_->isAvailable() && info->hasFeature(DiscoInfo::SecurityLabelsCatalogFeature)) { - GetSecurityLabelsCatalogRequest::ref request = GetSecurityLabelsCatalogRequest::create(JID(toJID_.toBare()), iqRouter_); + GetSecurityLabelsCatalogRequest::ref request = GetSecurityLabelsCatalogRequest::create(getBaseJID(), iqRouter_); request->onResponse.connect(boost::bind(&ChatControllerBase::handleSecurityLabelsCatalogResponse, this, _1, _2)); request->send(); @@ -109,5 +136,5 @@ void ChatControllerBase::handleAllMessagesRead() { int ChatControllerBase::getUnreadCount() { - return targetedUnreadMessages_.size(); + return boost::numeric_cast<int>(targetedUnreadMessages_.size()); } @@ -121,5 +148,8 @@ void ChatControllerBase::handleSendMessageRequest(const std::string &body, bool message->setBody(body); if (labelsEnabled_) { - SecurityLabelsCatalog::Item labelItem = chatWindow_->getSelectedSecurityLabel(); + if (!isCorrectionMessage) { + lastLabel_ = chatWindow_->getSelectedSecurityLabel(); + } + SecurityLabelsCatalog::Item labelItem = lastLabel_; if (labelItem.getLabel()) { message->addPayload(labelItem.getLabel()); @@ -127,6 +157,7 @@ void ChatControllerBase::handleSendMessageRequest(const std::string &body, bool } preSendMessageRequest(message); - if (useDelayForLatency_) { + boost::posix_time::ptime now = boost::posix_time::microsec_clock::universal_time(); + if (useDelayForLatency_) { message->addPayload(boost::make_shared<Delay>(now, selfJID_)); } @@ -138,8 +169,12 @@ void ChatControllerBase::handleSendMessageRequest(const std::string &body, bool postSendMessage(message->getBody(), boost::dynamic_pointer_cast<Stanza>(message)); onActivity(message->getBody()); + +#ifdef SWIFT_EXPERIMENTAL_HISTORY + logMessage(body, selfJID_, toJID_, now, false); +#endif } void ChatControllerBase::handleSecurityLabelsCatalogResponse(boost::shared_ptr<SecurityLabelsCatalog> catalog, ErrorPayload::ref error) { - if (!error) { + if (catalog && !error) { if (catalog->getItems().size() == 0) { chatWindow_->setSecurityLabelsEnabled(false); @@ -164,17 +199,21 @@ void ChatControllerBase::activateChatWindow() { } -std::string ChatControllerBase::addMessage(const std::string& message, const std::string& senderName, bool senderIsSelf, const boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time) { +bool ChatControllerBase::hasOpenWindow() const { + return chatWindow_ && chatWindow_->isVisible(); +} + +std::string ChatControllerBase::addMessage(const std::string& message, const std::string& senderName, bool senderIsSelf, const boost::shared_ptr<SecurityLabel> label, const boost::filesystem::path& avatarPath, const boost::posix_time::ptime& time, const HighlightAction& highlight) { if (boost::starts_with(message, "/me ")) { - return chatWindow_->addAction(String::getSplittedAtFirst(message, ' ').second, senderName, senderIsSelf, label, avatarPath, time); + return chatWindow_->addAction(chatMessageParser_->parseMessageBody(String::getSplittedAtFirst(message, ' ').second), senderName, senderIsSelf, label, pathToString(avatarPath), time, highlight); } else { - return chatWindow_->addMessage(message, senderName, senderIsSelf, label, avatarPath, time); + return chatWindow_->addMessage(chatMessageParser_->parseMessageBody(message,highlighter_->getNick(),senderIsSelf), senderName, senderIsSelf, label, pathToString(avatarPath), time, highlight); } } -void ChatControllerBase::replaceMessage(const std::string& message, const std::string& id, const boost::posix_time::ptime& time) { +void ChatControllerBase::replaceMessage(const std::string& message, const std::string& id, bool senderIsSelf, const boost::posix_time::ptime& time, const HighlightAction& highlight) { if (boost::starts_with(message, "/me ")) { - chatWindow_->replaceWithAction(String::getSplittedAtFirst(message, ' ').second, id, time); + chatWindow_->replaceWithAction(chatMessageParser_->parseMessageBody(String::getSplittedAtFirst(message, ' ').second), id, time, highlight); } else { - chatWindow_->replaceMessage(message, id, time); + chatWindow_->replaceMessage(chatMessageParser_->parseMessageBody(message,highlighter_->getNick(),senderIsSelf), id, time, highlight); } } @@ -194,7 +233,10 @@ void ChatControllerBase::handleIncomingMessage(boost::shared_ptr<MessageEvent> m boost::shared_ptr<Message> message = messageEvent->getStanza(); std::string body = message->getBody(); + HighlightAction highlight; if (message->isError()) { + if (!message->getTo().getResource().empty()) { std::string errorMessage = str(format(QT_TRANSLATE_NOOP("", "Couldn't send message: %1%")) % getErrorMessage(message->getPayload<ErrorPayload>())); - chatWindow_->addErrorMessage(errorMessage); + chatWindow_->addErrorMessage(chatMessageParser_->parseMessageBody(errorMessage)); + } } else if (messageEvent->getStanza()->getPayload<MUCInvitationPayload>()) { @@ -220,5 +262,5 @@ void ChatControllerBase::handleIncomingMessage(boost::shared_ptr<MessageEvent> m std::ostringstream s; s << "The following message took " << (now - delayPayloads[i]->getStamp()).total_milliseconds() / 1000.0 << " seconds to be delivered from " << delayPayloads[i]->getFrom()->toString() << "."; - chatWindow_->addSystemMessage(std::string(s.str())); + chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(std::string(s.str())), ChatWindow::DefaultDirection); } boost::shared_ptr<SecurityLabel> label = message->getPayload<SecurityLabel>(); @@ -232,4 +274,9 @@ void ChatControllerBase::handleIncomingMessage(boost::shared_ptr<MessageEvent> m onActivity(body); + // Highlight + if (!isIncomingMessageFromMe(message)) { + highlight = highlighter_->findAction(body, senderDisplayNameFromMessage(from)); + } + boost::shared_ptr<Replace> replace = message->getPayload<Replace>(); if (replace) { @@ -239,15 +286,17 @@ void ChatControllerBase::handleIncomingMessage(boost::shared_ptr<MessageEvent> m lastMessage = lastMessagesUIID_.find(from); if (lastMessage != lastMessagesUIID_.end()) { - replaceMessage(body, lastMessagesUIID_[from], timeStamp); + replaceMessage(body, lastMessagesUIID_[from], isIncomingMessageFromMe(message), timeStamp, highlight); } } else { - lastMessagesUIID_[from] = addMessage(body, senderDisplayNameFromMessage(from), isIncomingMessageFromMe(message), label, std::string(avatarManager_->getAvatarPath(from).string()), timeStamp); + lastMessagesUIID_[from] = addMessage(body, senderDisplayNameFromMessage(from), isIncomingMessageFromMe(message), label, avatarManager_->getAvatarPath(from), timeStamp, highlight); } + + logMessage(body, from, selfJID_, timeStamp, true); } chatWindow_->show(); - chatWindow_->setUnreadMessageCount(unreadMessages_.size()); + chatWindow_->setUnreadMessageCount(boost::numeric_cast<int>(unreadMessages_.size())); onUnreadCountChanged(); - postHandleIncomingMessage(messageEvent); + postHandleIncomingMessage(messageEvent, highlight); } @@ -259,28 +308,29 @@ std::string ChatControllerBase::getErrorMessage(boost::shared_ptr<ErrorPayload> else { switch (error->getCondition()) { - case ErrorPayload::BadRequest: return QT_TRANSLATE_NOOP("", "Bad request"); break; - case ErrorPayload::Conflict: return QT_TRANSLATE_NOOP("", "Conflict"); break; - case ErrorPayload::FeatureNotImplemented: return QT_TRANSLATE_NOOP("", "This feature is not implemented"); break; - case ErrorPayload::Forbidden: return QT_TRANSLATE_NOOP("", "Forbidden"); break; - case ErrorPayload::Gone: return QT_TRANSLATE_NOOP("", "Recipient can no longer be contacted"); break; - case ErrorPayload::InternalServerError: return QT_TRANSLATE_NOOP("", "Internal server error"); break; - case ErrorPayload::ItemNotFound: return QT_TRANSLATE_NOOP("", "Item not found"); break; - case ErrorPayload::JIDMalformed: return QT_TRANSLATE_NOOP("", "JID Malformed"); break; - case ErrorPayload::NotAcceptable: return QT_TRANSLATE_NOOP("", "Message was rejected"); break; - case ErrorPayload::NotAllowed: return QT_TRANSLATE_NOOP("", "Not allowed"); break; - case ErrorPayload::NotAuthorized: return QT_TRANSLATE_NOOP("", "Not authorized"); break; - case ErrorPayload::PaymentRequired: return QT_TRANSLATE_NOOP("", "Payment is required"); break; - case ErrorPayload::RecipientUnavailable: return QT_TRANSLATE_NOOP("", "Recipient is unavailable"); break; - case ErrorPayload::Redirect: return QT_TRANSLATE_NOOP("", "Redirect"); break; - case ErrorPayload::RegistrationRequired: return QT_TRANSLATE_NOOP("", "Registration required"); break; - case ErrorPayload::RemoteServerNotFound: return QT_TRANSLATE_NOOP("", "Recipient's server not found"); break; - case ErrorPayload::RemoteServerTimeout: return QT_TRANSLATE_NOOP("", "Remote server timeout"); break; - case ErrorPayload::ResourceConstraint: return QT_TRANSLATE_NOOP("", "The server is low on resources"); break; - case ErrorPayload::ServiceUnavailable: return QT_TRANSLATE_NOOP("", "The service is unavailable"); break; - case ErrorPayload::SubscriptionRequired: return QT_TRANSLATE_NOOP("", "A subscription is required"); break; - case ErrorPayload::UndefinedCondition: return QT_TRANSLATE_NOOP("", "Undefined condition"); break; - case ErrorPayload::UnexpectedRequest: return QT_TRANSLATE_NOOP("", "Unexpected request"); break; - } - } + case ErrorPayload::BadRequest: return QT_TRANSLATE_NOOP("", "Bad request"); + case ErrorPayload::Conflict: return QT_TRANSLATE_NOOP("", "Conflict"); + case ErrorPayload::FeatureNotImplemented: return QT_TRANSLATE_NOOP("", "This feature is not implemented"); + case ErrorPayload::Forbidden: return QT_TRANSLATE_NOOP("", "Forbidden"); + case ErrorPayload::Gone: return QT_TRANSLATE_NOOP("", "Recipient can no longer be contacted"); + case ErrorPayload::InternalServerError: return QT_TRANSLATE_NOOP("", "Internal server error"); + case ErrorPayload::ItemNotFound: return QT_TRANSLATE_NOOP("", "Item not found"); + case ErrorPayload::JIDMalformed: return QT_TRANSLATE_NOOP("", "JID Malformed"); + case ErrorPayload::NotAcceptable: return QT_TRANSLATE_NOOP("", "Message was rejected"); + case ErrorPayload::NotAllowed: return QT_TRANSLATE_NOOP("", "Not allowed"); + case ErrorPayload::NotAuthorized: return QT_TRANSLATE_NOOP("", "Not authorized"); + case ErrorPayload::PaymentRequired: return QT_TRANSLATE_NOOP("", "Payment is required"); + case ErrorPayload::RecipientUnavailable: return QT_TRANSLATE_NOOP("", "Recipient is unavailable"); + case ErrorPayload::Redirect: return QT_TRANSLATE_NOOP("", "Redirect"); + case ErrorPayload::RegistrationRequired: return QT_TRANSLATE_NOOP("", "Registration required"); + case ErrorPayload::RemoteServerNotFound: return QT_TRANSLATE_NOOP("", "Recipient's server not found"); + case ErrorPayload::RemoteServerTimeout: return QT_TRANSLATE_NOOP("", "Remote server timeout"); + case ErrorPayload::ResourceConstraint: return QT_TRANSLATE_NOOP("", "The server is low on resources"); + case ErrorPayload::ServiceUnavailable: return QT_TRANSLATE_NOOP("", "The service is unavailable"); + case ErrorPayload::SubscriptionRequired: return QT_TRANSLATE_NOOP("", "A subscription is required"); + case ErrorPayload::UndefinedCondition: return QT_TRANSLATE_NOOP("", "Undefined condition"); + case ErrorPayload::UnexpectedRequest: return QT_TRANSLATE_NOOP("", "Unexpected request"); + } + } + assert(false); return defaultMessage; } @@ -289,7 +339,7 @@ void ChatControllerBase::handleGeneralMUCInvitation(MUCInviteEvent::ref event) { unreadMessages_.push_back(event); chatWindow_->show(); - chatWindow_->setUnreadMessageCount(unreadMessages_.size()); + chatWindow_->setUnreadMessageCount(boost::numeric_cast<int>(unreadMessages_.size())); onUnreadCountChanged(); - chatWindow_->addMUCInvitation(senderDisplayNameFromMessage(event->getInviter()), event->getRoomJID(), event->getReason(), event->getPassword(), event->getDirect()); + chatWindow_->addMUCInvitation(senderDisplayNameFromMessage(event->getInviter()), event->getRoomJID(), event->getReason(), event->getPassword(), event->getDirect(), event->getImpromptu()); eventController_->handleIncomingEvent(event); } @@ -298,7 +348,11 @@ void ChatControllerBase::handleMUCInvitation(Message::ref message) { MUCInvitationPayload::ref invite = message->getPayload<MUCInvitationPayload>(); - MUCInviteEvent::ref inviteEvent = boost::make_shared<MUCInviteEvent>(toJID_, invite->getJID(), invite->getReason(), invite->getPassword(), true); + if (autoAcceptMUCInviteDecider_->isAutoAcceptedInvite(message->getFrom(), invite)) { + eventStream_->send(boost::make_shared<JoinMUCUIEvent>(invite->getJID(), boost::optional<std::string>(), boost::optional<std::string>(), false, false, true)); + } else { + MUCInviteEvent::ref inviteEvent = boost::make_shared<MUCInviteEvent>(toJID_, invite->getJID(), invite->getReason(), invite->getPassword(), true, invite->getIsImpromptu()); handleGeneralMUCInvitation(inviteEvent); } +} void ChatControllerBase::handleMediatedMUCInvitation(Message::ref message) { @@ -314,9 +368,7 @@ void ChatControllerBase::handleMediatedMUCInvitation(Message::ref message) { } - MUCInviteEvent::ref inviteEvent = boost::make_shared<MUCInviteEvent>(invite.from, from, reason, password, false); + MUCInviteEvent::ref inviteEvent = boost::make_shared<MUCInviteEvent>(invite.from, from, reason, password, false, false); handleGeneralMUCInvitation(inviteEvent); } - - } diff --git a/Swift/Controllers/Chat/ChatControllerBase.h b/Swift/Controllers/Chat/ChatControllerBase.h index 8654311..a0b848b 100644 --- a/Swift/Controllers/Chat/ChatControllerBase.h +++ b/Swift/Controllers/Chat/ChatControllerBase.h @@ -1,4 +1,4 @@ /* - * Copyright (c) 2010 Kevin Smith + * Copyright (c) 2010-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. @@ -9,28 +9,33 @@ #include <map> #include <vector> +#include <string> + #include <boost/shared_ptr.hpp> -#include "Swiften/Base/boost_bsignals.h" -#include <boost/filesystem.hpp> +#include <boost/filesystem/path.hpp> #include <boost/optional.hpp> #include <boost/date_time/posix_time/posix_time.hpp> -#include "Swiften/Network/Timer.h" -#include "Swiften/Network/TimerFactory.h" -#include "Swiften/Elements/Stanza.h" -#include <string> -#include "Swiften/Elements/DiscoInfo.h" -#include "Swift/Controllers/XMPPEvents/MessageEvent.h" +#include <Swiften/Network/Timer.h> +#include <Swiften/Network/TimerFactory.h> +#include <Swiften/Elements/Stanza.h> +#include <Swiften/Base/boost_bsignals.h> +#include <Swiften/Elements/DiscoInfo.h> +#include <Swiften/JID/JID.h> +#include <Swiften/Elements/SecurityLabelsCatalog.h> +#include <Swiften/Elements/ErrorPayload.h> +#include <Swiften/Presence/PresenceOracle.h> +#include <Swiften/Queries/IQRouter.h> +#include <Swiften/Base/IDGenerator.h> +#include <Swiften/MUC/MUCRegistry.h> + +#include <Swift/Controllers/XMPPEvents/MessageEvent.h> #include <Swift/Controllers/XMPPEvents/MUCInviteEvent.h> -#include "Swiften/JID/JID.h" -#include "Swiften/Elements/SecurityLabelsCatalog.h" -#include "Swiften/Elements/ErrorPayload.h" -#include "Swiften/Presence/PresenceOracle.h" -#include "Swiften/Queries/IQRouter.h" -#include "Swiften/Base/IDGenerator.h" +#include <Swift/Controllers/HistoryController.h> +#include <Swift/Controllers/HighlightManager.h> +#include <Swift/Controllers/UIInterfaces/ChatWindow.h> namespace Swift { class IQRouter; class StanzaChannel; - class ChatWindow; class ChatWindowFactory; class AvatarManager; @@ -38,4 +43,8 @@ namespace Swift { class EventController; class EntityCapsProvider; + class HighlightManager; + class Highlighter; + class ChatMessageParser; + class AutoAcceptMUCInviteDecider; class ChatControllerBase : public boost::bsignals::trackable { @@ -44,37 +53,45 @@ namespace Swift { void showChatWindow(); void activateChatWindow(); - void setAvailableServerFeatures(boost::shared_ptr<DiscoInfo> info); + bool hasOpenWindow() const; + virtual void setAvailableServerFeatures(boost::shared_ptr<DiscoInfo> info); void handleIncomingMessage(boost::shared_ptr<MessageEvent> message); - std::string addMessage(const std::string& message, const std::string& senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const std::string& avatarPath, const boost::posix_time::ptime& time); - void replaceMessage(const std::string& message, const std::string& id, const boost::posix_time::ptime& time); + std::string addMessage(const std::string& message, const std::string& senderName, bool senderIsSelf, boost::shared_ptr<SecurityLabel> label, const boost::filesystem::path& avatarPath, const boost::posix_time::ptime& time, const HighlightAction& highlight); + void replaceMessage(const std::string& message, const std::string& id, bool senderIsSelf, const boost::posix_time::ptime& time, const HighlightAction& highlight); virtual void setOnline(bool online); virtual void setEnabled(bool enabled); - virtual void setToJID(const JID& jid) {toJID_ = jid;}; + virtual void setToJID(const JID& jid) {toJID_ = jid;} /** Used for determining when something is recent.*/ boost::signal<void (const std::string& /*activity*/)> onActivity; boost::signal<void ()> onUnreadCountChanged; + boost::signal<void ()> onWindowClosed; int getUnreadCount(); const JID& getToJID() {return toJID_;} void handleCapsChanged(const JID& jid); + void setCanStartImpromptuChats(bool supportsImpromptu); + virtual ChatWindow* detachChatWindow(); + boost::signal<void(ChatWindow* /*window to reuse*/, const std::vector<JID>& /*invite people*/, const std::string& /*reason*/)> onConvertToMUC; protected: - ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider); + ChatControllerBase(const JID& self, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, const JID &toJID, PresenceOracle* presenceOracle, AvatarManager* avatarManager, bool useDelayForLatency, UIEventStream* eventStream, EventController* eventController, TimerFactory* timerFactory, EntityCapsProvider* entityCapsProvider, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, boost::shared_ptr<ChatMessageParser> chatMessageParser, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider); /** * Pass the Message appended, and the stanza used to send it. */ - virtual void postSendMessage(const std::string&, boost::shared_ptr<Stanza>) {}; + virtual void postSendMessage(const std::string&, boost::shared_ptr<Stanza>) {} virtual std::string senderDisplayNameFromMessage(const JID& from) = 0; virtual bool isIncomingMessageFromMe(boost::shared_ptr<Message>) = 0; - virtual void preHandleIncomingMessage(boost::shared_ptr<MessageEvent>) {}; - virtual void postHandleIncomingMessage(boost::shared_ptr<MessageEvent>) {}; - virtual void preSendMessageRequest(boost::shared_ptr<Message>) {}; + virtual void preHandleIncomingMessage(boost::shared_ptr<MessageEvent>) {} + virtual void postHandleIncomingMessage(boost::shared_ptr<MessageEvent>, const HighlightAction&) {} + virtual void preSendMessageRequest(boost::shared_ptr<Message>) {} virtual bool isFromContact(const JID& from); virtual boost::optional<boost::posix_time::ptime> getMessageTimestamp(boost::shared_ptr<Message>) const = 0; - virtual void dayTicked() {}; + virtual void dayTicked() {} virtual void handleBareJIDCapsChanged(const JID& jid) = 0; std::string getErrorMessage(boost::shared_ptr<ErrorPayload>); virtual void setContactIsReceivingPresence(bool /* isReceivingPresence */) {} virtual void cancelReplaces() = 0; + /** JID any iq for account should go to - bare except for PMs */ + virtual JID getBaseJID(); + virtual void logMessage(const std::string& message, const JID& fromJID, const JID& toJID, const boost::posix_time::ptime& timeStamp, bool isIncoming) = 0; private: @@ -109,4 +126,11 @@ namespace Swift { TimerFactory* timerFactory_; EntityCapsProvider* entityCapsProvider_; + SecurityLabelsCatalog::Item lastLabel_; + HistoryController* historyController_; + MUCRegistry* mucRegistry_; + Highlighter* highlighter_; + boost::shared_ptr<ChatMessageParser> chatMessageParser_; + AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider_; + UIEventStream* eventStream_; }; } diff --git a/Swift/Controllers/Chat/ChatMessageParser.cpp b/Swift/Controllers/Chat/ChatMessageParser.cpp new file mode 100644 index 0000000..5a608db --- /dev/null +++ b/Swift/Controllers/Chat/ChatMessageParser.cpp @@ -0,0 +1,195 @@ +/* + * Copyright (c) 2013-2014 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include <Swift/Controllers/Chat/ChatMessageParser.h> + +#include <vector> +#include <utility> + +#include <boost/smart_ptr/make_shared.hpp> +#include <boost/algorithm/string.hpp> + +#include <Swiften/Base/Regex.h> +#include <Swiften/Base/foreach.h> + +#include <SwifTools/Linkify.h> + + +namespace Swift { + + ChatMessageParser::ChatMessageParser(const std::map<std::string, std::string>& emoticons, HighlightRulesListPtr highlightRules, bool mucMode) + : emoticons_(emoticons), highlightRules_(highlightRules), mucMode_(mucMode) { + } + + typedef std::pair<std::string, std::string> StringPair; + + ChatWindow::ChatMessage ChatMessageParser::parseMessageBody(const std::string& body, const std::string& nick, bool senderIsSelf) { + ChatWindow::ChatMessage parsedMessage; + std::string remaining = body; + /* Parse one, URLs */ + while (!remaining.empty()) { + bool found = false; + std::pair<std::vector<std::string>, size_t> links = Linkify::splitLink(remaining); + remaining = ""; + for (size_t i = 0; i < links.first.size(); i++) { + const std::string& part = links.first[i]; + if (found) { + // Must be on the last part, then + remaining = part; + } + else { + if (i == links.second) { + found = true; + parsedMessage.append(boost::make_shared<ChatWindow::ChatURIMessagePart>(part)); + } + else { + parsedMessage.append(boost::make_shared<ChatWindow::ChatTextMessagePart>(part)); + } + } + } + } + + /* do emoticon substitution */ + parsedMessage = emoticonHighlight(parsedMessage); + + if (!senderIsSelf) { /* do not highlight our own messsages */ + /* do word-based color highlighting */ + parsedMessage = splitHighlight(parsedMessage, nick); + } + + return parsedMessage; + } + + ChatWindow::ChatMessage ChatMessageParser::emoticonHighlight(const ChatWindow::ChatMessage& message) + { + ChatWindow::ChatMessage parsedMessage = message; + + std::string regexString; + /* Parse two, emoticons */ + foreach (StringPair emoticon, emoticons_) { + /* Construct a regexp that finds an instance of any of the emoticons inside a group + * at the start or end of the line, or beside whitespace. + */ + regexString += regexString.empty() ? "" : "|"; + std::string escaped = "(" + Regex::escape(emoticon.first) + ")"; + regexString += "^" + escaped + "|"; + regexString += escaped + "$|"; + regexString += "\\s" + escaped + "|"; + regexString += escaped + "\\s"; + + } + if (!regexString.empty()) { + regexString += ""; + boost::regex emoticonRegex(regexString); + + ChatWindow::ChatMessage newMessage; + foreach (boost::shared_ptr<ChatWindow::ChatMessagePart> part, parsedMessage.getParts()) { + boost::shared_ptr<ChatWindow::ChatTextMessagePart> textPart; + if ((textPart = boost::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(part))) { + try { + boost::match_results<std::string::const_iterator> match; + const std::string& text = textPart->text; + std::string::const_iterator start = text.begin(); + while (regex_search(start, text.end(), match, emoticonRegex)) { + int matchIndex = 0; + for (matchIndex = 1; matchIndex < static_cast<int>(match.size()); matchIndex++) { + if (match[matchIndex].length() > 0) { + //This is the matching subgroup + break; + } + } + std::string::const_iterator matchStart = match[matchIndex].first; + std::string::const_iterator matchEnd = match[matchIndex].second; + if (start != matchStart) { + /* If we're skipping over plain text since the previous emoticon, record it as plain text */ + newMessage.append(boost::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, matchStart))); + } + boost::shared_ptr<ChatWindow::ChatEmoticonMessagePart> emoticonPart = boost::make_shared<ChatWindow::ChatEmoticonMessagePart>(); + std::string matchString = match[matchIndex].str(); + std::map<std::string, std::string>::const_iterator emoticonIterator = emoticons_.find(matchString); + assert (emoticonIterator != emoticons_.end()); + const StringPair& emoticon = *emoticonIterator; + emoticonPart->imagePath = emoticon.second; + emoticonPart->alternativeText = emoticon.first; + newMessage.append(emoticonPart); + start = matchEnd; + } + if (start != text.end()) { + /* If there's plain text after the last emoticon, record it */ + newMessage.append(boost::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, text.end()))); + } + + } + catch (std::runtime_error) { + /* Basically too expensive to compute the regex results and it gave up, so pass through as text */ + newMessage.append(part); + } + } + else { + newMessage.append(part); + } + } + parsedMessage = newMessage; + + } + return parsedMessage; + } + + ChatWindow::ChatMessage ChatMessageParser::splitHighlight(const ChatWindow::ChatMessage& message, const std::string& nick) + { + ChatWindow::ChatMessage parsedMessage = message; + + for (size_t i = 0; i < highlightRules_->getSize(); ++i) { + const HighlightRule& rule = highlightRules_->getRule(i); + if (rule.getMatchMUC() && !mucMode_) { + continue; /* this rule only applies to MUC's, and this is a CHAT */ + } else if (rule.getMatchChat() && mucMode_) { + continue; /* this rule only applies to CHAT's, and this is a MUC */ + } + const std::vector<boost::regex> keywordRegex = rule.getKeywordRegex(nick); + foreach(const boost::regex& regex, keywordRegex) { + ChatWindow::ChatMessage newMessage; + foreach (boost::shared_ptr<ChatWindow::ChatMessagePart> part, parsedMessage.getParts()) { + boost::shared_ptr<ChatWindow::ChatTextMessagePart> textPart; + if ((textPart = boost::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(part))) { + try { + boost::match_results<std::string::const_iterator> match; + const std::string& text = textPart->text; + std::string::const_iterator start = text.begin(); + while (regex_search(start, text.end(), match, regex)) { + std::string::const_iterator matchStart = match[0].first; + std::string::const_iterator matchEnd = match[0].second; + if (start != matchStart) { + /* If we're skipping over plain text since the previous emoticon, record it as plain text */ + newMessage.append(boost::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, matchStart))); + } + boost::shared_ptr<ChatWindow::ChatHighlightingMessagePart> highlightPart = boost::make_shared<ChatWindow::ChatHighlightingMessagePart>(); + highlightPart->text = match.str(); + highlightPart->foregroundColor = rule.getAction().getTextColor(); + highlightPart->backgroundColor = rule.getAction().getTextBackground(); + newMessage.append(highlightPart); + start = matchEnd; + } + if (start != text.end()) { + /* If there's plain text after the last emoticon, record it */ + newMessage.append(boost::make_shared<ChatWindow::ChatTextMessagePart>(std::string(start, text.end()))); + } + } + catch (std::runtime_error) { + /* Basically too expensive to compute the regex results and it gave up, so pass through as text */ + newMessage.append(part); + } + } else { + newMessage.append(part); + } + } + parsedMessage = newMessage; + } + } + + return parsedMessage; + } +} diff --git a/Swift/Controllers/Chat/ChatMessageParser.h b/Swift/Controllers/Chat/ChatMessageParser.h new file mode 100644 index 0000000..2f5c171 --- /dev/null +++ b/Swift/Controllers/Chat/ChatMessageParser.h @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2013-2014 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#pragma once + +#include <string> + +#include <Swift/Controllers/UIInterfaces/ChatWindow.h> + +namespace Swift { + + class ChatMessageParser { + public: + ChatMessageParser(const std::map<std::string, std::string>& emoticons, HighlightRulesListPtr highlightRules, bool mucMode = false); + ChatWindow::ChatMessage parseMessageBody(const std::string& body, const std::string& nick = "", bool senderIsSelf = false); + private: + ChatWindow::ChatMessage emoticonHighlight(const ChatWindow::ChatMessage& parsedMessage); + ChatWindow::ChatMessage splitHighlight(const ChatWindow::ChatMessage& parsedMessage, const std::string& nick); + std::map<std::string, std::string> emoticons_; + HighlightRulesListPtr highlightRules_; + bool mucMode_; + }; +} diff --git a/Swift/Controllers/Chat/ChatsManager.cpp b/Swift/Controllers/Chat/ChatsManager.cpp index b0aef95..979f87a 100644 --- a/Swift/Controllers/Chat/ChatsManager.cpp +++ b/Swift/Controllers/Chat/ChatsManager.cpp @@ -1,21 +1,45 @@ /* - * Copyright (c) 2010-2011 Kevin Smith + * Copyright (c) 2010-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. */ -#include "Swift/Controllers/Chat/ChatsManager.h" +#include <Swift/Controllers/Chat/ChatsManager.h> #include <boost/bind.hpp> #include <boost/algorithm/string.hpp> #include <boost/smart_ptr/make_shared.hpp> +#include <boost/archive/text_oarchive.hpp> +#include <boost/archive/text_iarchive.hpp> +#include <boost/serialization/optional.hpp> +#include <boost/serialization/vector.hpp> +#include <boost/serialization/map.hpp> +#include <boost/serialization/string.hpp> +#include <boost/serialization/split_free.hpp> #include <Swiften/Base/foreach.h> +#include <Swiften/Presence/PresenceSender.h> +#include <Swiften/Client/NickResolver.h> +#include <Swiften/MUC/MUCManager.h> +#include <Swiften/Elements/ChatState.h> +#include <Swiften/Elements/MUCUserPayload.h> +#include <Swiften/Elements/DeliveryReceipt.h> +#include <Swiften/Elements/DeliveryReceiptRequest.h> +#include <Swiften/MUC/MUCBookmarkManager.h> +#include <Swiften/Avatars/AvatarManager.h> +#include <Swiften/Elements/MUCInvitationPayload.h> +#include <Swiften/Roster/XMPPRoster.h> +#include <Swiften/Client/ClientBlockListManager.h> +#include <Swiften/Client/StanzaChannel.h> +#include <Swiften/VCards/VCardManager.h> + #include <Swift/Controllers/Chat/ChatController.h> #include <Swift/Controllers/Chat/ChatControllerBase.h> #include <Swift/Controllers/Chat/MUCSearchController.h> +#include <Swift/Controllers/Chat/AutoAcceptMUCInviteDecider.h> #include <Swift/Controllers/XMPPEvents/EventController.h> #include <Swift/Controllers/Chat/MUCController.h> #include <Swift/Controllers/UIEvents/RequestChatUIEvent.h> +#include <Swift/Controllers/UIEvents/CreateImpromptuMUCUIEvent.h> #include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> #include <Swift/Controllers/UIEvents/RequestJoinMUCUIEvent.h> @@ -23,24 +47,53 @@ #include <Swift/Controllers/UIEvents/RemoveMUCBookmarkUIEvent.h> #include <Swift/Controllers/UIEvents/EditMUCBookmarkUIEvent.h> +#include <Swift/Controllers/UIEvents/InviteToMUCUIEvent.h> #include <Swift/Controllers/UIInterfaces/ChatListWindowFactory.h> #include <Swift/Controllers/UIInterfaces/JoinMUCWindow.h> #include <Swift/Controllers/UIInterfaces/JoinMUCWindowFactory.h> -#include <Swiften/Presence/PresenceSender.h> -#include <Swiften/Client/NickResolver.h> -#include <Swiften/MUC/MUCManager.h> -#include <Swiften/Elements/ChatState.h> -#include <Swiften/Elements/MUCUserPayload.h> -#include <Swiften/Elements/DeliveryReceipt.h> -#include <Swiften/Elements/DeliveryReceiptRequest.h> -#include <Swiften/MUC/MUCBookmarkManager.h> #include <Swift/Controllers/FileTransfer/FileTransferController.h> #include <Swift/Controllers/FileTransfer/FileTransferOverview.h> #include <Swift/Controllers/ProfileSettingsProvider.h> -#include <Swiften/Avatars/AvatarManager.h> -#include <Swiften/Elements/MUCInvitationPayload.h> -#include <Swiften/Roster/XMPPRoster.h> #include <Swift/Controllers/Settings/SettingsProvider.h> #include <Swift/Controllers/SettingConstants.h> -#include <Swiften/Client/StanzaChannel.h> +#include <Swift/Controllers/WhiteboardManager.h> +#include <Swift/Controllers/Chat/ChatMessageParser.h> +#include <Swift/Controllers/Chat/UserSearchController.h> +#include <Swiften/Disco/DiscoServiceWalker.h> +#include <Swiften/Client/ClientBlockListManager.h> +#include <Swiften/StringCodecs/Base64.h> +#include <Swiften/Base/Log.h> + +BOOST_CLASS_VERSION(Swift::ChatListWindow::Chat, 1) + +namespace boost { +namespace serialization { + template<class Archive> void save(Archive& ar, const Swift::JID& jid, const unsigned int /*version*/) { + std::string jidStr = jid.toString(); + ar << jidStr; + } + + template<class Archive> void load(Archive& ar, Swift::JID& jid, const unsigned int /*version*/) { + std::string stringJID; + ar >> stringJID; + jid = Swift::JID(stringJID); + } + + template<class Archive> inline void serialize(Archive& ar, Swift::JID& t, const unsigned int file_version){ + split_free(ar, t, file_version); + } + + template<class Archive> void serialize(Archive& ar, Swift::ChatListWindow::Chat& chat, const unsigned int version) { + ar & chat.jid; + ar & chat.chatName; + ar & chat.activity; + ar & chat.isMUC; + ar & chat.nick; + ar & chat.impromptuJIDs; + if (version > 0) { + ar & chat.password; + } + } +} +} namespace Swift { @@ -72,5 +125,12 @@ ChatsManager::ChatsManager( XMPPRoster* roster, bool eagleMode, - SettingsProvider* settings) : + SettingsProvider* settings, + HistoryController* historyController, + WhiteboardManager* whiteboardManager, + HighlightManager* highlightManager, + ClientBlockListManager* clientBlockListManager, + const std::map<std::string, std::string>& emoticons, + UserSearchController* inviteUserSearchController, + VCardManager* vcardManager) : jid_(jid), joinMUCWindowFactory_(joinMUCWindowFactory), @@ -82,5 +142,12 @@ ChatsManager::ChatsManager( roster_(roster), eagleMode_(eagleMode), - settings_(settings) { + settings_(settings), + historyController_(historyController), + whiteboardManager_(whiteboardManager), + highlightManager_(highlightManager), + emoticons_(emoticons), + clientBlockListManager_(clientBlockListManager), + inviteUserSearchController_(inviteUserSearchController), + vcardManager_(vcardManager) { timerFactory_ = timerFactory; eventController_ = eventController; @@ -108,4 +175,8 @@ ChatsManager::ChatsManager( mucSearchController_->onMUCSelected.connect(boost::bind(&ChatsManager::handleMUCSelectedAfterSearch, this, _1)); ftOverview_->onNewFileTransferController.connect(boost::bind(&ChatsManager::handleNewFileTransferController, this, _1)); + whiteboardManager_->onSessionRequest.connect(boost::bind(&ChatsManager::handleWhiteboardSessionRequest, this, _1, _2)); + whiteboardManager_->onRequestAccepted.connect(boost::bind(&ChatsManager::handleWhiteboardStateChange, this, _1, ChatWindow::WhiteboardAccepted)); + whiteboardManager_->onSessionTerminate.connect(boost::bind(&ChatsManager::handleWhiteboardStateChange, this, _1, ChatWindow::WhiteboardTerminated)); + whiteboardManager_->onRequestRejected.connect(boost::bind(&ChatsManager::handleWhiteboardStateChange, this, _1, ChatWindow::WhiteboardRejected)); roster_->onJIDAdded.connect(boost::bind(&ChatsManager::handleJIDAddedToRoster, this, _1)); roster_->onJIDRemoved.connect(boost::bind(&ChatsManager::handleJIDRemovedFromRoster, this, _1)); @@ -119,4 +190,6 @@ ChatsManager::ChatsManager( setupBookmarks(); loadRecents(); + + autoAcceptMUCInviteDecider_ = new AutoAcceptMUCInviteDecider(jid.getDomain(), roster_, settings_); } @@ -136,23 +209,32 @@ ChatsManager::~ChatsManager() { delete mucBookmarkManager_; delete mucSearchController_; + delete autoAcceptMUCInviteDecider_; } void ChatsManager::saveRecents() { - std::string recents; - int i = 1; - foreach (ChatListWindow::Chat chat, recentChats_) { - std::vector<std::string> activity; - boost::split(activity, chat.activity, boost::is_any_of("\t\n")); - if (activity.empty()) { - /* Work around Boost bug https://svn.boost.org/trac/boost/ticket/4751 */ - activity.push_back(""); - } - std::string recent = chat.jid.toString() + "\t" + (eagleMode_ ? "" : activity[0]) + "\t" + (chat.isMUC ? "true" : "false") + "\t" + chat.nick; - recents += recent + "\n"; - if (i++ > 25) { - break; + std::stringstream serializeStream; + boost::archive::text_oarchive oa(serializeStream); + std::vector<ChatListWindow::Chat> recentsLimited = std::vector<ChatListWindow::Chat>(recentChats_.begin(), recentChats_.end()); + if (recentsLimited.size() > 25) { + recentsLimited.erase(recentsLimited.begin() + 25, recentsLimited.end()); } + if (eagleMode_) { + foreach(ChatListWindow::Chat& chat, recentsLimited) { + chat.activity = ""; } - profileSettings_->storeString(RECENT_CHATS, recents); + } + + class RemoveRecent { + public: + static bool ifPrivateMessage(const ChatListWindow::Chat& chat) { + return chat.isPrivateMessage; + } + }; + + recentsLimited.erase(std::remove_if(recentsLimited.begin(), recentsLimited.end(), RemoveRecent::ifPrivateMessage), recentsLimited.end()); + + oa << recentsLimited; + std::string serializedStr = Base64::encode(createByteArray(serializeStream.str())); + profileSettings_->storeString(RECENT_CHATS, serializedStr); } @@ -196,6 +278,24 @@ void ChatsManager::updatePresenceReceivingStateOnChatController(const JID &jid) } +ChatListWindow::Chat ChatsManager::updateChatStatusAndAvatarHelper(const ChatListWindow::Chat& chat) const { + ChatListWindow::Chat fixedChat = chat; + if (fixedChat.isMUC) { + if (mucControllers_.find(fixedChat.jid.toBare()) != mucControllers_.end()) { + fixedChat.statusType = StatusShow::Online; + } + } else { + if (avatarManager_) { + fixedChat.avatarPath = avatarManager_->getAvatarPath(fixedChat.jid); + } + Presence::ref presence = presenceOracle_->getHighestPriorityPresence(fixedChat.jid.toBare()); + fixedChat.statusType = presence ? presence->getShow() : StatusShow::None; + } + return fixedChat; +} + void ChatsManager::loadRecents() { std::string recentsString(profileSettings_->getStringSetting(RECENT_CHATS)); + if (recentsString.find("\t") != std::string::npos) { + // old format std::vector<std::string> recents; boost::split(recents, recentsString, boost::is_any_of("\n")); @@ -219,19 +319,28 @@ void ChatsManager::loadRecents() { StatusShow::Type type = StatusShow::None; boost::filesystem::path path; - if (isMUC) { - if (mucControllers_.find(jid.toBare()) != mucControllers_.end()) { - type = StatusShow::Online; - } - } else { - if (avatarManager_) { - path = avatarManager_->getAvatarPath(jid); + + ChatListWindow::Chat chat(jid, nickResolver_->jidToNick(jid), activity, 0, type, path, isMUC, false, nick); + chat = updateChatStatusAndAvatarHelper(chat); + prependRecent(chat); } - Presence::ref presence = presenceOracle_->getHighestPriorityPresence(jid.toBare()); - type = presence ? presence->getShow() : StatusShow::None; + } else if (!recentsString.empty()){ + // boost searilaize based format + ByteArray debase64 = Base64::decode(recentsString); + std::vector<ChatListWindow::Chat> recentChats; + std::stringstream deserializeStream(std::string(reinterpret_cast<const char*>(vecptr(debase64)), debase64.size())); + try { + boost::archive::text_iarchive ia(deserializeStream); + ia >> recentChats; + } catch (const boost::archive::archive_exception& e) { + SWIFT_LOG(debug) << "Failed to load recents: " << e.what() << std::endl; + return; } - ChatListWindow::Chat chat(jid, nickResolver_->jidToNick(jid), activity, 0, type, path, isMUC, nick); + foreach(ChatListWindow::Chat chat, recentChats) { + chat.statusType = StatusShow::None; + chat = updateChatStatusAndAvatarHelper(chat); prependRecent(chat); } + } handleUnreadCountChanged(NULL); } @@ -260,5 +369,5 @@ void ChatsManager::handleMUCBookmarkAdded(const MUCBookmark& bookmark) { std::map<JID, MUCController*>::iterator it = mucControllers_.find(bookmark.getRoom()); if (it == mucControllers_.end() && bookmark.getAutojoin()) { - handleJoinMUCRequest(bookmark.getRoom(), bookmark.getPassword(), bookmark.getNick(), false, false); + handleJoinMUCRequest(bookmark.getRoom(), bookmark.getPassword(), bookmark.getNick(), false, false, false ); } chatListWindow_->addMUCBookmark(bookmark); @@ -269,5 +378,5 @@ void ChatsManager::handleMUCBookmarkRemoved(const MUCBookmark& bookmark) { } -ChatListWindow::Chat ChatsManager::createChatListChatItem(const JID& jid, const std::string& activity) { +ChatListWindow::Chat ChatsManager::createChatListChatItem(const JID& jid, const std::string& activity, bool privateMessage) { int unreadCount = 0; if (mucRegistry_->isMUC(jid)) { @@ -275,4 +384,5 @@ ChatListWindow::Chat ChatsManager::createChatListChatItem(const JID& jid, const StatusShow::Type type = StatusShow::None; std::string nick = ""; + std::string password = ""; if (controller) { unreadCount = controller->getUnreadCount(); @@ -281,7 +391,17 @@ ChatListWindow::Chat ChatsManager::createChatListChatItem(const JID& jid, const } nick = controller->getNick(); + + if (controller->getPassword()) { + password = *controller->getPassword(); } - return ChatListWindow::Chat(jid, jid.toString(), activity, unreadCount, type, boost::filesystem::path(), true, nick); + if (controller->isImpromptu()) { + ChatListWindow::Chat chat = ChatListWindow::Chat(jid, jid.toString(), activity, unreadCount, type, boost::filesystem::path(), true, privateMessage, nick, password); + std::map<std::string, JID> participants = controller->getParticipantJIDs(); + chat.impromptuJIDs = participants; + return chat; + } + } + return ChatListWindow::Chat(jid, jid.toString(), activity, unreadCount, type, boost::filesystem::path(), true, privateMessage, nick, password); } else { ChatController* controller = getChatControllerIfExists(jid, false); @@ -293,14 +413,11 @@ ChatListWindow::Chat ChatsManager::createChatListChatItem(const JID& jid, const StatusShow::Type type = presence ? presence->getShow() : StatusShow::None; boost::filesystem::path avatarPath = avatarManager_ ? avatarManager_->getAvatarPath(bareishJID) : boost::filesystem::path(); - return ChatListWindow::Chat(bareishJID, nickResolver_->jidToNick(bareishJID), activity, unreadCount, type, avatarPath, false); + return ChatListWindow::Chat(bareishJID, nickResolver_->jidToNick(bareishJID), activity, unreadCount, type, avatarPath, false, privateMessage); } } void ChatsManager::handleChatActivity(const JID& jid, const std::string& activity, bool isMUC) { - if (mucRegistry_->isMUC(jid.toBare()) && !isMUC) { - /* Don't include PMs in MUC rooms.*/ - return; - } - ChatListWindow::Chat chat = createChatListChatItem(jid, activity); + const bool privateMessage = mucRegistry_->isMUC(jid.toBare()) && !isMUC; + ChatListWindow::Chat chat = createChatListChatItem(jid, activity, privateMessage); /* FIXME: handle nick changes */ appendRecent(chat); @@ -309,4 +426,9 @@ void ChatsManager::handleChatActivity(const JID& jid, const std::string& activit } +void ChatsManager::handleChatClosed(const JID& /*jid*/) { + cleanupPrivateMessageRecents(); + chatListWindow_->setRecents(recentChats_); +} + void ChatsManager::handleUnreadCountChanged(ChatControllerBase* controller) { int unreadTotal = 0; @@ -332,12 +454,49 @@ void ChatsManager::handleUnreadCountChanged(ChatControllerBase* controller) { } -void ChatsManager::appendRecent(const ChatListWindow::Chat& chat) { +boost::optional<ChatListWindow::Chat> ChatsManager::removeExistingChat(const ChatListWindow::Chat& chat) { + std::list<ChatListWindow::Chat>::iterator result = std::find(recentChats_.begin(), recentChats_.end(), chat); + if (result != recentChats_.end()) { + ChatListWindow::Chat existingChat = *result; recentChats_.erase(std::remove(recentChats_.begin(), recentChats_.end(), chat), recentChats_.end()); - recentChats_.push_front(chat); + return boost::optional<ChatListWindow::Chat>(existingChat); + } else { + return boost::optional<ChatListWindow::Chat>(); + } +} + +void ChatsManager::cleanupPrivateMessageRecents() { + /* if we leave a MUC and close a PM, remove it's recent chat entry */ + const std::list<ChatListWindow::Chat> chats = recentChats_; + foreach (const ChatListWindow::Chat& chat, chats) { + if (chat.isPrivateMessage) { + typedef std::map<JID, MUCController*> ControllerMap; + ControllerMap::iterator muc = mucControllers_.find(chat.jid.toBare()); + if (muc == mucControllers_.end() || !muc->second->isJoined()) { + ChatController* chatController = getChatControllerIfExists(chat.jid); + if (!chatController || !chatController->hasOpenWindow()) { + removeExistingChat(chat); + break; + } + } + } + } +} + +void ChatsManager::appendRecent(const ChatListWindow::Chat& chat) { + boost::optional<ChatListWindow::Chat> oldChat = removeExistingChat(chat); + ChatListWindow::Chat mergedChat = chat; + if (oldChat && !oldChat->impromptuJIDs.empty()) { + mergedChat.impromptuJIDs.insert(oldChat->impromptuJIDs.begin(), oldChat->impromptuJIDs.end()); + } + recentChats_.push_front(mergedChat); } void ChatsManager::prependRecent(const ChatListWindow::Chat& chat) { - recentChats_.erase(std::remove(recentChats_.begin(), recentChats_.end(), chat), recentChats_.end()); - recentChats_.push_back(chat); + boost::optional<ChatListWindow::Chat> oldChat = removeExistingChat(chat); + ChatListWindow::Chat mergedChat = chat; + if (oldChat && !oldChat->impromptuJIDs.empty()) { + mergedChat.impromptuJIDs.insert(oldChat->impromptuJIDs.begin(), oldChat->impromptuJIDs.end()); + } + recentChats_.push_back(mergedChat); } @@ -349,13 +508,13 @@ void ChatsManager::handleUserLeftMUC(MUCController* mucController) { if (chat.isMUC && chat.jid == (*it).first) { chat.statusType = StatusShow::None; - chatListWindow_->setRecents(recentChats_); - break; } } mucControllers_.erase(it); delete mucController; - return; + break; } } + cleanupPrivateMessageRecents(); + chatListWindow_->setRecents(recentChats_); } @@ -367,4 +526,25 @@ void ChatsManager::handleSettingChanged(const std::string& settingPath) { } +void ChatsManager::finalizeImpromptuJoin(MUC::ref muc, const std::vector<JID>& jidsToInvite, const std::string& reason, const boost::optional<JID>& reuseChatJID) { + // send impromptu invites for the new MUC + std::vector<JID> missingJIDsToInvite = jidsToInvite; + + typedef std::pair<std::string, MUCOccupant> StringMUCOccupantPair; + std::map<std::string, MUCOccupant> occupants = muc->getOccupants(); + foreach(StringMUCOccupantPair occupant, occupants) { + boost::optional<JID> realJID = occupant.second.getRealJID(); + if (realJID) { + missingJIDsToInvite.erase(std::remove(missingJIDsToInvite.begin(), missingJIDsToInvite.end(), realJID->toBare()), missingJIDsToInvite.end()); + } + } + + if (reuseChatJID) { + muc->invitePerson(reuseChatJID.get(), reason, true, true); + } + foreach(const JID& jid, missingJIDsToInvite) { + muc->invitePerson(jid, reason, true); + } +} + void ChatsManager::handleUIEvent(boost::shared_ptr<UIEvent> event) { boost::shared_ptr<RequestChatUIEvent> chatEvent = boost::dynamic_pointer_cast<RequestChatUIEvent>(event); @@ -384,4 +564,15 @@ void ChatsManager::handleUIEvent(boost::shared_ptr<UIEvent> event) { } + boost::shared_ptr<CreateImpromptuMUCUIEvent> createImpromptuMUCEvent = boost::dynamic_pointer_cast<CreateImpromptuMUCUIEvent>(event); + if (createImpromptuMUCEvent) { + assert(!localMUCServiceJID_.toString().empty()); + // create new muc + JID roomJID = createImpromptuMUCEvent->getRoomJID().toString().empty() ? JID(idGenerator_.generateID(), localMUCServiceJID_) : createImpromptuMUCEvent->getRoomJID(); + + // join muc + MUC::ref muc = handleJoinMUCRequest(roomJID, boost::optional<std::string>(), nickResolver_->jidToNick(jid_), false, true, true); + mucControllers_[roomJID]->onImpromptuConfigCompleted.connect(boost::bind(&ChatsManager::finalizeImpromptuJoin, this, muc, createImpromptuMUCEvent->getJIDs(), createImpromptuMUCEvent->getReason(), boost::optional<JID>())); + mucControllers_[roomJID]->activateChatWindow(); + } boost::shared_ptr<EditMUCBookmarkUIEvent> editMUCBookmarkEvent = boost::dynamic_pointer_cast<EditMUCBookmarkUIEvent>(event); @@ -390,5 +581,5 @@ void ChatsManager::handleUIEvent(boost::shared_ptr<UIEvent> event) { } else if (JoinMUCUIEvent::ref joinEvent = boost::dynamic_pointer_cast<JoinMUCUIEvent>(event)) { - handleJoinMUCRequest(joinEvent->getJID(), joinEvent->getPassword(), joinEvent->getNick(), joinEvent->getShouldJoinAutomatically(), joinEvent->getCreateAsReservedRoomIfNew()); + handleJoinMUCRequest(joinEvent->getJID(), joinEvent->getPassword(), joinEvent->getNick(), joinEvent->getShouldJoinAutomatically(), joinEvent->getCreateAsReservedRoomIfNew(), joinEvent->isImpromptu()); mucControllers_[joinEvent->getJID()]->activateChatWindow(); } @@ -412,4 +603,20 @@ void ChatsManager::markAllRecentsOffline() { } +void ChatsManager::handleTransformChatToMUC(ChatController* chatController, ChatWindow* chatWindow, const std::vector<JID>& jidsToInvite, const std::string& reason) { + JID reuseChatInvite = chatController->getToJID(); + chatControllers_.erase(chatController->getToJID()); + delete chatController; + + // join new impromptu muc + assert(!localMUCServiceJID_.toString().empty()); + + // create new muc + JID roomJID = JID(idGenerator_.generateID(), localMUCServiceJID_); + + // join muc + MUC::ref muc = handleJoinMUCRequest(roomJID, boost::optional<std::string>(), nickResolver_->jidToNick(jid_), false, true, true, chatWindow); + mucControllers_[roomJID]->onImpromptuConfigCompleted.connect(boost::bind(&ChatsManager::finalizeImpromptuJoin, this, muc, jidsToInvite, reason, boost::optional<JID>(reuseChatInvite))); +} + /** * If a resource goes offline, release bound chatdialog to that resource. @@ -489,4 +696,9 @@ void ChatsManager::setOnline(bool enabled) { } else { setupBookmarks(); + localMUCServiceFinderWalker_ = boost::make_shared<DiscoServiceWalker>(jid_.getDomain(), iqRouter_); + localMUCServiceFinderWalker_->onServiceFound.connect(boost::bind(&ChatsManager::handleLocalServiceFound, this, _1, _2)); + localMUCServiceFinderWalker_->onWalkAborted.connect(boost::bind(&ChatsManager::handleLocalServiceWalkFinished, this)); + localMUCServiceFinderWalker_->onWalkComplete.connect(boost::bind(&ChatsManager::handleLocalServiceWalkFinished, this)); + localMUCServiceFinderWalker_->beginWalk(); } @@ -513,10 +725,14 @@ ChatController* ChatsManager::getChatControllerOrFindAnother(const JID &contact) ChatController* ChatsManager::createNewChatController(const JID& contact) { assert(chatControllers_.find(contact) == chatControllers_.end()); - ChatController* controller = new ChatController(jid_, stanzaChannel_, iqRouter_, chatWindowFactory_, contact, nickResolver_, presenceOracle_, avatarManager_, mucRegistry_->isMUC(contact.toBare()), useDelayForLatency_, uiEventStream_, eventController_, timerFactory_, entityCapsProvider_, userWantsReceipts_, settings_); + boost::shared_ptr<ChatMessageParser> chatMessageParser = boost::make_shared<ChatMessageParser>(emoticons_, highlightManager_->getRules(), false); /* a message parser that knows this is a chat (not a room/MUC) */ + ChatController* controller = new ChatController(jid_, stanzaChannel_, iqRouter_, chatWindowFactory_, contact, nickResolver_, presenceOracle_, avatarManager_, mucRegistry_->isMUC(contact.toBare()), useDelayForLatency_, uiEventStream_, eventController_, timerFactory_, entityCapsProvider_, userWantsReceipts_, settings_, historyController_, mucRegistry_, highlightManager_, clientBlockListManager_, chatMessageParser, autoAcceptMUCInviteDecider_); chatControllers_[contact] = controller; controller->setAvailableServerFeatures(serverDiscoInfo_); controller->onActivity.connect(boost::bind(&ChatsManager::handleChatActivity, this, contact, _1, false)); + controller->onWindowClosed.connect(boost::bind(&ChatsManager::handleChatClosed, this, contact)); controller->onUnreadCountChanged.connect(boost::bind(&ChatsManager::handleUnreadCountChanged, this, controller)); + controller->onConvertToMUC.connect(boost::bind(&ChatsManager::handleTransformChatToMUC, this, controller, _1, _2, _3)); updatePresenceReceivingStateOnChatController(contact); + controller->setCanStartImpromptuChats(!localMUCServiceJID_.toString().empty()); return controller; } @@ -545,5 +761,4 @@ ChatController* ChatsManager::getChatControllerIfExists(const JID &contact, bool return pair.second; } - } } @@ -560,8 +775,9 @@ void ChatsManager::rebindControllerJID(const JID& from, const JID& to) { } -void ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::optional<std::string>& password, const boost::optional<std::string>& nickMaybe, bool addAutoJoin, bool createAsReservedIfNew) { +MUC::ref ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::optional<std::string>& password, const boost::optional<std::string>& nickMaybe, bool addAutoJoin, bool createAsReservedIfNew, bool isImpromptu, ChatWindow* reuseChatwindow) { + MUC::ref muc; if (!stanzaChannel_->isAvailable()) { /* This is potentially not the optimal solution, but it will avoid consistency issues.*/ - return; + return muc; } if (addAutoJoin) { @@ -582,13 +798,31 @@ void ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::optional } else { std::string nick = (nickMaybe && !(*nickMaybe).empty()) ? nickMaybe.get() : nickResolver_->jidToNick(jid_); - MUC::ref muc = mucManager->createMUC(mucJID); + muc = mucManager->createMUC(mucJID); if (createAsReservedIfNew) { muc->setCreateAsReservedIfNew(); } - MUCController* controller = new MUCController(jid_, muc, password, nick, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory_, eventController_, entityCapsProvider_); + if (isImpromptu) { + muc->setCreateAsReservedIfNew(); + } + + MUCController* controller = NULL; + SingleChatWindowFactoryAdapter* chatWindowFactoryAdapter = NULL; + if (reuseChatwindow) { + chatWindowFactoryAdapter = new SingleChatWindowFactoryAdapter(reuseChatwindow); + } + boost::shared_ptr<ChatMessageParser> chatMessageParser = boost::make_shared<ChatMessageParser>(emoticons_, highlightManager_->getRules(), true); /* a message parser that knows this is a room/MUC (not a chat) */ + controller = new MUCController(jid_, muc, password, nick, stanzaChannel_, iqRouter_, reuseChatwindow ? chatWindowFactoryAdapter : chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory_, eventController_, entityCapsProvider_, roster_, historyController_, mucRegistry_, highlightManager_, chatMessageParser, isImpromptu, autoAcceptMUCInviteDecider_, vcardManager_); + if (chatWindowFactoryAdapter) { + /* The adapters are only passed to chat windows, which are deleted in their + * controllers' dtor, which are deleted in ChatManager's dtor. The adapters + * are also deleted there.*/ + chatWindowFactoryAdapters_[controller] = chatWindowFactoryAdapter; + } + mucControllers_[mucJID] = controller; controller->setAvailableServerFeatures(serverDiscoInfo_); controller->onUserLeft.connect(boost::bind(&ChatsManager::handleUserLeftMUC, this, controller)); controller->onUserJoined.connect(boost::bind(&ChatsManager::handleChatActivity, this, mucJID.toBare(), "", true)); + controller->onUserNicknameChanged.connect(boost::bind(&ChatsManager::handleUserNicknameChanged, this, controller, _1, _2)); controller->onActivity.connect(boost::bind(&ChatsManager::handleChatActivity, this, mucJID.toBare(), _1, true)); controller->onUnreadCountChanged.connect(boost::bind(&ChatsManager::handleUnreadCountChanged, this, controller)); @@ -597,4 +831,5 @@ void ChatsManager::handleJoinMUCRequest(const JID &mucJID, const boost::optional mucControllers_[mucJID]->showChatWindow(); + return muc; } @@ -603,8 +838,31 @@ void ChatsManager::handleSearchMUCRequest() { } +void ChatsManager::handleUserNicknameChanged(MUCController* mucController, const std::string& oldNickname, const std::string& newNickname) { + JID oldMUCChatJID = mucController->getToJID().withResource(oldNickname); + JID newMUCChatJID = mucController->getToJID().withResource(newNickname); + + SWIFT_LOG(debug) << "nickname change in " << mucController->getToJID().toString() << " from " << oldNickname << " to " << newNickname << std::endl; + + // get current chat controller + ChatController *chatController = getChatControllerIfExists(oldMUCChatJID); + if (chatController) { + // adjust chat controller + chatController->setToJID(newMUCChatJID); + nickResolver_->onNickChanged(newMUCChatJID, oldNickname); + chatControllers_.erase(oldMUCChatJID); + chatControllers_[newMUCChatJID] = chatController; + + chatController->onActivity.disconnect(boost::bind(&ChatsManager::handleChatActivity, this, oldMUCChatJID, _1, false)); + chatController->onActivity.connect(boost::bind(&ChatsManager::handleChatActivity, this, newMUCChatJID, _1, false)); + /*for (std::list<ChatListWindow::Chat>::iterator i = recentChats_.begin(); i != recentChats_.end(); i++) { + if (i->jid == + }*/ + } +} + void ChatsManager::handleIncomingMessage(boost::shared_ptr<Message> message) { JID jid = message->getFrom(); boost::shared_ptr<MessageEvent> event(new MessageEvent(message)); - bool isInvite = message->getPayload<MUCInvitationPayload>(); + bool isInvite = !!message->getPayload<MUCInvitationPayload>(); bool isMediatedInvite = (message->getPayload<MUCUserPayload>() && message->getPayload<MUCUserPayload>()->getInvite()); if (isMediatedInvite) { @@ -628,7 +886,34 @@ void ChatsManager::handleIncomingMessage(boost::shared_ptr<Message> message) { } + // check for impromptu invite to potentially auto-accept + MUCInvitationPayload::ref invite = message->getPayload<MUCInvitationPayload>(); + if (invite && autoAcceptMUCInviteDecider_->isAutoAcceptedInvite(message->getFrom(), invite)) { + if (invite->getIsContinuation()) { + // check for existing chat controller for the from JID + ChatController* controller = getChatControllerIfExists(jid); + if (controller) { + ChatWindow* window = controller->detachChatWindow(); + chatControllers_.erase(jid); + delete controller; + handleJoinMUCRequest(invite->getJID(), boost::optional<std::string>(), boost::optional<std::string>(), false, false, true, window); + return; + } + } else { + handleJoinMUCRequest(invite->getJID(), boost::optional<std::string>(), boost::optional<std::string>(), false, false, true); + return; + } + } + //if not a mucroom + if (!event->isReadable() && !isInvite && !isMediatedInvite) { + /* Only route such messages if a window exists, don't open new windows for them.*/ + ChatController* controller = getChatControllerIfExists(jid); + if (controller) { + controller->handleIncomingMessage(event); + } + } else { getChatControllerOrCreate(jid)->handleIncomingMessage(event); } +} void ChatsManager::handleMUCSelectedAfterSearch(const JID& muc) { @@ -648,6 +933,37 @@ void ChatsManager::handleNewFileTransferController(FileTransferController* ftc) } +void ChatsManager::handleWhiteboardSessionRequest(const JID& contact, bool senderIsSelf) { + ChatController* chatController = getChatControllerOrCreate(contact); + chatController->handleWhiteboardSessionRequest(senderIsSelf); + chatController->activateChatWindow(); +} + +void ChatsManager::handleWhiteboardStateChange(const JID& contact, const ChatWindow::WhiteboardSessionState state) { + ChatController* chatController = getChatControllerOrCreate(contact); + chatController->handleWhiteboardStateChange(state); + chatController->activateChatWindow(); + if (state == ChatWindow::WhiteboardAccepted) { + boost::filesystem::path path; + JID bareJID = contact.toBare(); + if (avatarManager_) { + path = avatarManager_->getAvatarPath(bareJID); + } + ChatListWindow::Chat chat(bareJID, nickResolver_->jidToNick(bareJID), "", 0, StatusShow::None, path, false); + chatListWindow_->addWhiteboardSession(chat); + } else { + chatListWindow_->removeWhiteboardSession(contact.toBare()); + } +} + void ChatsManager::handleRecentActivated(const ChatListWindow::Chat& chat) { - if (chat.isMUC) { + if (chat.isMUC && !chat.impromptuJIDs.empty()) { + typedef std::pair<std::string, JID> StringJIDPair; + std::vector<JID> inviteJIDs; + foreach(StringJIDPair pair, chat.impromptuJIDs) { + inviteJIDs.push_back(pair.second); + } + uiEventStream_->send(boost::make_shared<CreateImpromptuMUCUIEvent>(inviteJIDs, chat.jid, "")); + } + else if (chat.isMUC) { /* FIXME: This means that recents requiring passwords will just flat-out not work */ uiEventStream_->send(boost::make_shared<JoinMUCUIEvent>(chat.jid, boost::optional<std::string>(), chat.nick)); @@ -658,3 +974,56 @@ void ChatsManager::handleRecentActivated(const ChatListWindow::Chat& chat) { } +void ChatsManager::handleLocalServiceFound(const JID& service, boost::shared_ptr<DiscoInfo> info) { + foreach (DiscoInfo::Identity identity, info->getIdentities()) { + if ((identity.getCategory() == "directory" + && identity.getType() == "chatroom") + || (identity.getCategory() == "conference" + && identity.getType() == "text")) { + localMUCServiceJID_ = service; + localMUCServiceFinderWalker_->endWalk(); + SWIFT_LOG(debug) << "Use following MUC service for impromptu chats: " << localMUCServiceJID_ << std::endl; + break; + } + } +} + +void ChatsManager::handleLocalServiceWalkFinished() { + onImpromptuMUCServiceDiscovered(!localMUCServiceJID_.toString().empty()); +} + +std::vector<ChatListWindow::Chat> ChatsManager::getRecentChats() const { + return std::vector<ChatListWindow::Chat>(recentChats_.begin(), recentChats_.end()); +} + +std::vector<Contact::ref> Swift::ChatsManager::getContacts(bool withMUCNicks) { + std::vector<Contact::ref> result; + foreach (ChatListWindow::Chat chat, recentChats_) { + if (!chat.isMUC) { + result.push_back(boost::make_shared<Contact>(chat.chatName.empty() ? chat.jid.toString() : chat.chatName, chat.jid, chat.statusType, chat.avatarPath)); + } + } + if (withMUCNicks) { + /* collect MUC nicks */ + typedef std::map<JID, MUCController*>::value_type Item; + foreach (const Item& item, mucControllers_) { + JID mucJID = item.second->getToJID(); + std::map<std::string, JID> participants = item.second->getParticipantJIDs(); + typedef std::map<std::string, JID>::value_type ParticipantType; + foreach (const ParticipantType& participant, participants) { + const JID nickJID = JID(mucJID.getNode(), mucJID.getDomain(), participant.first); + Presence::ref presence = presenceOracle_->getLastPresence(nickJID); + const boost::filesystem::path avatar = avatarManager_->getAvatarPath(nickJID); + result.push_back(boost::make_shared<Contact>(participant.first, JID(), presence->getShow(), avatar)); + } + } + } + return result; +} + +ChatsManager::SingleChatWindowFactoryAdapter::SingleChatWindowFactoryAdapter(ChatWindow* chatWindow) : chatWindow_(chatWindow) {} +ChatsManager::SingleChatWindowFactoryAdapter::~SingleChatWindowFactoryAdapter() {} +ChatWindow* ChatsManager::SingleChatWindowFactoryAdapter::createChatWindow(const JID &, UIEventStream*) { + return chatWindow_; +} + } diff --git a/Swift/Controllers/Chat/ChatsManager.h b/Swift/Controllers/Chat/ChatsManager.h index a8c69c4..82daf67 100644 --- a/Swift/Controllers/Chat/ChatsManager.h +++ b/Swift/Controllers/Chat/ChatsManager.h @@ -1,4 +1,4 @@ /* - * Copyright (c) 2010-2011 Kevin Smith + * Copyright (c) 2010-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. @@ -8,8 +8,9 @@ #include <map> +#include <string> #include <boost/shared_ptr.hpp> -#include <string> +#include <Swiften/Base/IDGenerator.h> #include <Swiften/Elements/DiscoInfo.h> #include <Swiften/Elements/Message.h> @@ -17,7 +18,14 @@ #include <Swiften/JID/JID.h> #include <Swiften/MUC/MUCRegistry.h> +#include <Swiften/MUC/MUCBookmark.h> +#include <Swiften/MUC/MUC.h> + + +#include <Swift/Controllers/ContactProvider.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/UIInterfaces/ChatListWindow.h> -#include <Swiften/MUC/MUCBookmark.h> +#include <Swift/Controllers/UIInterfaces/ChatWindow.h> +#include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> + namespace Swift { @@ -27,5 +35,4 @@ namespace Swift { class MUCController; class MUCManager; - class ChatWindowFactory; class JoinMUCWindow; class JoinMUCWindowFactory; @@ -48,8 +55,17 @@ namespace Swift { class XMPPRoster; class SettingsProvider; + class WhiteboardManager; + class HistoryController; + class HighlightManager; + class ClientBlockListManager; + class ChatMessageParser; + class DiscoServiceWalker; + class AutoAcceptMUCInviteDecider; + class UserSearchController; + class VCardManager; - class ChatsManager { + class ChatsManager : public ContactProvider { public: - ChatsManager(JID jid, StanzaChannel* stanzaChannel, IQRouter* iqRouter, EventController* eventController, ChatWindowFactory* chatWindowFactory, JoinMUCWindowFactory* joinMUCWindowFactory, NickResolver* nickResolver, PresenceOracle* presenceOracle, PresenceSender* presenceSender, UIEventStream* uiEventStream, ChatListWindowFactory* chatListWindowFactory, bool useDelayForLatency, TimerFactory* timerFactory, MUCRegistry* mucRegistry, EntityCapsProvider* entityCapsProvider, MUCManager* mucManager, MUCSearchWindowFactory* mucSearchWindowFactory, ProfileSettingsProvider* profileSettings, FileTransferOverview* ftOverview, XMPPRoster* roster, bool eagleMode, SettingsProvider* settings); + ChatsManager(JID jid, StanzaChannel* stanzaChannel, IQRouter* iqRouter, EventController* eventController, ChatWindowFactory* chatWindowFactory, JoinMUCWindowFactory* joinMUCWindowFactory, NickResolver* nickResolver, PresenceOracle* presenceOracle, PresenceSender* presenceSender, UIEventStream* uiEventStream, ChatListWindowFactory* chatListWindowFactory, bool useDelayForLatency, TimerFactory* timerFactory, MUCRegistry* mucRegistry, EntityCapsProvider* entityCapsProvider, MUCManager* mucManager, MUCSearchWindowFactory* mucSearchWindowFactory, ProfileSettingsProvider* profileSettings, FileTransferOverview* ftOverview, XMPPRoster* roster, bool eagleMode, SettingsProvider* settings, HistoryController* historyController_, WhiteboardManager* whiteboardManager, HighlightManager* highlightManager, ClientBlockListManager* clientBlockListManager, const std::map<std::string, std::string>& emoticons, UserSearchController* inviteUserSearchController, VCardManager* vcardManager); virtual ~ChatsManager(); void setAvatarManager(AvatarManager* avatarManager); @@ -57,9 +73,25 @@ namespace Swift { void setServerDiscoInfo(boost::shared_ptr<DiscoInfo> info); void handleIncomingMessage(boost::shared_ptr<Message> message); + std::vector<ChatListWindow::Chat> getRecentChats() const; + virtual std::vector<Contact::ref> getContacts(bool withMUCNicks); + + boost::signal<void (bool supportsImpromptu)> onImpromptuMUCServiceDiscovered; private: - ChatListWindow::Chat createChatListChatItem(const JID& jid, const std::string& activity); + class SingleChatWindowFactoryAdapter : public ChatWindowFactory { + public: + SingleChatWindowFactoryAdapter(ChatWindow* chatWindow); + virtual ~SingleChatWindowFactoryAdapter(); + virtual ChatWindow* createChatWindow(const JID &, UIEventStream*); + + private: + ChatWindow* chatWindow_; + }; + + private: + ChatListWindow::Chat createChatListChatItem(const JID& jid, const std::string& activity, bool privateMessage); void handleChatRequest(const std::string& contact); - void handleJoinMUCRequest(const JID& muc, const boost::optional<std::string>& password, const boost::optional<std::string>& nick, bool addAutoJoin, bool createAsReservedIfNew); + void finalizeImpromptuJoin(MUC::ref muc, const std::vector<JID>& jidsToInvite, const std::string& reason, const boost::optional<JID>& reuseChatJID = boost::optional<JID>()); + MUC::ref handleJoinMUCRequest(const JID& muc, const boost::optional<std::string>& password, const boost::optional<std::string>& nick, bool addAutoJoin, bool createAsReservedIfNew, bool isImpromptu, ChatWindow* reuseChatwindow = 0); void handleSearchMUCRequest(); void handleMUCSelectedAfterSearch(const JID&); @@ -70,7 +102,13 @@ namespace Swift { void handleMUCBookmarkRemoved(const MUCBookmark& bookmark); void handleUserLeftMUC(MUCController* mucController); + void handleUserNicknameChanged(MUCController* mucController, const std::string& oldNickname, const std::string& newNickname); void handleBookmarksReady(); void handleChatActivity(const JID& jid, const std::string& activity, bool isMUC); + void handleChatClosed(const JID& jid); void handleNewFileTransferController(FileTransferController*); + void handleWhiteboardSessionRequest(const JID& contact, bool senderIsSelf); + void handleWhiteboardStateChange(const JID& contact, const ChatWindow::WhiteboardSessionState state); + boost::optional<ChatListWindow::Chat> removeExistingChat(const ChatListWindow::Chat& chat); + void cleanupPrivateMessageRecents(); void appendRecent(const ChatListWindow::Chat& chat); void prependRecent(const ChatListWindow::Chat& chat); @@ -90,6 +128,12 @@ namespace Swift { void handleSettingChanged(const std::string& settingPath); void markAllRecentsOffline(); + void handleTransformChatToMUC(ChatController* chatController, ChatWindow* chatWindow, const std::vector<JID>& jidsToInvite, const std::string& reason); + + void handleLocalServiceFound(const JID& service, boost::shared_ptr<DiscoInfo> info); + void handleLocalServiceWalkFinished(); void updatePresenceReceivingStateOnChatController(const JID&); + ChatListWindow::Chat updateChatStatusAndAvatarHelper(const ChatListWindow::Chat& chat) const; + ChatController* getChatControllerOrFindAnother(const JID &contact); @@ -101,4 +145,5 @@ namespace Swift { std::map<JID, MUCController*> mucControllers_; std::map<JID, ChatController*> chatControllers_; + std::map<ChatControllerBase*, SingleChatWindowFactoryAdapter*> chatWindowFactoryAdapters_; EventController* eventController_; JID jid_; @@ -130,4 +175,15 @@ namespace Swift { bool userWantsReceipts_; SettingsProvider* settings_; + HistoryController* historyController_; + WhiteboardManager* whiteboardManager_; + HighlightManager* highlightManager_; + std::map<std::string, std::string> emoticons_; + ClientBlockListManager* clientBlockListManager_; + JID localMUCServiceJID_; + boost::shared_ptr<DiscoServiceWalker> localMUCServiceFinderWalker_; + AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider_; + UserSearchController* inviteUserSearchController_; + IDGenerator idGenerator_; + VCardManager* vcardManager_; }; } diff --git a/Swift/Controllers/Chat/MUCController.cpp b/Swift/Controllers/Chat/MUCController.cpp index e4209f4..fe90c60 100644 --- a/Swift/Controllers/Chat/MUCController.cpp +++ b/Swift/Controllers/Chat/MUCController.cpp @@ -1,4 +1,4 @@ /* - * Copyright (c) 2010 Kevin Smith + * Copyright (c) 2010-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. @@ -11,28 +11,38 @@ #include <boost/algorithm/string.hpp> -#include <Swift/Controllers/Intl.h> +#include <Swiften/Avatars/AvatarManager.h> +#include <Swiften/Base/foreach.h> +#include <Swiften/Base/foreach.h> #include <Swiften/Base/format.h> +#include <Swiften/Base/Log.h> +#include <Swiften/Client/StanzaChannel.h> +#include <Swiften/Disco/EntityCapsProvider.h> +#include <Swiften/Elements/Delay.h> +#include <Swiften/MUC/MUC.h> #include <Swiften/Network/Timer.h> #include <Swiften/Network/TimerFactory.h> -#include <Swiften/Base/foreach.h> +#include <Swiften/Roster/XMPPRoster.h> + #include <SwifTools/TabComplete.h> -#include <Swiften/Base/foreach.h> -#include <Swift/Controllers/XMPPEvents/EventController.h> -#include <Swift/Controllers/UIInterfaces/ChatWindow.h> -#include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> -#include <Swift/Controllers/UIEvents/UIEventStream.h> -#include <Swift/Controllers/UIEvents/RequestChatUIEvent.h> -#include <Swift/Controllers/UIEvents/RequestAddUserDialogUIEvent.h> -#include <Swift/Controllers/Roster/GroupRosterItem.h> + +#include <Swift/Controllers/Chat/ChatMessageParser.h> +#include <Swift/Controllers/Highlighter.h> +#include <Swift/Controllers/Intl.h> #include <Swift/Controllers/Roster/ContactRosterItem.h> -#include <Swiften/Avatars/AvatarManager.h> -#include <Swiften/Elements/Delay.h> -#include <Swiften/MUC/MUC.h> -#include <Swiften/Client/StanzaChannel.h> +#include <Swift/Controllers/Roster/GroupRosterItem.h> +#include <Swift/Controllers/Roster/ItemOperations/SetAvatar.h> +#include <Swift/Controllers/Roster/ItemOperations/SetPresence.h> +#include <Swift/Controllers/Roster/ItemOperations/SetMUC.h> #include <Swift/Controllers/Roster/Roster.h> -#include <Swift/Controllers/Roster/SetAvatar.h> -#include <Swift/Controllers/Roster/SetPresence.h> -#include <Swiften/Disco/EntityCapsProvider.h> - +#include <Swift/Controllers/Roster/RosterVCardProvider.h> +#include <Swift/Controllers/UIEvents/InviteToMUCUIEvent.h> +#include <Swift/Controllers/UIEvents/RequestAddUserDialogUIEvent.h> +#include <Swift/Controllers/UIEvents/RequestChatUIEvent.h> +#include <Swift/Controllers/UIEvents/RequestInviteToMUCUIEvent.h> +#include <Swift/Controllers/UIEvents/ShowProfileForRosterItemUIEvent.h> +#include <Swift/Controllers/UIEvents/UIEventStream.h> +#include <Swift/Controllers/UIInterfaces/ChatWindow.h> +#include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> +#include <Swift/Controllers/XMPPEvents/EventController.h> #define MUC_JOIN_WARNING_TIMEOUT_MILLISECONDS 60000 @@ -57,6 +67,14 @@ MUCController::MUCController ( TimerFactory* timerFactory, EventController* eventController, - EntityCapsProvider* entityCapsProvider) : - ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, muc->getJID(), presenceOracle, avatarManager, useDelayForLatency, uiEventStream, eventController, timerFactory, entityCapsProvider), muc_(muc), nick_(nick), desiredNick_(nick), password_(password) { + EntityCapsProvider* entityCapsProvider, + XMPPRoster* roster, + HistoryController* historyController, + MUCRegistry* mucRegistry, + HighlightManager* highlightManager, + boost::shared_ptr<ChatMessageParser> chatMessageParser, + bool isImpromptu, + AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, + VCardManager* vcardManager) : + ChatControllerBase(self, stanzaChannel, iqRouter, chatWindowFactory, muc->getJID(), presenceOracle, avatarManager, useDelayForLatency, uiEventStream, eventController, timerFactory, entityCapsProvider, historyController, mucRegistry, highlightManager, chatMessageParser, autoAcceptMUCInviteDecider), muc_(muc), nick_(nick), desiredNick_(nick), password_(password), renameCounter_(0), isImpromptu_(isImpromptu), isImpromptuAlreadyConfigured_(false) { parting_ = true; joined_ = false; @@ -65,18 +83,20 @@ MUCController::MUCController ( doneGettingHistory_ = false; events_ = uiEventStream; + xmppRoster_ = roster; roster_ = new Roster(false, true); + rosterVCardProvider_ = new RosterVCardProvider(roster_, vcardManager, JID::WithResource); completer_ = new TabComplete(); chatWindow_->setRosterModel(roster_); chatWindow_->setTabComplete(completer_); - chatWindow_->setName(muc->getJID().getNode()); chatWindow_->onClosed.connect(boost::bind(&MUCController::handleWindowClosed, this)); chatWindow_->onOccupantSelectionChanged.connect(boost::bind(&MUCController::handleWindowOccupantSelectionChanged, this, _1)); chatWindow_->onOccupantActionSelected.connect(boost::bind(&MUCController::handleActionRequestedOnOccupant, this, _1, _2)); chatWindow_->onChangeSubjectRequest.connect(boost::bind(&MUCController::handleChangeSubjectRequest, this, _1)); + chatWindow_->onBookmarkRequest.connect(boost::bind(&MUCController::handleBookmarkRequest, this)); chatWindow_->onConfigureRequest.connect(boost::bind(&MUCController::handleConfigureRequest, this, _1)); chatWindow_->onConfigurationFormCancelled.connect(boost::bind(&MUCController::handleConfigurationCancelled, this)); chatWindow_->onDestroyRequest.connect(boost::bind(&MUCController::handleDestroyRoomRequest, this)); - chatWindow_->onInvitePersonToThisMUCRequest.connect(boost::bind(&MUCController::handleInvitePersonToThisMUCRequest, this, _1, _2)); + chatWindow_->onInviteToChat.connect(boost::bind(&MUCController::handleInvitePersonToThisMUCRequest, this, _1)); chatWindow_->onGetAffiliationsRequest.connect(boost::bind(&MUCController::handleGetAffiliationsRequest, this)); chatWindow_->onChangeAffiliationsRequest.connect(boost::bind(&MUCController::handleChangeAffiliationsRequest, this, _1)); @@ -84,12 +104,13 @@ MUCController::MUCController ( muc_->onJoinFailed.connect(boost::bind(&MUCController::handleJoinFailed, this, _1)); muc_->onOccupantJoined.connect(boost::bind(&MUCController::handleOccupantJoined, this, _1)); + muc_->onOccupantNicknameChanged.connect(boost::bind(&MUCController::handleOccupantNicknameChanged, this, _1, _2)); muc_->onOccupantPresenceChange.connect(boost::bind(&MUCController::handleOccupantPresenceChange, this, _1)); muc_->onOccupantLeft.connect(boost::bind(&MUCController::handleOccupantLeft, this, _1, _2, _3)); - muc_->onOccupantRoleChanged.connect(boost::bind(&MUCController::handleOccupantRoleChanged, this, _1, _2, _3)); - muc_->onOccupantAffiliationChanged.connect(boost::bind(&MUCController::handleOccupantAffiliationChanged, this, _1, _2, _3)); - muc_->onConfigurationFailed.connect(boost::bind(&MUCController::handleConfigurationFailed, this, _1)); - muc_->onConfigurationFormReceived.connect(boost::bind(&MUCController::handleConfigurationFormReceived, this, _1)); muc_->onRoleChangeFailed.connect(boost::bind(&MUCController::handleOccupantRoleChangeFailed, this, _1, _2, _3)); muc_->onAffiliationListReceived.connect(boost::bind(&MUCController::handleAffiliationListReceived, this, _1, _2)); + muc_->onConfigurationFailed.connect(boost::bind(&MUCController::handleConfigurationFailed, this, _1)); + muc_->onConfigurationFormReceived.connect(boost::bind(&MUCController::handleConfigurationFormReceived, this, _1)); + highlighter_->setMode(isImpromptu_ ? Highlighter::ChatMode : Highlighter::MUCMode); + highlighter_->setNick(nick_); if (timerFactory) { loginCheckTimer_ = boost::shared_ptr<Timer>(timerFactory->createTimer(MUC_JOIN_WARNING_TIMEOUT_MILLISECONDS)); @@ -97,5 +118,13 @@ MUCController::MUCController ( loginCheckTimer_->start(); } - chatWindow_->convertToMUC(); + if (isImpromptu) { + muc_->onUnlocked.connect(boost::bind(&MUCController::handleRoomUnlocked, this)); + chatWindow_->convertToMUC(ChatWindow::ImpromptuMUC); + } else { + muc_->onOccupantRoleChanged.connect(boost::bind(&MUCController::handleOccupantRoleChanged, this, _1, _2, _3)); + muc_->onOccupantAffiliationChanged.connect(boost::bind(&MUCController::handleOccupantAffiliationChanged, this, _1, _2, _3)); + chatWindow_->convertToMUC(ChatWindow::StandardMUC); + chatWindow_->setName(muc->getJID().getNode()); + } setOnline(true); if (avatarManager_ != NULL) { @@ -103,8 +132,11 @@ MUCController::MUCController ( } handleBareJIDCapsChanged(muc->getJID()); + eventStream_->onUIEvent.connect(boost::bind(&MUCController::handleUIEvent, this, _1)); } MUCController::~MUCController() { + eventStream_->onUIEvent.disconnect(boost::bind(&MUCController::handleUIEvent, this, _1)); chatWindow_->setRosterModel(NULL); + delete rosterVCardProvider_; delete roster_; if (loginCheckTimer_) { @@ -125,5 +157,5 @@ void MUCController::handleWindowOccupantSelectionChanged(ContactRosterItem* item MUCOccupant::Affiliation affiliation = muc_->getOccupant(getNick()).getAffiliation(); MUCOccupant::Role role = muc_->getOccupant(getNick()).getRole(); - if (role == MUCOccupant::Moderator) + if (role == MUCOccupant::Moderator && !isImpromptu_) { if (affiliation == MUCOccupant::Admin || affiliation == MUCOccupant::Owner) { @@ -140,4 +172,5 @@ void MUCController::handleWindowOccupantSelectionChanged(ContactRosterItem* item actions.push_back(ChatWindow::AddContact); } + actions.push_back(ChatWindow::ShowProfile); } chatWindow_->setAvailableOccupantActions(actions); @@ -158,4 +191,5 @@ void MUCController::handleActionRequestedOnOccupant(ChatWindow::OccupantAction a case ChatWindow::MakeVisitor: muc_->changeOccupantRole(mucJID, MUCOccupant::Visitor);break; case ChatWindow::AddContact: if (occupant.getRealJID()) events_->send(boost::make_shared<RequestAddUserDialogUIEvent>(realJID, occupant.getNick()));break; + case ChatWindow::ShowProfile: events_->send(boost::make_shared<ShowProfileForRosterItemUIEvent>(mucJID));break; } } @@ -189,7 +223,13 @@ void MUCController::rejoin() { } //FIXME: check for received activity +#ifdef SWIFT_EXPERIMENTAL_HISTORY + if (lastActivity_ == boost::posix_time::not_a_date_time && historyController_) { + lastActivity_ = historyController_->getLastTimeStampFromMUC(selfJID_, toJID_); + } +#endif if (lastActivity_ == boost::posix_time::not_a_date_time) { muc_->joinAs(nick_); - } else { + } + else { muc_->joinWithContextSince(nick_, lastActivity_); } @@ -205,7 +245,33 @@ const std::string& MUCController::getNick() { } +const boost::optional<std::string> MUCController::getPassword() const { + return password_; +} + +bool MUCController::isImpromptu() const { + return isImpromptu_; +} + +std::map<std::string, JID> MUCController::getParticipantJIDs() const { + std::map<std::string, JID> participants; + typedef std::pair<std::string, MUCOccupant> MUCOccupantPair; + std::map<std::string, MUCOccupant> occupants = muc_->getOccupants(); + foreach(const MUCOccupantPair& occupant, occupants) { + if (occupant.first != nick_) { + participants[occupant.first] = occupant.second.getRealJID().is_initialized() ? occupant.second.getRealJID().get().toBare() : JID(); + } + } + return participants; +} + +void MUCController::sendInvites(const std::vector<JID>& jids, const std::string& reason) const { + foreach (const JID& jid, jids) { + muc_->invitePerson(jid, reason, isImpromptu_); + } +} + void MUCController::handleJoinTimeoutTick() { receivedActivity(); - chatWindow_->addSystemMessage(str(format(QT_TRANSLATE_NOOP("", "Room %1% is not responding. This operation may never complete.")) % toJID_.toString())); + chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(str(format(QT_TRANSLATE_NOOP("", "Room %1% is not responding. This operation may never complete.")) % toJID_.toString())), ChatWindow::DefaultDirection); } @@ -216,4 +282,7 @@ void MUCController::receivedActivity() { } +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wswitch-enum" + void MUCController::handleJoinFailed(boost::shared_ptr<ErrorPayload> error) { receivedActivity(); @@ -255,24 +324,46 @@ void MUCController::handleJoinFailed(boost::shared_ptr<ErrorPayload> error) { } errorMessage = str(format(QT_TRANSLATE_NOOP("", "Couldn't join room: %1%.")) % errorMessage); - chatWindow_->addErrorMessage(errorMessage); + chatWindow_->addErrorMessage(chatMessageParser_->parseMessageBody(errorMessage)); parting_ = true; - if (!rejoinNick.empty()) { - nick_ = rejoinNick; + if (!rejoinNick.empty() && renameCounter_ < 10) { + renameCounter_++; + setNick(rejoinNick); rejoin(); } } +#pragma clang diagnostic pop + void MUCController::handleJoinComplete(const std::string& nick) { receivedActivity(); + renameCounter_ = 0; joined_ = true; - std::string joinMessage = str(format(QT_TRANSLATE_NOOP("", "You have entered room %1% as %2%.")) % toJID_.toString() % nick); - nick_ = nick; - chatWindow_->addSystemMessage(joinMessage); + std::string joinMessage; + if (isImpromptu_) { + joinMessage = str(format(QT_TRANSLATE_NOOP("", "You have joined the chat as %1%.")) % nick); + } else { + joinMessage = str(format(QT_TRANSLATE_NOOP("", "You have entered room %1% as %2%.")) % toJID_.toString() % nick); + } + setNick(nick); + chatWindow_->replaceLastMessage(chatMessageParser_->parseMessageBody(joinMessage), ChatWindow::UpdateTimestamp); + +#ifdef SWIFT_EXPERIMENTAL_HISTORY + addRecentLogs(); +#endif + clearPresenceQueue(); shouldJoinOnReconnect_ = true; setEnabled(true); + if (isImpromptu_) { + setAvailableRoomActions(MUCOccupant::NoAffiliation, MUCOccupant::Participant); + } else { MUCOccupant occupant = muc_->getOccupant(nick); setAvailableRoomActions(occupant.getAffiliation(), occupant.getRole()); + } onUserJoined(); + + if (isImpromptu_) { + setImpromptuWindowTitle(); + } } @@ -281,6 +372,5 @@ void MUCController::handleAvatarChanged(const JID& jid) { return; } - std::string path = avatarManager_->getAvatarPath(jid).string(); - roster_->applyOnItems(SetAvatar(jid, path, JID::WithResource)); + roster_->applyOnItems(SetAvatar(jid, avatarManager_->getAvatarPath(jid), JID::WithResource)); } @@ -305,15 +395,21 @@ void MUCController::handleOccupantJoined(const MUCOccupant& occupant) { NickJoinPart event(occupant.getNick(), Join); appendToJoinParts(joinParts_, event); - std::string groupName(roleToGroupName(occupant.getRole())); - roster_->addContact(jid, realJID, occupant.getNick(), groupName, avatarManager_->getAvatarPath(jid).string()); - roster_->getGroup(groupName)->setManualSort(roleToSortName(occupant.getRole())); + MUCOccupant::Role role = MUCOccupant::Participant; + MUCOccupant::Affiliation affiliation = MUCOccupant::NoAffiliation; + if (!isImpromptu_) { + role = occupant.getRole(); + affiliation = occupant.getAffiliation(); + } + std::string groupName(roleToGroupName(role)); + roster_->addContact(jid, realJID, occupant.getNick(), groupName, avatarManager_->getAvatarPath(jid)); + roster_->applyOnItems(SetMUC(jid, role, affiliation)); + roster_->getGroup(groupName)->setManualSort(roleToSortName(role)); if (joined_) { std::string joinString; - MUCOccupant::Role role = occupant.getRole(); if (role != MUCOccupant::NoRole && role != MUCOccupant::Participant) { - joinString = str(format(QT_TRANSLATE_NOOP("", "%1% has entered the room as a %2%.")) % occupant.getNick() % roleToFriendlyName(role)); + joinString = str(format(QT_TRANSLATE_NOOP("", "%1% has entered the %3% as a %2%.")) % occupant.getNick() % roleToFriendlyName(role) % (isImpromptu_ ? QT_TRANSLATE_NOOP("", "chat") : QT_TRANSLATE_NOOP("", "room"))); } else { - joinString = str(format(QT_TRANSLATE_NOOP("", "%1% has entered the room.")) % occupant.getNick()); + joinString = str(format(QT_TRANSLATE_NOOP("", "%1% has entered the %2%.")) % occupant.getNick() % (isImpromptu_ ? QT_TRANSLATE_NOOP("", "chat") : QT_TRANSLATE_NOOP("", "room"))); } if (shouldUpdateJoinParts()) { @@ -321,5 +417,9 @@ void MUCController::handleOccupantJoined(const MUCOccupant& occupant) { } else { addPresenceMessage(joinString); + } + if (isImpromptu_) { + setImpromptuWindowTitle(); + onActivity(""); } } @@ -331,5 +431,5 @@ void MUCController::handleOccupantJoined(const MUCOccupant& occupant) { void MUCController::addPresenceMessage(const std::string& message) { lastWasPresence_ = true; - chatWindow_->addPresenceMessage(message); + chatWindow_->addPresenceMessage(chatMessageParser_->parseMessageBody(message), ChatWindow::DefaultDirection); } @@ -369,4 +469,5 @@ std::string MUCController::roleToFriendlyName(MUCOccupant::Role role) { case MUCOccupant::NoRole: return ""; } + assert(false); return ""; } @@ -379,9 +480,10 @@ std::string MUCController::roleToSortName(MUCOccupant::Role role) { case MUCOccupant::NoRole: return "4"; } + assert(false); return "5"; } JID MUCController::nickToJID(const std::string& nick) { - return JID(toJID_.getNode(), toJID_.getDomain(), nick); + return muc_->getJID().withResource(nick); } @@ -398,6 +500,5 @@ void MUCController::preHandleIncomingMessage(boost::shared_ptr<MessageEvent> mes clearPresenceQueue(); boost::shared_ptr<Message> message = messageEvent->getStanza(); - if (joined_ && messageEvent->getStanza()->getFrom().getResource() != nick_ && messageTargetsMe(message) && !message->getPayload<Delay>() && messageEvent->isReadable() -) { + if (joined_ && messageEvent->getStanza()->getFrom().getResource() != nick_ && messageTargetsMe(message) && !message->getPayload<Delay>() && messageEvent->isReadable()) { chatWindow_->flash(); } @@ -405,4 +506,7 @@ void MUCController::preHandleIncomingMessage(boost::shared_ptr<MessageEvent> mes messageEvent->setTargetsMe(false); } + if (messageEvent->isReadable() && isImpromptu_) { + chatWindow_->flash(); /* behave like a regular char*/ + } if (joined_) { std::string nick = message->getFrom().getResource(); @@ -416,5 +520,5 @@ void MUCController::preHandleIncomingMessage(boost::shared_ptr<MessageEvent> mes if (message->hasSubject() && message->getBody().empty()) { - chatWindow_->addSystemMessage(str(format(QT_TRANSLATE_NOOP("", "The room subject is now: %1%")) % message->getSubject()));; + chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(str(format(QT_TRANSLATE_NOOP("", "The room subject is now: %1%")) % message->getSubject())), ChatWindow::DefaultDirection);; chatWindow_->setSubject(message->getSubject()); doneGettingHistory_ = true; @@ -426,12 +530,18 @@ void MUCController::preHandleIncomingMessage(boost::shared_ptr<MessageEvent> mes if (!doneGettingHistory_) { + checkDuplicates(message); messageEvent->conclude(); } } -void MUCController::postHandleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent) { +void MUCController::postHandleIncomingMessage(boost::shared_ptr<MessageEvent> messageEvent, const HighlightAction& highlight) { boost::shared_ptr<Message> message = messageEvent->getStanza(); - if (joined_ && messageEvent->getStanza()->getFrom().getResource() != nick_ && messageTargetsMe(message) && !message->getPayload<Delay>()) { + if (joined_ && messageEvent->getStanza()->getFrom().getResource() != nick_ && !message->getPayload<Delay>()) { + if (messageTargetsMe(message) || isImpromptu_) { eventController_->handleIncomingEvent(messageEvent); + if (!messageEvent->getConcluded()) { + highlighter_->handleHighlightAction(highlight); + } + } } } @@ -447,7 +557,8 @@ void MUCController::handleOccupantRoleChanged(const std::string& nick, const MUC } std::string group(roleToGroupName(occupant.getRole())); - roster_->addContact(jid, realJID, nick, group, avatarManager_->getAvatarPath(jid).string()); + roster_->addContact(jid, realJID, nick, group, avatarManager_->getAvatarPath(jid)); roster_->getGroup(group)->setManualSort(roleToSortName(occupant.getRole())); - chatWindow_->addSystemMessage(str(format(QT_TRANSLATE_NOOP("", "%1% is now a %2%")) % nick % roleToFriendlyName(occupant.getRole()))); + roster_->applyOnItems(SetMUC(jid, occupant.getRole(), occupant.getAffiliation())); + chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(str(format(QT_TRANSLATE_NOOP("", "%1% is now a %2%")) % nick % roleToFriendlyName(occupant.getRole()))), ChatWindow::DefaultDirection); if (nick == nick_) { setAvailableRoomActions(occupant.getAffiliation(), occupant.getRole()); @@ -460,4 +571,7 @@ void MUCController::handleOccupantAffiliationChanged(const std::string& nick, co setAvailableRoomActions(affiliation, muc_->getOccupant(nick_).getRole()); } + JID jid(nickToJID(nick)); + MUCOccupant occupant = muc_->getOccupant(nick); + roster_->applyOnItems(SetMUC(jid, occupant.getRole(), affiliation)); } @@ -469,5 +583,4 @@ std::string MUCController::roleToGroupName(MUCOccupant::Role role) { case MUCOccupant::Visitor: result = QT_TRANSLATE_NOOP("", "Visitors"); break; case MUCOccupant::NoRole: result = QT_TRANSLATE_NOOP("", "Occupants"); break; - default: assert(false); } return result; @@ -482,9 +595,14 @@ void MUCController::setOnline(bool online) { } else { if (shouldJoinOnReconnect_) { - chatWindow_->addSystemMessage(str(format(QT_TRANSLATE_NOOP("", "Trying to enter room %1%")) % toJID_.toString())); + renameCounter_ = 0; + if (isImpromptu_) { + chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(QT_TRANSLATE_NOOP("", "Trying to join chat")), ChatWindow::DefaultDirection); + } else { + chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(str(format(QT_TRANSLATE_NOOP("", "Trying to enter room %1%")) % toJID_.toString())), ChatWindow::DefaultDirection); + } if (loginCheckTimer_) { loginCheckTimer_->start(); } - nick_ = desiredNick_; + setNick(desiredNick_); rejoin(); } @@ -523,6 +641,20 @@ void MUCController::handleOccupantLeft(const MUCOccupant& occupant, MUC::Leaving case MUC::LeavePart: break; } + if (isImpromptu_) { + partMessage = str(format(QT_TRANSLATE_NOOP("", "%1% has left the chat%2%")) % occupant.getNick() % partType); + } else { partMessage = str(format(QT_TRANSLATE_NOOP("", "%1% has left the room%2%")) % occupant.getNick() % partType); } + } + else if (isImpromptu_) { + switch (type) { + case MUC::LeaveKick: + case MUC::LeaveBan: clearPresenceQueue(); clearAfter = true; partMessage = QT_TRANSLATE_NOOP("", "You have been removed from this chat"); break; + case MUC::LeaveNotMember: clearPresenceQueue(); clearAfter = true; partMessage = QT_TRANSLATE_NOOP("", "You have been removed from this chat"); break; + case MUC::LeaveDestroy: clearPresenceQueue(); clearAfter = true; partMessage = QT_TRANSLATE_NOOP("", "This chat has ended"); break; + case MUC::Disconnect: + case MUC::LeavePart: partMessage = QT_TRANSLATE_NOOP("", "You have left the chat"); + } + } else { switch (type) { @@ -555,4 +687,46 @@ void MUCController::handleOccupantLeft(const MUCOccupant& occupant, MUC::Leaving clearPresenceQueue(); } + + if (isImpromptu_) { + setImpromptuWindowTitle(); + } +} + +void MUCController::handleOccupantNicknameChanged(const std::string& oldNickname, const std::string& newNickname) { + addPresenceMessage(generateNicknameChangeString(oldNickname, newNickname)); + JID oldJID = muc_->getJID().withResource(oldNickname); + JID newJID = muc_->getJID().withResource(newNickname); + + // adjust occupants + currentOccupants_.erase(oldNickname); + currentOccupants_.insert(newNickname); + + // adjust completer + completer_->removeWord(oldNickname); + completer_->addWord(newNickname); + + // update contact + roster_->removeContact(oldJID); + MUCOccupant occupant = muc_->getOccupant(newNickname); + + JID realJID; + if (occupant.getRealJID()) { + realJID = occupant.getRealJID().get(); + } + MUCOccupant::Role role = MUCOccupant::Participant; + MUCOccupant::Affiliation affiliation = MUCOccupant::NoAffiliation; + if (!isImpromptu_) { + role = occupant.getRole(); + affiliation = occupant.getAffiliation(); + } + std::string groupName(roleToGroupName(role)); + roster_->addContact(newJID, realJID, newNickname, groupName, avatarManager_->getAvatarPath(newJID)); + roster_->applyOnItems(SetMUC(newJID, role, affiliation)); + if (avatarManager_ != NULL) { + handleAvatarChanged(newJID); + } + + clearPresenceQueue(); + onUserNicknameChanged(oldNickname, newNickname); } @@ -580,5 +754,5 @@ boost::optional<boost::posix_time::ptime> MUCController::getMessageTimestamp(boo void MUCController::updateJoinParts() { - chatWindow_->replaceLastMessage(generateJoinPartString(joinParts_)); + chatWindow_->replaceLastMessage(chatMessageParser_->parseMessageBody(generateJoinPartString(joinParts_, isImpromptu())), ChatWindow::UpdateTimestamp); } @@ -593,5 +767,6 @@ void MUCController::appendToJoinParts(std::vector<NickJoinPart>& joinParts, cons case Join: type = (type == Part) ? PartThenJoin : Join; break; case Part: type = (type == Join) ? JoinThenPart : Part; break; - default: /*Nothing to see here */;break; + case PartThenJoin: break; + case JoinThenPart: break; } (*it).type = type; @@ -620,5 +795,5 @@ std::string MUCController::concatenateListOfNames(const std::vector<NickJoinPart } -std::string MUCController::generateJoinPartString(const std::vector<NickJoinPart>& joinParts) { +std::string MUCController::generateJoinPartString(const std::vector<NickJoinPart>& joinParts, bool isImpromptu) { std::vector<NickJoinPart> sorted[4]; std::string eventStrings[4]; @@ -635,32 +810,32 @@ std::string MUCController::generateJoinPartString(const std::vector<NickJoinPart case Join: if (sorted[i].size() > 1) { - eventString = QT_TRANSLATE_NOOP("", "%1% have entered the room"); + eventString = (isImpromptu ? QT_TRANSLATE_NOOP("", "%1% have joined the chat") : QT_TRANSLATE_NOOP("", "%1% have entered the room")); } else { - eventString = QT_TRANSLATE_NOOP("", "%1% has entered the room"); + eventString = (isImpromptu ? QT_TRANSLATE_NOOP("", "%1% has joined the chat") : QT_TRANSLATE_NOOP("", "%1% has entered the room")); } break; case Part: if (sorted[i].size() > 1) { - eventString = QT_TRANSLATE_NOOP("", "%1% have left the room"); + eventString = (isImpromptu ? QT_TRANSLATE_NOOP("", "%1% have left the chat") : QT_TRANSLATE_NOOP("", "%1% have left the room")); } else { - eventString = QT_TRANSLATE_NOOP("", "%1% has left the room"); + eventString = (isImpromptu ? QT_TRANSLATE_NOOP("", "%1% have left the chat") : QT_TRANSLATE_NOOP("", "%1% has left the room")); } break; case JoinThenPart: if (sorted[i].size() > 1) { - eventString = QT_TRANSLATE_NOOP("", "%1% have entered then left the room"); + eventString = (isImpromptu ? QT_TRANSLATE_NOOP("", "%1% have joined then left the chat") : QT_TRANSLATE_NOOP("", "%1% have entered then left the room")); } else { - eventString = QT_TRANSLATE_NOOP("", "%1% has entered then left the room"); + eventString = (isImpromptu ? QT_TRANSLATE_NOOP("", "%1% has joined then left the chat") : QT_TRANSLATE_NOOP("", "%1% has entered then left the room")); } break; case PartThenJoin: if (sorted[i].size() > 1) { - eventString = QT_TRANSLATE_NOOP("", "%1% have left then returned to the room"); + eventString = (isImpromptu ? QT_TRANSLATE_NOOP("", "%1% have left then returned to the chat") : QT_TRANSLATE_NOOP("", "%1% have left then returned to the room")); } else { - eventString = QT_TRANSLATE_NOOP("", "%1% has left then returned to the room"); + eventString = (isImpromptu ? QT_TRANSLATE_NOOP("", "%1% has left then returned to the chat") : QT_TRANSLATE_NOOP("", "%1% has left then returned to the room")); } break; @@ -683,8 +858,20 @@ std::string MUCController::generateJoinPartString(const std::vector<NickJoinPart } +std::string MUCController::generateNicknameChangeString(const std::string& oldNickname, const std::string& newNickname) { + return str(boost::format(QT_TRANSLATE_NOOP("", "%1% is now known as %2%.")) % oldNickname % newNickname); +} + void MUCController::handleChangeSubjectRequest(const std::string& subject) { muc_->changeSubject(subject); } +void MUCController::handleBookmarkRequest() { + const JID jid = muc_->getJID(); + MUCBookmark bookmark(jid, jid.toBare().toString()); + bookmark.setPassword(password_); + bookmark.setNick(nick_); + chatWindow_->showBookmarkWindow(bookmark); +} + void MUCController::handleConfigureRequest(Form::ref form) { if (form) { @@ -699,5 +886,5 @@ void MUCController::handleConfigurationFailed(ErrorPayload::ref error) { std::string errorMessage = getErrorMessage(error); errorMessage = str(format(QT_TRANSLATE_NOOP("", "Room configuration failed: %1%.")) % errorMessage); - chatWindow_->addErrorMessage(errorMessage); + chatWindow_->addErrorMessage(chatMessageParser_->parseMessageBody(errorMessage)); } @@ -705,10 +892,22 @@ void MUCController::handleOccupantRoleChangeFailed(ErrorPayload::ref error, cons std::string errorMessage = getErrorMessage(error); errorMessage = str(format(QT_TRANSLATE_NOOP("", "Occupant role change failed: %1%.")) % errorMessage); - chatWindow_->addErrorMessage(errorMessage); + chatWindow_->addErrorMessage(chatMessageParser_->parseMessageBody(errorMessage)); +} + +void MUCController::configureAsImpromptuRoom(Form::ref form) { + muc_->configureRoom(buildImpromptuRoomConfiguration(form)); + isImpromptuAlreadyConfigured_ = true; + onImpromptuConfigCompleted(); } void MUCController::handleConfigurationFormReceived(Form::ref form) { + if (isImpromptu_) { + if (!isImpromptuAlreadyConfigured_) { + configureAsImpromptuRoom(form); + } + } else { chatWindow_->showRoomConfigurationForm(form); } +} void MUCController::handleConfigurationCancelled() { @@ -720,6 +919,17 @@ void MUCController::handleDestroyRoomRequest() { } -void MUCController::handleInvitePersonToThisMUCRequest(const JID& jid, const std::string& reason) { - muc_->invitePerson(jid, reason); +void MUCController::handleInvitePersonToThisMUCRequest(const std::vector<JID>& jidsToInvite) { + RequestInviteToMUCUIEvent::ImpromptuMode mode = isImpromptu_ ? RequestInviteToMUCUIEvent::Impromptu : RequestInviteToMUCUIEvent::NotImpromptu; + boost::shared_ptr<UIEvent> event(new RequestInviteToMUCUIEvent(muc_->getJID(), jidsToInvite, mode)); + eventStream_->send(event); +} + +void MUCController::handleUIEvent(boost::shared_ptr<UIEvent> event) { + boost::shared_ptr<InviteToMUCUIEvent> inviteEvent = boost::dynamic_pointer_cast<InviteToMUCUIEvent>(event); + if (inviteEvent && inviteEvent->getRoom() == muc_->getJID()) { + foreach (const JID& jid, inviteEvent->getInvites()) { + muc_->invitePerson(jid, inviteEvent->getReason(), isImpromptu_); + } + } } @@ -751,3 +961,124 @@ void MUCController::handleAffiliationListReceived(MUCOccupant::Affiliation affil } +void MUCController::logMessage(const std::string& message, const JID& fromJID, const JID& toJID, const boost::posix_time::ptime& timeStamp, bool isIncoming) { + // log only incoming messages + if (isIncoming && historyController_) { + historyController_->addMessage(message, fromJID, toJID, HistoryMessage::Groupchat, timeStamp); + } +} + +void MUCController::addRecentLogs() { + if (!historyController_) { + return; + } + + joinContext_ = historyController_->getMUCContext(selfJID_, toJID_, lastActivity_); + + foreach (const HistoryMessage& message, joinContext_) { + bool senderIsSelf = nick_ == message.getFromJID().getResource(); + + // the chatWindow uses utc timestamps + addMessage(message.getMessage(), senderDisplayNameFromMessage(message.getFromJID()), senderIsSelf, boost::shared_ptr<SecurityLabel>(new SecurityLabel()), avatarManager_->getAvatarPath(message.getFromJID()), message.getTime() - boost::posix_time::hours(message.getOffset()), HighlightAction()); + } +} + +void MUCController::checkDuplicates(boost::shared_ptr<Message> newMessage) { + std::string body = newMessage->getBody(); + JID jid = newMessage->getFrom(); + boost::optional<boost::posix_time::ptime> time = newMessage->getTimestamp(); + + reverse_foreach (const HistoryMessage& message, joinContext_) { + boost::posix_time::ptime messageTime = message.getTime() - boost::posix_time::hours(message.getOffset()); + if (time && time < messageTime) { + break; + } + if (time && time != messageTime) { + continue; + } + if (message.getFromJID() != jid) { + continue; + } + if (message.getMessage() != body) { + continue; + } + + // Mark the message as unreadable + newMessage->setBody(""); + } +} + +void MUCController::setNick(const std::string& nick) { + nick_ = nick; + highlighter_->setNick(nick_); +} + +Form::ref MUCController::buildImpromptuRoomConfiguration(Form::ref roomConfigurationForm) { + Form::ref result = boost::make_shared<Form>(Form::SubmitType); + std::string impromptuConfigs[] = { "muc#roomconfig_enablelogging", "muc#roomconfig_persistentroom", "muc#roomconfig_publicroom", "muc#roomconfig_whois"}; + std::set<std::string> impromptuConfigsMissing(impromptuConfigs, impromptuConfigs + 4); + foreach (boost::shared_ptr<FormField> field, roomConfigurationForm->getFields()) { + boost::shared_ptr<FormField> resultField; + if (field->getName() == "muc#roomconfig_enablelogging") { + resultField = boost::make_shared<FormField>(FormField::BooleanType, "0"); + } + if (field->getName() == "muc#roomconfig_persistentroom") { + resultField = boost::make_shared<FormField>(FormField::BooleanType, "0"); + } + if (field->getName() == "muc#roomconfig_publicroom") { + resultField = boost::make_shared<FormField>(FormField::BooleanType, "0"); + } + if (field->getName() == "muc#roomconfig_whois") { + resultField = boost::make_shared<FormField>(FormField::ListSingleType, "anyone"); + } + + if (field->getName() == "FORM_TYPE") { + resultField = boost::make_shared<FormField>(FormField::HiddenType, "http://jabber.org/protocol/muc#roomconfig"); + } + + if (resultField) { + impromptuConfigsMissing.erase(field->getName()); + resultField->setName(field->getName()); + result->addField(resultField); + } + } + + foreach (const std::string& config, impromptuConfigsMissing) { + if (config == "muc#roomconfig_publicroom") { + chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(QT_TRANSLATE_NOOP("", "This server doesn't support hiding your chat from other users.")), ChatWindow::DefaultDirection); + } else if (config == "muc#roomconfig_whois") { + chatWindow_->addSystemMessage(chatMessageParser_->parseMessageBody(QT_TRANSLATE_NOOP("", "This server doesn't support sharing people's real identity in this chat.")), ChatWindow::DefaultDirection); + } + } + + return result; +} + +void MUCController::setImpromptuWindowTitle() { + std::string title; + typedef std::pair<std::string, MUCOccupant> StringMUCOccupantPair; + std::map<std::string, MUCOccupant> occupants = muc_->getOccupants(); + if (occupants.size() <= 1) { + title = QT_TRANSLATE_NOOP("", "Empty Chat"); + } else { + foreach (StringMUCOccupantPair pair, occupants) { + if (pair.first != nick_) { + title += (title.empty() ? "" : ", ") + pair.first; + } + } + } + chatWindow_->setName(title); +} + +void MUCController::handleRoomUnlocked() { + // Handle buggy MUC implementations where the joined room already exists and is unlocked. + // Configure the room again in this case. + if (!isImpromptuAlreadyConfigured_) { + if (isImpromptu_ && (muc_->getOccupant(nick_).getAffiliation() == MUCOccupant::Owner)) { + muc_->requestConfigurationForm(); + } else if (isImpromptu_) { + onImpromptuConfigCompleted(); + } + } +} + } diff --git a/Swift/Controllers/Chat/MUCController.h b/Swift/Controllers/Chat/MUCController.h index 9550ca9..2d732a1 100644 --- a/Swift/Controllers/Chat/MUCController.h +++ b/Swift/Controllers/Chat/MUCController.h @@ -1,4 +1,4 @@ /* - * Copyright (c) 2010 Kevin Smith + * Copyright (c) 2010-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. @@ -7,12 +7,13 @@ #pragma once +#include <set> +#include <string> +#include <map> + #include <boost/shared_ptr.hpp> -#include <Swiften/Base/boost_bsignals.h> #include <boost/signals/connection.hpp> -#include <set> -#include <string> +#include <Swiften/Base/boost_bsignals.h> #include <Swiften/Network/Timer.h> -#include <Swift/Controllers/Chat/ChatControllerBase.h> #include <Swiften/Elements/Message.h> #include <Swiften/Elements/DiscoInfo.h> @@ -20,4 +21,6 @@ #include <Swiften/MUC/MUC.h> #include <Swiften/Elements/MUCOccupant.h> + +#include <Swift/Controllers/Chat/ChatControllerBase.h> #include <Swift/Controllers/Roster/RosterItem.h> #include <Swift/Controllers/UIInterfaces/ChatWindow.h> @@ -32,9 +35,14 @@ namespace Swift { class TimerFactory; class TabComplete; + class XMPPRoster; + class HighlightManager; + class UIEvent; + class VCardManager; + class RosterVCardProvider; enum JoinPart {Join, Part, JoinThenPart, PartThenJoin}; struct NickJoinPart { - NickJoinPart(const std::string& nick, JoinPart type) : nick(nick), type(type) {}; + NickJoinPart(const std::string& nick, JoinPart type) : nick(nick), type(type) {} std::string nick; JoinPart type; @@ -43,15 +51,22 @@ namespace Swift { class MUCController : public ChatControllerBase { public: - MUCController(const JID& self, MUC::ref muc, const boost::optional<std::string>& password, const std::string &nick, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, PresenceOracle* presenceOracle, AvatarManager* avatarManager, UIEventStream* events, bool useDelayForLatency, TimerFactory* timerFactory, EventController* eventController, EntityCapsProvider* entityCapsProvider); - ~MUCController(); + MUCController(const JID& self, MUC::ref muc, const boost::optional<std::string>& password, const std::string &nick, StanzaChannel* stanzaChannel, IQRouter* iqRouter, ChatWindowFactory* chatWindowFactory, PresenceOracle* presenceOracle, AvatarManager* avatarManager, UIEventStream* events, bool useDelayForLatency, TimerFactory* timerFactory, EventController* eventController, EntityCapsProvider* entityCapsProvider, XMPPRoster* roster, HistoryController* historyController, MUCRegistry* mucRegistry, HighlightManager* highlightManager, boost::shared_ptr<ChatMessageParser> chatMessageParser, bool isImpromptu, AutoAcceptMUCInviteDecider* autoAcceptMUCInviteDecider, VCardManager* vcardManager); + virtual ~MUCController(); boost::signal<void ()> onUserLeft; boost::signal<void ()> onUserJoined; + boost::signal<void ()> onImpromptuConfigCompleted; + boost::signal<void (const std::string&, const std::string& )> onUserNicknameChanged; virtual void setOnline(bool online); void rejoin(); static void appendToJoinParts(std::vector<NickJoinPart>& joinParts, const NickJoinPart& newEvent); - static std::string generateJoinPartString(const std::vector<NickJoinPart>& joinParts); + static std::string generateJoinPartString(const std::vector<NickJoinPart>& joinParts, bool isImpromptu); static std::string concatenateListOfNames(const std::vector<NickJoinPart>& joinParts); + static std::string generateNicknameChangeString(const std::string& oldNickname, const std::string& newNickname); bool isJoined(); const std::string& getNick(); + const boost::optional<std::string> getPassword() const; + bool isImpromptu() const; + std::map<std::string, JID> getParticipantJIDs() const; + void sendInvites(const std::vector<JID>& jids, const std::string& reason) const; protected: @@ -61,6 +76,7 @@ namespace Swift { boost::optional<boost::posix_time::ptime> getMessageTimestamp(boost::shared_ptr<Message> message) const; void preHandleIncomingMessage(boost::shared_ptr<MessageEvent>); - void postHandleIncomingMessage(boost::shared_ptr<MessageEvent>); + void postHandleIncomingMessage(boost::shared_ptr<MessageEvent>, const HighlightAction&); void cancelReplaces(); + void logMessage(const std::string& message, const JID& fromJID, const JID& toJID, const boost::posix_time::ptime& timeStamp, bool isIncoming); private: @@ -73,4 +89,5 @@ namespace Swift { void handleAvatarChanged(const JID& jid); void handleOccupantJoined(const MUCOccupant& occupant); + void handleOccupantNicknameChanged(const std::string& oldNickname, const std::string& newNickname); void handleOccupantLeft(const MUCOccupant& occupant, MUC::LeavingType type, const std::string& reason); void handleOccupantPresenceChange(boost::shared_ptr<Presence> presence); @@ -81,4 +98,5 @@ namespace Swift { void handleJoinTimeoutTick(); void handleChangeSubjectRequest(const std::string&); + void handleBookmarkRequest(); std::string roleToGroupName(MUCOccupant::Role role); std::string roleToSortName(MUCOccupant::Role role); @@ -96,5 +114,5 @@ namespace Swift { void handleConfigurationFormReceived(Form::ref); void handleDestroyRoomRequest(); - void handleInvitePersonToThisMUCRequest(const JID& jid, const std::string& reason); + void handleInvitePersonToThisMUCRequest(const std::vector<JID>& jidsToInvite); void handleConfigurationCancelled(); void handleOccupantRoleChangeFailed(ErrorPayload::ref, const JID&, MUCOccupant::Role); @@ -102,4 +120,14 @@ namespace Swift { void handleAffiliationListReceived(MUCOccupant::Affiliation affiliation, const std::vector<JID>& jids); void handleChangeAffiliationsRequest(const std::vector<std::pair<MUCOccupant::Affiliation, JID> >& changes); + void handleInviteToMUCWindowDismissed(); + void handleInviteToMUCWindowCompleted(); + void handleUIEvent(boost::shared_ptr<UIEvent> event); + void addRecentLogs(); + void checkDuplicates(boost::shared_ptr<Message> newMessage); + void setNick(const std::string& nick); + void setImpromptuWindowTitle(); + void handleRoomUnlocked(); + void configureAsImpromptuRoom(Form::ref form); + Form::ref buildImpromptuRoomConfiguration(Form::ref roomConfigurationForm); private: @@ -121,4 +149,10 @@ namespace Swift { boost::posix_time::ptime lastActivity_; boost::optional<std::string> password_; + XMPPRoster* xmppRoster_; + std::vector<HistoryMessage> joinContext_; + size_t renameCounter_; + bool isImpromptu_; + bool isImpromptuAlreadyConfigured_; + RosterVCardProvider* rosterVCardProvider_; }; } diff --git a/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp b/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp new file mode 100644 index 0000000..2a07654 --- /dev/null +++ b/Swift/Controllers/Chat/UnitTest/ChatMessageParserTest.cpp @@ -0,0 +1,267 @@ +/* + * Copyright (c) 2013-2014 Kevin Smith + * Licensed under the GNU General Public License v3. + * See Documentation/Licenses/GPLv3.txt for more information. + */ + +#include <cppunit/extensions/HelperMacros.h> +#include <cppunit/extensions/TestFactoryRegistry.h> +#include <hippomocks.h> + +#include <Swift/Controllers/Chat/ChatMessageParser.h> + +using namespace Swift; + +class ChatMessageParserTest : public CppUnit::TestFixture { + CPPUNIT_TEST_SUITE(ChatMessageParserTest); + CPPUNIT_TEST(testFullBody); + CPPUNIT_TEST(testOneEmoticon); + CPPUNIT_TEST(testBareEmoticon); + CPPUNIT_TEST(testHiddenEmoticon); + CPPUNIT_TEST(testEndlineEmoticon); + CPPUNIT_TEST(testBoundedEmoticons); + CPPUNIT_TEST_SUITE_END(); + +public: + void setUp() { + smile1_ = ":)"; + smile1Path_ = "/blah/smile1.png"; + smile2_ = ":("; + smile2Path_ = "/blah/smile2.jpg"; + emoticons_[smile1_] = smile1Path_; + emoticons_[smile2_] = smile2Path_; + } + + void tearDown() { + emoticons_.clear(); + } + + void assertText(const ChatWindow::ChatMessage& result, size_t index, const std::string& text) { + boost::shared_ptr<ChatWindow::ChatTextMessagePart> part = boost::dynamic_pointer_cast<ChatWindow::ChatTextMessagePart>(result.getParts()[index]); + CPPUNIT_ASSERT_EQUAL(text, part->text); + } + + void assertEmoticon(const ChatWindow::ChatMessage& result, size_t index, const std::string& text, const std::string& path) { + boost::shared_ptr<ChatWindow::ChatEmoticonMessagePart> part = boost::dynamic_pointer_cast<ChatWindow::ChatEmoticonMessagePart>(result.getParts()[index]); + CPPUNIT_ASSERT(!!part); + CPPUNIT_ASSERT_EQUAL(text, part->alternativeText); + CPPUNIT_ASSERT_EQUAL(path, part->imagePath); + } + + void assertHighlight(const ChatWindow::ChatMessage& result, size_t index, const std::string& text) { + boost::shared_ptr<ChatWindow::ChatHighlightingMessagePart> part = boost::dynamic_pointer_cast<ChatWindow::ChatHighlightingMessagePart>(result.getParts()[index]); + CPPUNIT_ASSERT_EQUAL(text, part->text); + } + + void assertURL(const ChatWindow::ChatMessage& result, size_t index, const std::string& text) { + boost::shared_ptr<ChatWindow::ChatURIMessagePart> part = boost::dynamic_pointer_cast<ChatWindow::ChatURIMessagePart>(result.getParts()[index]); + CPPUNIT_ASSERT_EQUAL(text, part->target); + } + + static HighlightRule ruleFromKeyword(const std::string& keyword, bool matchCase, bool matchWholeWord) + { + HighlightRule rule; + std::vector<std::string> keywords; + keywords.push_back(keyword); + rule.setKeywords(keywords); + rule.setMatchCase(matchCase); + rule.setMatchWholeWords(matchWholeWord); + rule.setMatchChat(true); + return rule; + } + + static const HighlightRulesListPtr ruleListFromKeyword(const std::string& keyword, bool matchCase, bool matchWholeWord) + { + boost::shared_ptr<HighlightManager::HighlightRulesList> list = boost::make_shared<HighlightManager::HighlightRulesList>(); + list->addRule(ruleFromKeyword(keyword, matchCase, matchWholeWord)); + return list; + } + + static const HighlightRulesListPtr ruleListFromKeywords(const HighlightRule &rule1, const HighlightRule &rule2) + { + boost::shared_ptr<HighlightManager::HighlightRulesList> list = boost::make_shared<HighlightManager::HighlightRulesList>(); + list->addRule(rule1); + list->addRule(rule2); + return list; + } + + static HighlightRulesListPtr ruleListWithNickHighlight() + { + HighlightRule rule; + rule.setMatchChat(true); + rule.setNickIsKeyword(true); + rule.setMatchCase(true); + rule.setMatchWholeWords(true); + boost::shared_ptr<HighlightManager::HighlightRulesList> list = boost::make_shared<HighlightManager::HighlightRulesList>(); + list->addRule(rule); + return list; + } + + void testFullBody() { + const std::string no_special_message = "a message with no special content"; + ChatMessageParser testling(emoticons_, boost::make_shared<HighlightManager::HighlightRulesList>()); + ChatWindow::ChatMessage result = testling.parseMessageBody(no_special_message); + assertText(result, 0, no_special_message); + + testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, false)); + result = testling.parseMessageBody(":) shiny :( trigger :) http://wonderland.lit/blah http://denmark.lit boom boom"); + assertEmoticon(result, 0, smile1_, smile1Path_); + assertText(result, 1, " shiny "); + assertEmoticon(result, 2, smile2_, smile2Path_); + assertText(result, 3, " "); + assertHighlight(result, 4, "trigger"); + assertText(result, 5, " "); + assertEmoticon(result, 6, smile1_, smile1Path_); + assertText(result, 7, " "); + assertURL(result, 8, "http://wonderland.lit/blah"); + assertText(result, 9, " "); + assertURL(result, 10, "http://denmark.lit"); + assertText(result, 11, " boom boom"); + + testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, false)); + result = testling.parseMessageBody("testtriggermessage"); + assertText(result, 0, "test"); + assertHighlight(result, 1, "trigger"); + assertText(result, 2, "message"); + + testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, true)); + result = testling.parseMessageBody("testtriggermessage"); + assertText(result, 0, "testtriggermessage"); + + testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", true, false)); + result = testling.parseMessageBody("TrIgGeR"); + assertText(result, 0, "TrIgGeR"); + + testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, false)); + result = testling.parseMessageBody("TrIgGeR"); + assertHighlight(result, 0, "TrIgGeR"); + + testling = ChatMessageParser(emoticons_, ruleListFromKeyword("trigger", false, false)); + result = testling.parseMessageBody("partialTrIgGeRmatch"); + assertText(result, 0, "partial"); + assertHighlight(result, 1, "TrIgGeR"); + assertText(result, 2, "match"); + + testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", false, false))); + result = testling.parseMessageBody("zero one two three"); + assertText(result, 0, "zero "); + assertHighlight(result, 1, "one"); + assertText(result, 2, " two "); + assertHighlight(result, 3, "three"); + + testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", false, false))); + result = testling.parseMessageBody("zero oNe two tHrEe"); + assertText(result, 0, "zero "); + assertHighlight(result, 1, "oNe"); + assertText(result, 2, " two "); + assertHighlight(result, 3, "tHrEe"); + + testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", true, false))); + result = testling.parseMessageBody("zero oNe two tHrEe"); + assertText(result, 0, "zero "); + assertHighlight(result, 1, "oNe"); + assertText(result, 2, " two tHrEe"); + + testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", true, false), ruleFromKeyword("three", true, false))); + result = testling.parseMessageBody("zero oNe two tHrEe"); + assertText(result, 0, "zero oNe two tHrEe"); + + testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, false), ruleFromKeyword("three", false, false))); + result = testling.parseMessageBody("zeroonetwothree"); + assertText(result, 0, "zero"); + assertHighlight(result, 1, "one"); + assertText(result, 2, "two"); + assertHighlight(result, 3, "three"); + + testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", true, false), ruleFromKeyword("three", false, false))); + result = testling.parseMessageBody("zeroOnEtwoThReE"); + assertText(result, 0, "zeroOnEtwo"); + assertHighlight(result, 1, "ThReE"); + + testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, true), ruleFromKeyword("three", false, false))); + result = testling.parseMessageBody("zeroonetwothree"); + assertText(result, 0, "zeroonetwo"); + assertHighlight(result, 1, "three"); + + testling = ChatMessageParser(emoticons_, ruleListFromKeywords(ruleFromKeyword("one", false, true), ruleFromKeyword("three", false, true))); + result = testling.parseMessageBody("zeroonetwothree"); + assertText(result, 0, "zeroonetwothree"); + + testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); + result = testling.parseMessageBody("Alice", "Alice"); + assertHighlight(result, 0, "Alice"); + + testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); + result = testling.parseMessageBody("TextAliceText", "Alice"); + assertText(result, 0, "TextAliceText"); + + testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); + result = testling.parseMessageBody("Text Alice Text", "Alice"); + assertText(result, 0, "Text "); + assertHighlight(result, 1, "Alice"); + assertText(result, 2, " Text"); + + testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); + result = testling.parseMessageBody("Alice Text", "Alice"); + assertHighlight(result, 0, "Alice"); + assertText(result, 1, " Text"); + + testling = ChatMessageParser(emoticons_, ruleListWithNickHighlight()); + result = testling.parseMessageBody("Text Alice", "Alice"); + assertText(result, 0, "Text "); + assertHighlight(result, 1, "Alice"); + } + + void testOneEmoticon() { + ChatMessageParser testling(emoticons_, boost::make_shared<HighlightManager::HighlightRulesList>()); + ChatWindow::ChatMessage result = testling.parseMessageBody(" :) "); + assertText(result, 0, " "); + assertEmoticon(result, 1, smile1_, smile1Path_); + assertText(result, 2, " "); + } + + + void testBareEmoticon() { + ChatMessageParser testling(emoticons_, boost::make_shared<HighlightManager::HighlightRulesList>()); + ChatWindow::ChatMessage result = testling.parseMessageBody(":)"); + assertEmoticon(result, 0, smile1_, smile1Path_); + } + + void testHiddenEmoticon() { + ChatMessageParser testling(emoticons_, boost::make_shared<HighlightManager::HighlightRulesList>()); + ChatWindow::ChatMessage result = testling.parseMessageBody("b:)a"); + assertText(result, 0, "b:)a"); + } + + void testEndlineEmoticon() { + ChatMessageParser testling(emoticons_, boost::make_shared<HighlightManager::HighlightRulesList>()); + ChatWindow::ChatMessage result = testling.parseMessageBody("Lazy:)"); + assertText(result, 0, "Lazy"); + assertEmoticon(result, 1, smile1_, smile1Path_); + } + + void testBoundedEmoticons() { + ChatMessageParser testling(emoticons_, boost::make_shared<HighlightManager::HighlightRulesList>()); + ChatWindow::ChatMessage result = testling.parseMessageBody(":)Lazy:("); + assertEmoticon(result, 0, smile1_, smile1Path_); + assertText(result, 1, "Lazy"); + assertEmoticon(result, 2, smile2_, smile2Path_); + } + + void testEmoticonParenthesis() { + ChatMessageParser testling(emoticons_, boost::make_shared<HighlightManager::HighlightRulesList>()); + ChatWindow::ChatMessage result = testling.parseMessageBody("(Like this :))"); + assertText(result, 0, "(Like this "); + assertEmoticon(result, 1, smile1_, smile1Path_); + assertText(result, 2, ")"); + } + +private: + std::map<std::string, std::string> emoticons_; + std::string smile1_; + std::string smile1Path_; + std::string smile2_; + std::string smile2Path_; +}; + +CPPUNIT_TEST_SUITE_REGISTRATION(ChatMessageParserTest); diff --git a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp index bbfb22f..4c604ac 100644 --- a/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp +++ b/Swift/Controllers/Chat/UnitTest/ChatsManagerTest.cpp @@ -7,47 +7,56 @@ #include <cppunit/extensions/HelperMacros.h> #include <cppunit/extensions/TestFactoryRegistry.h> -#include "3rdParty/hippomocks.h" + +#include <hippomocks.h> #include <boost/bind.hpp> -#include "Swift/Controllers/Chat/ChatsManager.h" - -#include "Swift/Controllers/Chat/UnitTest/MockChatListWindow.h" -#include "Swift/Controllers/UIInterfaces/ChatWindow.h" -#include "Swift/Controllers/Settings/DummySettingsProvider.h" -#include "Swift/Controllers/UIInterfaces/ChatWindowFactory.h" -#include "Swift/Controllers/UIInterfaces/ChatListWindowFactory.h" -#include "Swift/Controllers/UIInterfaces/JoinMUCWindowFactory.h" -#include "Swift/Controllers/UIInterfaces/MUCSearchWindowFactory.h" -#include "Swiften/Client/Client.h" -#include "Swiften/Disco/EntityCapsManager.h" -#include "Swiften/Disco/CapsProvider.h" -#include "Swiften/MUC/MUCManager.h" -#include "Swift/Controllers/Chat/ChatController.h" -#include "Swift/Controllers/XMPPEvents/EventController.h" -#include "Swift/Controllers/Chat/MUCController.h" -#include "Swiften/Presence/StanzaChannelPresenceSender.h" -#include "Swiften/Avatars/NullAvatarManager.h" -#include "Swiften/Avatars/AvatarMemoryStorage.h" -#include "Swiften/VCards/VCardManager.h" -#include "Swiften/VCards/VCardMemoryStorage.h" -#include "Swiften/Client/NickResolver.h" -#include "Swiften/Presence/DirectedPresenceSender.h" -#include "Swiften/Roster/XMPPRosterImpl.h" -#include "Swift/Controllers/UnitTest/MockChatWindow.h" -#include "Swiften/Client/DummyStanzaChannel.h" -#include "Swiften/Queries/DummyIQChannel.h" -#include "Swiften/Presence/PresenceOracle.h" -#include "Swiften/Jingle/JingleSessionManager.h" -#include "Swiften/FileTransfer/UnitTest/DummyFileTransferManager.h" -#include "Swift/Controllers/UIEvents/RequestChatUIEvent.h" -#include "Swift/Controllers/UIEvents/JoinMUCUIEvent.h" -#include "Swift/Controllers/UIEvents/UIEventStream.h" -#include <Swift/Controllers/ProfileSettingsProvider.h> -#include "Swift/Controllers/FileTransfer/FileTransferOverview.h" -#include "Swiften/Elements/DeliveryReceiptRequest.h" -#include "Swiften/Elements/DeliveryReceipt.h" +#include <Swiften/Avatars/AvatarMemoryStorage.h> +#include <Swiften/Avatars/NullAvatarManager.h> #include <Swiften/Base/Algorithm.h> +#include <Swiften/Client/Client.h> +#include <Swiften/Client/ClientBlockListManager.h> +#include <Swiften/Client/DummyStanzaChannel.h> +#include <Swiften/Client/NickResolver.h> +#include <Swiften/Crypto/CryptoProvider.h> +#include <Swiften/Crypto/PlatformCryptoProvider.h> +#include <Swiften/Disco/CapsProvider.h> +#include <Swiften/Disco/EntityCapsManager.h> +#include <Swiften/Elements/DeliveryReceipt.h> +#include <Swiften/Elements/DeliveryReceiptRequest.h> +#include <Swiften/FileTransfer/UnitTest/DummyFileTransferManager.h> +#include <Swiften/Jingle/JingleSessionManager.h> +#include <Swiften/MUC/MUCManager.h> +#include <Swiften/Presence/DirectedPresenceSender.h> +#include <Swiften/Presence/PresenceOracle.h> +#include <Swiften/Presence/StanzaChannelPresenceSender.h> +#include <Swiften/Queries/DummyIQChannel.h> +#include <Swiften/Roster/XMPPRosterImpl.h> +#include <Swiften/VCards/VCardManager.h> +#include <Swiften/VCards/VCardManager.h> +#include <Swiften/VCards/VCardMemoryStorage.h> +#include <Swiften/Whiteboard/WhiteboardSessionManager.h> + +#include <Swift/Controllers/Chat/ChatsManager.h> +#include <Swift/Controllers/Chat/ChatController.h> +#include <Swift/Controllers/Chat/MUCController.h> +#include <Swift/Controllers/Chat/UnitTest/MockChatListWindow.h> +#include <Swift/Controllers/FileTransfer/FileTransferOverview.h> +#include <Swift/Controllers/ProfileSettingsProvider.h> #include <Swift/Controllers/SettingConstants.h> +#include <Swift/Controllers/Settings/DummySettingsProvider.h> +#include <Swift/Controllers/UIEvents/JoinMUCUIEvent.h> +#include <Swift/Controllers/UIEvents/RequestChatUIEvent.h> +#include <Swift/Controllers/UIEvents/UIEventStream.h> +#include <Swift/Controllers/UIInterfaces/ChatListWindowFactory.h> +#include <Swift/Controllers/UIInterfaces/ChatWindow.h> +#include <Swift/Controllers/UIInterfaces/ChatWindowFactory.h> +#include <Swift/Controllers/UIInterfaces/JoinMUCWindowFactory.h> +#include <Swift/Controllers/UIInterfaces/MUCSearchWindowFactory.h> +#include <Swift/Controllers/UIInterfaces/WhiteboardWindowFactory.h> +#include <Swift/Controllers/UnitTest/MockChatWindow.h> +#include <Swift/Controllers/WhiteboardManager.h> +#include <Swift/Controllers/XMPPEvents/EventController.h> + using namespace Swift; @@ -101,19 +110,33 @@ public: ftManager_ = new DummyFileTransferManager(); ftOverview_ = new FileTransferOverview(ftManager_); + avatarManager_ = new NullAvatarManager(); + wbSessionManager_ = new WhiteboardSessionManager(iqRouter_, stanzaChannel_, presenceOracle_, entityCapsManager_); + wbManager_ = new WhiteboardManager(whiteboardWindowFactory_, uiEventStream_, nickResolver_, wbSessionManager_); + highlightManager_ = new HighlightManager(settings_); + crypto_ = PlatformCryptoProvider::create(); + vcardStorage_ = new VCardMemoryStorage(crypto_); + vcardManager_ = new VCardManager(jid_, iqRouter_, vcardStorage_); mocks_->ExpectCall(chatListWindowFactory_, ChatListWindowFactory::createChatListWindow).With(uiEventStream_).Return(chatListWindow_); - manager_ = new ChatsManager(jid_, stanzaChannel_, iqRouter_, eventController_, chatWindowFactory_, joinMUCWindowFactory_, nickResolver_, presenceOracle_, directedPresenceSender_, uiEventStream_, chatListWindowFactory_, true, NULL, mucRegistry_, entityCapsManager_, mucManager_, mucSearchWindowFactory_, profileSettings_, ftOverview_, xmppRoster_, false, settings_); + clientBlockListManager_ = new ClientBlockListManager(iqRouter_); + manager_ = new ChatsManager(jid_, stanzaChannel_, iqRouter_, eventController_, chatWindowFactory_, joinMUCWindowFactory_, nickResolver_, presenceOracle_, directedPresenceSender_, uiEventStream_, chatListWindowFactory_, true, NULL, mucRegistry_, entityCapsManager_, mucManager_, mucSearchWindowFactory_, profileSettings_, ftOverview_, xmppRoster_, false, settings_, NULL, wbManager_, highlightManager_, clientBlockListManager_, emoticons_, NULL, vcardManager_); - avatarManager_ = new NullAvatarManager(); manager_->setAvatarManager(avatarManager_); - }; + } void tearDown() { + delete highlightManager_; //delete chatListWindowFactory delete profileSettings_; delete avatarManager_; delete manager_; + delete clientBlockListManager_; + delete vcardManager_; + delete vcardStorage_; + delete crypto_; delete ftOverview_; delete ftManager_; + delete wbSessionManager_; + delete wbManager_; delete directedPresenceSender_; delete presenceSender_; @@ -434,4 +457,5 @@ private: message->setFrom(from); message->setID(id); + message->setBody("This will cause the window to open"); message->addPayload(boost::make_shared<DeliveryReceiptRequest>()); return message; @@ -460,4 +484,5 @@ private: UIEventStream* uiEventStream_; ChatListWindowFactory* chatListWindowFactory_; + WhiteboardWindowFactory* whiteboardWindowFactory_; MUCSearchWindowFactory* mucSearchWindowFactory_; MUCRegistry* mucRegistry_; @@ -471,4 +496,12 @@ private: FileTransferOverview* ftOverview_; FileTransferManager* ftManager_; + WhiteboardSessionManager* wbSessionManager_; + WhiteboardManager* wbManager_; + HighlightManager* highlightManager_; + ClientBlockListManager* clientBlockListManager_; + VCardManager* vcardManager_; + CryptoProvider* crypto_; + VCardStorage* vcardStorage_; + std::map<std::string, std::string> emoticons_; }; diff --git a/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp index d4fbcfd..bb22e43 100644 --- a/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp +++ b/Swift/Controllers/Chat/UnitTest/MUCControllerTest.cpp @@ -1,4 +1,4 @@ /* - * Copyright (c) 2010 Kevin Smith + * Copyright (c) 2010-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. @@ -8,6 +8,7 @@ #include <cppunit/extensions/TestFactoryRegistry.h> #include <boost/algorithm/string.hpp> -#include "3rdParty/hippomocks.h" +#include <hippomocks.h> +#include "Swiften/Base/foreach.h" #include "Swift/Controllers/XMPPEvents/EventController.h" #include "Swiften/Presence/DirectedPresenceSender.h" @@ -21,4 +22,5 @@ #include "Swift/Controllers/UIEvents/UIEventStream.h" #include "Swift/Controllers/UnitTest/MockChatWindow.h" +#include "Swiften/MUC/UnitTest/MockMUC.h" #include "Swiften/Client/DummyStanzaChannel.h" #include "Swiften/Queries/DummyIQChannel.h" @@ -27,4 +29,14 @@ #include "Swiften/Elements/MUCUserPayload.h" #include "Swiften/Disco/DummyEntityCapsProvider.h" +#include <Swiften/VCards/VCardMemoryStorage.h> +#include <Swiften/Crypto/PlatformCryptoProvider.h> +#include <Swiften/VCards/VCardManager.h> +#include <Swift/Controllers/Settings/DummySettingsProvider.h> +#include <Swift/Controllers/Chat/ChatMessageParser.h> +#include <Swift/Controllers/Chat/UserSearchController.h> +#include <Swift/Controllers/UIInterfaces/UserSearchWindowFactory.h> +#include <Swift/Controllers/Roster/Roster.h> +#include <Swift/Controllers/Roster/GroupRosterItem.h> +#include <Swiften/Crypto/CryptoProvider.h> using namespace Swift; @@ -40,8 +52,11 @@ class MUCControllerTest : public CppUnit::TestFixture { CPPUNIT_TEST(testMessageWithEmptyLabelItem); CPPUNIT_TEST(testMessageWithLabelItem); + CPPUNIT_TEST(testCorrectMessageWithLabelItem); + CPPUNIT_TEST(testRoleAffiliationStates); CPPUNIT_TEST_SUITE_END(); public: void setUp() { + crypto_ = boost::shared_ptr<CryptoProvider>(PlatformCryptoProvider::create()); self_ = JID("girl@wonderland.lit/rabbithole"); nick_ = "aLiCe"; @@ -53,4 +68,5 @@ public: eventController_ = new EventController(); chatWindowFactory_ = mocks_->InterfaceMock<ChatWindowFactory>(); + userSearchWindowFactory_ = mocks_->InterfaceMock<UserSearchWindowFactory>(); presenceOracle_ = new PresenceOracle(stanzaChannel_); presenceSender_ = new StanzaChannelPresenceSender(stanzaChannel_); @@ -62,12 +78,21 @@ public: mucRegistry_ = new MUCRegistry(); entityCapsProvider_ = new DummyEntityCapsProvider(); - muc_ = boost::make_shared<MUC>(stanzaChannel_, iqRouter_, directedPresenceSender_, mucJID_, mucRegistry_); + settings_ = new DummySettingsProvider(); + highlightManager_ = new HighlightManager(settings_); + muc_ = boost::make_shared<MockMUC>(mucJID_); mocks_->ExpectCall(chatWindowFactory_, ChatWindowFactory::createChatWindow).With(muc_->getJID(), uiEventStream_).Return(window_); - controller_ = new MUCController (self_, muc_, boost::optional<std::string>(), nick_, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory, eventController_, entityCapsProvider_); - }; + chatMessageParser_ = boost::make_shared<ChatMessageParser>(std::map<std::string, std::string>(), highlightManager_->getRules(), true); + vcardStorage_ = new VCardMemoryStorage(crypto_.get()); + vcardManager_ = new VCardManager(self_, iqRouter_, vcardStorage_); + controller_ = new MUCController (self_, muc_, boost::optional<std::string>(), nick_, stanzaChannel_, iqRouter_, chatWindowFactory_, presenceOracle_, avatarManager_, uiEventStream_, false, timerFactory, eventController_, entityCapsProvider_, NULL, NULL, mucRegistry_, highlightManager_, chatMessageParser_, false, NULL, vcardManager_); + } void tearDown() { - delete entityCapsProvider_; delete controller_; + delete vcardManager_; + delete vcardStorage_; + delete highlightManager_; + delete settings_; + delete entityCapsProvider_; delete eventController_; delete presenceOracle_; @@ -185,5 +210,5 @@ public: void testMessageWithLabelItem() { - SecurityLabel::ref label = boost::make_shared<SecurityLabel>(); + boost::shared_ptr<SecurityLabel> label = boost::make_shared<SecurityLabel>(); label->setLabel("a"); SecurityLabelsCatalog::Item labelItem; @@ -211,4 +236,42 @@ public: } + void testCorrectMessageWithLabelItem() { + boost::shared_ptr<SecurityLabel> label = boost::make_shared<SecurityLabel>(); + label->setLabel("a"); + SecurityLabelsCatalog::Item labelItem; + labelItem.setSelector("Bob"); + labelItem.setLabel(label); + boost::shared_ptr<SecurityLabel> label2 = boost::make_shared<SecurityLabel>(); + label->setLabel("b"); + SecurityLabelsCatalog::Item labelItem2; + labelItem2.setSelector("Charlie"); + labelItem2.setLabel(label2); + window_->label_ = labelItem; + boost::shared_ptr<DiscoInfo> features = boost::make_shared<DiscoInfo>(); + features->addFeature(DiscoInfo::SecurityLabelsCatalogFeature); + controller_->setAvailableServerFeatures(features); + IQ::ref iq = iqChannel_->iqs_[iqChannel_->iqs_.size() - 1]; + SecurityLabelsCatalog::ref labelPayload = boost::make_shared<SecurityLabelsCatalog>(); + labelPayload->addItem(labelItem); + IQ::ref result = IQ::createResult(self_, iq->getID(), labelPayload); + iqChannel_->onIQReceived(result); + std::string messageBody("agamemnon"); + window_->onSendMessageRequest(messageBody, false); + boost::shared_ptr<Stanza> rawStanza = stanzaChannel_->sentStanzas[stanzaChannel_->sentStanzas.size() - 1]; + Message::ref message = boost::dynamic_pointer_cast<Message>(rawStanza); + CPPUNIT_ASSERT_EQUAL(iq->getTo(), result->getFrom()); + CPPUNIT_ASSERT(window_->labelsEnabled_); + CPPUNIT_ASSERT(stanzaChannel_->isAvailable()); /* Otherwise will prevent sends. */ + CPPUNIT_ASSERT(message); + CPPUNIT_ASSERT_EQUAL(messageBody, message->getBody()); + CPPUNIT_ASSERT_EQUAL(label, message->getPayload<SecurityLabel>()); + window_->label_ = labelItem2; + window_->onSendMessageRequest(messageBody, true); + rawStanza = stanzaChannel_->sentStanzas[stanzaChannel_->sentStanzas.size() - 1]; + message = boost::dynamic_pointer_cast<Message>(rawStanza); + CPPUNIT_ASSERT_EQUAL(messageBody, message->getBody()); + CPPUNIT_ASSERT_EQUAL(label, message->getPayload<SecurityLabel>()); + } + void checkEqual(const std::vector<NickJoinPart>& expected, const std::vector<NickJoinPart>& actual) { CPPUNIT_ASSERT_EQUAL(expected.size(), actual.size()); @@ -258,11 +321,11 @@ public: std::vector<NickJoinPart> list; list.push_back(NickJoinPart("Kev", Join)); - CPPUNIT_ASSERT_EQUAL(std::string("Kev has entered the room"), MUCController::generateJoinPartString(list)); + CPPUNIT_ASSERT_EQUAL(std::string("Kev has entered the room"), MUCController::generateJoinPartString(list, false)); list.push_back(NickJoinPart("Remko", Part)); - CPPUNIT_ASSERT_EQUAL(std::string("Kev has entered the room and Remko has left the room"), MUCController::generateJoinPartString(list)); + CPPUNIT_ASSERT_EQUAL(std::string("Kev has entered the room and Remko has left the room"), MUCController::generateJoinPartString(list, false)); list.push_back(NickJoinPart("Bert", Join)); - CPPUNIT_ASSERT_EQUAL(std::string("Kev and Bert have entered the room and Remko has left the room"), MUCController::generateJoinPartString(list)); + CPPUNIT_ASSERT_EQUAL(std::string("Kev and Bert have entered the room and Remko has left the room"), MUCController::generateJoinPartString(list, false)); list.push_back(NickJoinPart("Ernie", Join)); - CPPUNIT_ASSERT_EQUAL(std::string("Kev, Bert and Ernie have entered the room and Remko has left the room"), MUCController::generateJoinPartString(list)); + CPPUNIT_ASSERT_EQUAL(std::string("Kev, Bert and Ernie have entered the room and Remko has left the room"), MUCController::generateJoinPartString(list, false)); } @@ -270,11 +333,74 @@ public: std::vector<NickJoinPart> list; list.push_back(NickJoinPart("Kev", JoinThenPart)); - CPPUNIT_ASSERT_EQUAL(std::string("Kev has entered then left the room"), MUCController::generateJoinPartString(list)); + CPPUNIT_ASSERT_EQUAL(std::string("Kev has entered then left the room"), MUCController::generateJoinPartString(list, false)); list.push_back(NickJoinPart("Remko", Part)); - CPPUNIT_ASSERT_EQUAL(std::string("Remko has left the room and Kev has entered then left the room"), MUCController::generateJoinPartString(list)); + CPPUNIT_ASSERT_EQUAL(std::string("Remko has left the room and Kev has entered then left the room"), MUCController::generateJoinPartString(list, false)); list.push_back(NickJoinPart("Bert", PartThenJoin)); - CPPUNIT_ASSERT_EQUAL(std::string("Remko has left the room, Kev has entered then left the room and Bert has left then returned to the room"), MUCController::generateJoinPartString(list)); + CPPUNIT_ASSERT_EQUAL(std::string("Remko has left the room, Kev has entered then left the room and Bert has left then returned to the room"), MUCController::generateJoinPartString(list, false)); list.push_back(NickJoinPart("Ernie", JoinThenPart)); - CPPUNIT_ASSERT_EQUAL(std::string("Remko has left the room, Kev and Ernie have entered then left the room and Bert has left then returned to the room"), MUCController::generateJoinPartString(list)); + CPPUNIT_ASSERT_EQUAL(std::string("Remko has left the room, Kev and Ernie have entered then left the room and Bert has left then returned to the room"), MUCController::generateJoinPartString(list, false)); + } + + JID jidFromOccupant(const MUCOccupant& occupant) { + return JID(mucJID_.toString()+"/"+occupant.getNick()); + } + + void testRoleAffiliationStates() { + + typedef std::map<std::string, MUCOccupant> occupant_map; + occupant_map occupants; + occupants.insert(occupant_map::value_type("Kev", MUCOccupant("Kev", MUCOccupant::Participant, MUCOccupant::Owner))); + occupants.insert(occupant_map::value_type("Remko", MUCOccupant("Remko", MUCOccupant::Participant, MUCOccupant::Owner))); + occupants.insert(occupant_map::value_type("Bert", MUCOccupant("Bert", MUCOccupant::Participant, MUCOccupant::Owner))); + occupants.insert(occupant_map::value_type("Ernie", MUCOccupant("Ernie", MUCOccupant::Participant, MUCOccupant::Owner))); + + /* populate the MUC with fake users */ + typedef const std::pair<std::string,MUCOccupant> occupantIterator; + foreach(occupantIterator &occupant, occupants) { + muc_->insertOccupant(occupant.second); + } + + std::vector<MUCOccupant> alterations; + alterations.push_back(MUCOccupant("Kev", MUCOccupant::Visitor, MUCOccupant::Admin)); + alterations.push_back(MUCOccupant("Remko", MUCOccupant::Moderator, MUCOccupant::Member)); + alterations.push_back(MUCOccupant("Bert", MUCOccupant::Visitor, MUCOccupant::Outcast)); + alterations.push_back(MUCOccupant("Ernie", MUCOccupant::NoRole, MUCOccupant::Member)); + alterations.push_back(MUCOccupant("Bert", MUCOccupant::Moderator, MUCOccupant::Owner)); + alterations.push_back(MUCOccupant("Kev", MUCOccupant::Participant, MUCOccupant::Outcast)); + alterations.push_back(MUCOccupant("Bert", MUCOccupant::Visitor, MUCOccupant::NoAffiliation)); + alterations.push_back(MUCOccupant("Remko", MUCOccupant::NoRole, MUCOccupant::NoAffiliation)); + alterations.push_back(MUCOccupant("Ernie", MUCOccupant::Visitor, MUCOccupant::Outcast)); + + foreach(const MUCOccupant& alteration, alterations) { + /* perform an alteration to a user's role and affiliation */ + occupant_map::iterator occupant = occupants.find(alteration.getNick()); + CPPUNIT_ASSERT(occupant != occupants.end()); + const JID jid = jidFromOccupant(occupant->second); + /* change the affiliation, leave the role in place */ + muc_->changeAffiliation(jid, alteration.getAffiliation()); + occupant->second = MUCOccupant(occupant->first, occupant->second.getRole(), alteration.getAffiliation()); + testRoleAffiliationStatesVerify(occupants); + /* change the role, leave the affiliation in place */ + muc_->changeOccupantRole(jid, alteration.getRole()); + occupant->second = MUCOccupant(occupant->first, alteration.getRole(), occupant->second.getAffiliation()); + testRoleAffiliationStatesVerify(occupants); + } + } + + void testRoleAffiliationStatesVerify(const std::map<std::string, MUCOccupant> &occupants) { + /* verify that the roster is in sync */ + GroupRosterItem* group = window_->getRosterModel()->getRoot(); + foreach(RosterItem* rosterItem, group->getChildren()) { + GroupRosterItem* child = dynamic_cast<GroupRosterItem*>(rosterItem); + CPPUNIT_ASSERT(child); + foreach(RosterItem* childItem, child->getChildren()) { + ContactRosterItem* item = dynamic_cast<ContactRosterItem*>(childItem); + CPPUNIT_ASSERT(item); + std::map<std::string, MUCOccupant>::const_iterator occupant = occupants.find(item->getJID().getResource()); + CPPUNIT_ASSERT(occupant != occupants.end()); + CPPUNIT_ASSERT(item->getMUCRole() == occupant->second.getRole()); + CPPUNIT_ASSERT(item->getMUCAffiliation() == occupant->second.getAffiliation()); + } + } } @@ -282,5 +408,5 @@ private: JID self_; JID mucJID_; - MUC::ref muc_; + MockMUC::ref muc_; std::string nick_; DummyStanzaChannel* stanzaChannel_; @@ -289,4 +415,5 @@ private: EventController* eventController_; ChatWindowFactory* chatWindowFactory_; + UserSearchWindowFactory* userSearchWindowFactory_; MUCController* controller_; // NickResolver* nickResolver_; @@ -300,4 +427,10 @@ private: MUCRegistry* mucRegistry_; DummyEntityCapsProvider* entityCapsProvider_; + DummySettingsProvider* settings_; + HighlightManager* highlightManager_; + boost::shared_ptr<ChatMessageParser> chatMessageParser_; + boost::shared_ptr<CryptoProvider> crypto_; + VCardManager* vcardManager_; + VCardMemoryStorage* vcardStorage_; }; diff --git a/Swift/Controllers/Chat/UnitTest/MockChatListWindow.h b/Swift/Controllers/Chat/UnitTest/MockChatListWindow.h index 6ac8d4a..287c4b9 100644 --- a/Swift/Controllers/Chat/UnitTest/MockChatListWindow.h +++ b/Swift/Controllers/Chat/UnitTest/MockChatListWindow.h @@ -1,4 +1,4 @@ /* - * Copyright (c) 2011 Kevin Smith + * Copyright (c) 2011-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. @@ -13,12 +13,15 @@ namespace Swift { class MockChatListWindow : public ChatListWindow { public: - MockChatListWindow() {}; - virtual ~MockChatListWindow() {}; + MockChatListWindow() {} + virtual ~MockChatListWindow() {} void addMUCBookmark(const MUCBookmark& /*bookmark*/) {} void removeMUCBookmark(const MUCBookmark& /*bookmark*/) {} + void addWhiteboardSession(const ChatListWindow::Chat& /*chat*/) {} + void removeWhiteboardSession(const JID& /*jid*/) {} void setBookmarksEnabled(bool /*enabled*/) {} void setRecents(const std::list<ChatListWindow::Chat>& /*recents*/) {} void setUnreadCount(int /*unread*/) {} void clearBookmarks() {} + void setOnline(bool /*isOnline*/) {} }; diff --git a/Swift/Controllers/Chat/UserSearchController.cpp b/Swift/Controllers/Chat/UserSearchController.cpp index 839f4fa..f1849c9 100644 --- a/Swift/Controllers/Chat/UserSearchController.cpp +++ b/Swift/Controllers/Chat/UserSearchController.cpp @@ -1,4 +1,4 @@ /* - * Copyright (c) 2010 Kevin Smith + * Copyright (c) 2010-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. @@ -16,16 +16,22 @@ #include <Swiften/Disco/DiscoServiceWalker.h> #include <Swiften/VCards/VCardManager.h> +#include <Swiften/Presence/PresenceOracle.h> +#include <Swiften/Avatars/AvatarManager.h> #include <Swift/Controllers/ContactEditController.h> #include <Swift/Controllers/UIEvents/UIEventStream.h> #include <Swift/Controllers/UIEvents/RequestChatWithUserDialogUIEvent.h> #include <Swift/Controllers/UIEvents/RequestAddUserDialogUIEvent.h> +#include <Swift/Controllers/UIEvents/RequestInviteToMUCUIEvent.h> #include <Swift/Controllers/UIInterfaces/UserSearchWindow.h> #include <Swift/Controllers/UIInterfaces/UserSearchWindowFactory.h> #include <Swift/Controllers/Roster/RosterController.h> +#include <Swift/Controllers/ContactSuggester.h> namespace Swift { -UserSearchController::UserSearchController(Type type, const JID& jid, UIEventStream* uiEventStream, VCardManager* vcardManager, UserSearchWindowFactory* factory, IQRouter* iqRouter, RosterController* rosterController) : type_(type), jid_(jid), uiEventStream_(uiEventStream), vcardManager_(vcardManager), factory_(factory), iqRouter_(iqRouter), rosterController_(rosterController) { +UserSearchController::UserSearchController(Type type, const JID& jid, UIEventStream* uiEventStream, VCardManager* vcardManager, UserSearchWindowFactory* factory, IQRouter* iqRouter, RosterController* rosterController, ContactSuggester* contactSuggester, AvatarManager* avatarManager, PresenceOracle* presenceOracle) : type_(type), jid_(jid), uiEventStream_(uiEventStream), vcardManager_(vcardManager), factory_(factory), iqRouter_(iqRouter), rosterController_(rosterController), contactSuggester_(contactSuggester), avatarManager_(avatarManager), presenceOracle_(presenceOracle) { uiEventStream_->onUIEvent.connect(boost::bind(&UserSearchController::handleUIEvent, this, _1)); vcardManager_->onVCardChanged.connect(boost::bind(&UserSearchController::handleVCardChanged, this, _1, _2)); + avatarManager_->onAvatarChanged.connect(boost::bind(&UserSearchController::handleAvatarChanged, this, _1)); + presenceOracle_->onPresenceChange.connect(boost::bind(&UserSearchController::handlePresenceChanged, this, _1)); window_ = NULL; discoWalker_ = NULL; @@ -39,38 +45,61 @@ UserSearchController::~UserSearchController() { window_->onFormRequested.disconnect(boost::bind(&UserSearchController::handleFormRequested, this, _1)); window_->onSearchRequested.disconnect(boost::bind(&UserSearchController::handleSearch, this, _1, _2)); + window_->onJIDUpdateRequested.disconnect(boost::bind(&UserSearchController::handleJIDUpdateRequested, this, _1)); + window_->onJIDAddRequested.disconnect(boost::bind(&UserSearchController::handleJIDAddRequested, this, _1)); delete window_; } + presenceOracle_->onPresenceChange.disconnect(boost::bind(&UserSearchController::handlePresenceChanged, this, _1)); + avatarManager_->onAvatarChanged.disconnect(boost::bind(&UserSearchController::handleAvatarChanged, this, _1)); vcardManager_->onVCardChanged.disconnect(boost::bind(&UserSearchController::handleVCardChanged, this, _1, _2)); uiEventStream_->onUIEvent.disconnect(boost::bind(&UserSearchController::handleUIEvent, this, _1)); } +UserSearchWindow* UserSearchController::getUserSearchWindow() { + initializeUserWindow(); + assert(window_); + return window_; +} + +void UserSearchController::setCanInitiateImpromptuMUC(bool supportsImpromptu) { + if (!window_) { + initializeUserWindow(); + } + window_->setCanStartImpromptuChats(supportsImpromptu); +} + void UserSearchController::handleUIEvent(boost::shared_ptr<UIEvent> event) { bool handle = false; - boost::shared_ptr<RequestAddUserDialogUIEvent> request = boost::shared_ptr<RequestAddUserDialogUIEvent>(); - if (type_ == AddContact) { - if ((request = boost::dynamic_pointer_cast<RequestAddUserDialogUIEvent>(event))) { + boost::shared_ptr<RequestAddUserDialogUIEvent> addUserRequest = boost::shared_ptr<RequestAddUserDialogUIEvent>(); + RequestInviteToMUCUIEvent::ref inviteToMUCRequest = RequestInviteToMUCUIEvent::ref(); + switch (type_) { + case AddContact: + if ((addUserRequest = boost::dynamic_pointer_cast<RequestAddUserDialogUIEvent>(event))) { handle = true; } - } else { + break; + case StartChat: if (boost::dynamic_pointer_cast<RequestChatWithUserDialogUIEvent>(event)) { handle = true; } + break; + case InviteToChat: + if ((inviteToMUCRequest = boost::dynamic_pointer_cast<RequestInviteToMUCUIEvent>(event))) { + handle = true; } - if (handle) { - if (!window_) { - window_ = factory_->createUserSearchWindow(type_ == AddContact ? UserSearchWindow::AddContact : UserSearchWindow::ChatToContact, uiEventStream_, rosterController_->getGroups()); - window_->onNameSuggestionRequested.connect(boost::bind(&UserSearchController::handleNameSuggestionRequest, this, _1)); - window_->onFormRequested.connect(boost::bind(&UserSearchController::handleFormRequested, this, _1)); - window_->onSearchRequested.connect(boost::bind(&UserSearchController::handleSearch, this, _1, _2)); - window_->setSelectedService(JID(jid_.getDomain())); - window_->clear(); + break; } + if (handle) { + initializeUserWindow(); window_->show(); - if (request) { - const std::string& name = request->getPredefinedName(); - const JID& jid = request->getPredefinedJID(); + if (addUserRequest) { + const std::string& name = addUserRequest->getPredefinedName(); + const JID& jid = addUserRequest->getPredefinedJID(); if (!name.empty() && jid.isValid()) { window_->prepopulateJIDAndName(jid, name); } + } else if (inviteToMUCRequest) { + window_->setCanSupplyDescription(!inviteToMUCRequest->isImpromptu()); + window_->setJIDs(inviteToMUCRequest->getInvites()); + window_->setRoomJID(inviteToMUCRequest->getRoom()); } return; @@ -99,5 +128,4 @@ void UserSearchController::endDiscoWalker() { } - void UserSearchController::handleDiscoServiceFound(const JID& jid, boost::shared_ptr<DiscoInfo> info) { //bool isUserDirectory = false; @@ -167,4 +195,26 @@ void UserSearchController::handleNameSuggestionRequest(const JID &jid) { } +void UserSearchController::handleContactSuggestionsRequested(std::string text) { + const std::vector<JID> existingJIDs = window_->getJIDs(); + std::vector<Contact::ref> suggestions = contactSuggester_->getSuggestions(text, false); + /* do not suggest contacts that have already been added to the chat list */ + std::vector<Contact::ref>::iterator i = suggestions.begin(); + while (i != suggestions.end()) { + bool found = false; + foreach (const JID& jid, existingJIDs) { + if ((*i)->jid == jid) { + found = true; + break; + } + } + if (found) { + i = suggestions.erase(i); + } else { + i++; + } + } + window_->setContactSuggestions(suggestions); +} + void UserSearchController::handleVCardChanged(const JID& jid, VCard::ref vcard) { if (jid == suggestionsJID_) { @@ -172,4 +222,61 @@ void UserSearchController::handleVCardChanged(const JID& jid, VCard::ref vcard) suggestionsJID_ = JID(); } + handleJIDUpdateRequested(std::vector<JID>(1, jid)); +} + +void UserSearchController::handleAvatarChanged(const JID& jid) { + handleJIDUpdateRequested(std::vector<JID>(1, jid)); +} + +void UserSearchController::handlePresenceChanged(Presence::ref presence) { + handleJIDUpdateRequested(std::vector<JID>(1, presence->getFrom().toBare())); +} + +void UserSearchController::handleJIDUpdateRequested(const std::vector<JID>& jids) { + if (window_) { + std::vector<Contact::ref> updates; + foreach(const JID& jid, jids) { + updates.push_back(convertJIDtoContact(jid)); + } + window_->updateContacts(updates); + } +} + +void UserSearchController::handleJIDAddRequested(const std::vector<JID>& jids) { + std::vector<Contact::ref> contacts; + foreach(const JID& jid, jids) { + contacts.push_back(convertJIDtoContact(jid)); + } + window_->addContacts(contacts); +} + +Contact::ref UserSearchController::convertJIDtoContact(const JID& jid) { + Contact::ref contact = boost::make_shared<Contact>(); + contact->jid = jid; + + // name lookup + boost::optional<XMPPRosterItem> rosterItem = rosterController_->getItem(jid); + if (rosterItem && !rosterItem->getName().empty()) { + contact->name = rosterItem->getName(); + } else { + VCard::ref vcard = vcardManager_->getVCard(jid); + if (vcard && !vcard->getFullName().empty()) { + contact->name = vcard->getFullName(); + } else { + contact->name = jid.toString(); + } + } + + // presence lookup + Presence::ref presence = presenceOracle_->getHighestPriorityPresence(jid); + if (presence) { + contact->statusType = presence->getShow(); + } else { + contact->statusType = StatusShow::None; + } + + // avatar lookup + contact->avatarPath = avatarManager_->getAvatarPath(jid); + return contact; } @@ -179,3 +286,30 @@ void UserSearchController::handleDiscoWalkFinished() { } +void UserSearchController::initializeUserWindow() { + if (!window_) { + UserSearchWindow::Type windowType = UserSearchWindow::AddContact; + switch(type_) { + case AddContact: + windowType = UserSearchWindow::AddContact; + break; + case StartChat: + windowType = UserSearchWindow::ChatToContact; + break; + case InviteToChat: + windowType = UserSearchWindow::InviteToChat; + break; + } + + window_ = factory_->createUserSearchWindow(windowType, uiEventStream_, rosterController_->getGroups()); + window_->onNameSuggestionRequested.connect(boost::bind(&UserSearchController::handleNameSuggestionRequest, this, _1)); + window_->onFormRequested.connect(boost::bind(&UserSearchController::handleFormRequested, this, _1)); + window_->onSearchRequested.connect(boost::bind(&UserSearchController::handleSearch, this, _1, _2)); + window_->onContactSuggestionsRequested.connect(boost::bind(&UserSearchController::handleContactSuggestionsRequested, this, _1)); + window_->onJIDUpdateRequested.connect(boost::bind(&UserSearchController::handleJIDUpdateRequested, this, _1)); + window_->onJIDAddRequested.connect(boost::bind(&UserSearchController::handleJIDAddRequested, this, _1)); + window_->setSelectedService(JID(jid_.getDomain())); + window_->clear(); + } +} + } diff --git a/Swift/Controllers/Chat/UserSearchController.h b/Swift/Controllers/Chat/UserSearchController.h index ce0754c..d630580 100644 --- a/Swift/Controllers/Chat/UserSearchController.h +++ b/Swift/Controllers/Chat/UserSearchController.h @@ -1,4 +1,4 @@ /* - * Copyright (c) 2010 Kevin Smith + * Copyright (c) 2010-2014 Kevin Smith * Licensed under the GNU General Public License v3. * See Documentation/Licenses/GPLv3.txt for more information. @@ -8,15 +8,18 @@ #include <boost/shared_ptr.hpp> + #include <map> #include <vector> -#include <Swiften/Base/boost_bsignals.h> - -#include <Swiften/Elements/SearchPayload.h> #include <string> -#include <Swiften/JID/JID.h> + +#include <Swift/Controllers/Contact.h> +#include <Swiften/Base/boost_bsignals.h> #include <Swiften/Elements/DiscoInfo.h> #include <Swiften/Elements/DiscoItems.h> #include <Swiften/Elements/ErrorPayload.h> +#include <Swiften/Elements/Presence.h> +#include <Swiften/Elements/SearchPayload.h> #include <Swiften/Elements/VCard.h> +#include <Swiften/JID/JID.h> namespace Swift { @@ -29,4 +32,7 @@ namespace Swift { class RosterController; class VCardManager; + class ContactSuggester; + class AvatarManager; + class PresenceOracle; class UserSearchResult { @@ -42,8 +48,11 @@ namespace Swift { class UserSearchController { public: - enum Type {AddContact, StartChat}; - UserSearchController(Type type, const JID& jid, UIEventStream* uiEventStream, VCardManager* vcardManager, UserSearchWindowFactory* userSearchWindowFactory, IQRouter* iqRouter, RosterController* rosterController); + enum Type {AddContact, StartChat, InviteToChat}; + UserSearchController(Type type, const JID& jid, UIEventStream* uiEventStream, VCardManager* vcardManager, UserSearchWindowFactory* userSearchWindowFactory, IQRouter* iqRouter, RosterController* rosterController, ContactSuggester* contactSuggester, AvatarManager* avatarManager, PresenceOracle* presenceOracle); ~UserSearchController(); + UserSearchWindow* getUserSearchWindow(); + void setCanInitiateImpromptuMUC(bool supportsImpromptu); + private: void handleUIEvent(boost::shared_ptr<UIEvent> event); @@ -55,6 +64,13 @@ namespace Swift { void handleSearchResponse(boost::shared_ptr<SearchPayload> results, ErrorPayload::ref error); void handleNameSuggestionRequest(const JID& jid); + void handleContactSuggestionsRequested(std::string text); void handleVCardChanged(const JID& jid, VCard::ref vcard); + void handleAvatarChanged(const JID& jid); + void handlePresenceChanged(Presence::ref presence); + void handleJIDUpdateRequested(const std::vector<JID>& jids); + void handleJIDAddRequested(const std::vector<JID>& jids); + Contact::ref convertJIDtoContact(const JID& jid); void endDiscoWalker(); + void initializeUserWindow(); private: @@ -69,4 +85,7 @@ namespace Swift { UserSearchWindow* window_; DiscoServiceWalker* discoWalker_; + ContactSuggester* contactSuggester_; + AvatarManager* avatarManager_; + PresenceOracle* presenceOracle_; }; } |