/* Copyright (c) 2010 Katharine Berry All rights reserved. * * @file growlmanager.cpp * @Implementation of desktop notification system (aka growl). * * Redistribution and use in source and binary forms, with or * without modification, are permitted provided that the following * conditions are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * 3. Neither the name Katharine Berry nor the names of any contributors * may be used to endorse or promote products derived from this * software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY KATHARINE BERRY AND CONTRIBUTORS "AS IS" * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL KATHARINE BERRY OR CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #include "llviewerprecompiledheaders.h" #include "growlmanager.h" #include "fscommon.h" #include "growlnotifier.h" #include "llagent.h" #include "llagentdata.h" #include "llappviewer.h" #include "llfloaterimnearbychathandler.h" #include "llimview.h" #include "llnotificationmanager.h" #include "llscriptfloater.h" #include "llsdserialize.h" #include "llstartup.h" #include "llurlregistry.h" // for SLURL parsing #include "llviewercontrol.h" #include "llviewerwindow.h" #include "llwindow.h" // Platform-specific includes #ifndef LL_LINUX #include "growlnotifierwin.h" #elif LL_LINUX #include "desktopnotifierlinux.h" #endif GrowlManager *gGrowlManager = NULL; GrowlManager::GrowlManager() : LLEventTimer(GROWL_THROTTLE_CLEANUP_PERIOD), mNotifier(NULL), mNotificationConnection(), mInstantMessageConnection(), mScriptDialogConnection(), mChatMessageConnection() { // Create a notifier appropriate to the platform. #ifndef LL_LINUX mNotifier = new GrowlNotifierWin(); LL_INFOS("GrowlManagerInit") << "Created GrowlNotifierWin." << LL_ENDL; #elif LL_LINUX mNotifier = new DesktopNotifierLinux(); LL_INFOS("GrowlManagerInit") << "Created DesktopNotifierLinux." << LL_ENDL; #else mNotifier = new GrowlNotifier(); LL_INFOS("GrowlManagerInit") << "Created generic GrowlNotifier." << LL_ENDL; #endif #ifndef LL_LINUX if (mNotifier) { // Need to call loadConfig for Windows first before we know if // Growl is usable -Ansariel loadConfig(); if (!mNotifier->isUsable()) { LL_INFOS("GrowlManagerInit") << "Growl not available" << LL_ENDL; return; } } else { LL_INFOS("GrowlManagerInit") << "Growl not available" << LL_ENDL; return; } #else // Don't do anything more if Growl isn't usable. if( !mNotifier || !mNotifier->isUsable()) { LL_INFOS("GrowlManagerInit") << "Growl not available" << LL_ENDL; return; } loadConfig(); #endif // Hook into LLNotifications... // We hook into all of them, even though (at the time of writing) nothing uses "alert", so more notifications can be added easily. mGrowlNotificationsChannel = new LLNotificationChannel("GrowlNotifications", "Visible", &filterOldNotifications); mNotificationConnection = LLNotifications::instance().getChannel("GrowlNotifications")->connectChanged(&onLLNotification); // Also hook into IM notifications. mInstantMessageConnection = LLIMModel::instance().addNewMsgCallback(&GrowlManager::onInstantMessage); // Hook into script dialogs mScriptDialogConnection = LLScriptFloaterManager::instance().addNewObjectCallback(&GrowlManager::onScriptDialog); // Hook into new chat messages (needed for object IMs that will go into nearby chat) mChatMessageConnection = LLNotificationsUI::LLNotificationManager::instance().getChatHandler()->addNewChatCallback(&GrowlManager::onNearbyChatMessage); } GrowlManager::~GrowlManager() { if (mNotificationConnection.connected()) { mNotificationConnection.disconnect(); } if (mInstantMessageConnection.connected()) { mInstantMessageConnection.disconnect(); } if (mScriptDialogConnection.connected()) { mScriptDialogConnection.disconnect(); } if (mChatMessageConnection.connected()) { mChatMessageConnection.disconnect(); } if (mNotifier) { delete mNotifier; } } void GrowlManager::loadConfig() { std::string config_file = gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, "growl_notifications.xml"); if (config_file.empty()) { LL_WARNS("GrowlConfig") << "Couldn't find growl_notifications.xml" << LL_ENDL; return; } LL_INFOS("GrowlConfig") << "Loading growl notification config from " << config_file << LL_ENDL; llifstream configs(config_file.c_str()); LLSD notificationLLSD; std::set notificationTypes; notificationTypes.insert(GROWL_KEYWORD_ALERT_TYPE); notificationTypes.insert(GROWL_IM_MESSAGE_TYPE); if (configs.is_open()) { LLSDSerialize::fromXML(notificationLLSD, configs); for (LLSD::map_iterator itr = notificationLLSD.beginMap(); itr != notificationLLSD.endMap(); ++itr) { GrowlNotification ntype; ntype.growlName = itr->second.get("GrowlName").asString(); notificationTypes.insert(ntype.growlName); if (itr->second.has("GrowlTitle")) { ntype.growlTitle = itr->second.get("GrowlTitle").asString(); } if (itr->second.has("GrowlBody")) { ntype.growlBody = itr->second.get("GrowlBody").asString(); } if (itr->second.has("UseDefaultTextForTitle")) { ntype.useDefaultTextForTitle = itr->second.get("UseDefaultTextForTitle").asBoolean(); } else { ntype.useDefaultTextForTitle = false; } if (itr->second.has("UseDefaultTextForBody")) { ntype.useDefaultTextForBody = itr->second.get("UseDefaultTextForBody").asBoolean(); } else { ntype.useDefaultTextForBody = false; } if (!ntype.useDefaultTextForBody && !ntype.useDefaultTextForTitle && ntype.growlBody.empty() && ntype.growlTitle.empty()) { ntype.useDefaultTextForBody = true; } this->mNotifications[itr->first] = ntype; } configs.close(); this->mNotifier->registerApplication("Firestorm Viewer", notificationTypes); } else { LL_WARNS("GrowlConfig") << "Couldn't open growl config file." << LL_ENDL; } } static LLTrace::BlockTimerStatHandle FTM_GROWL_NOTIFY("Growl Notify"); void GrowlManager::performNotification(const std::string& title, const std::string& message, const std::string& type) { if (LLAppViewer::instance()->isExiting()) { return; } static LLCachedControl enabled(gSavedSettings, "FSEnableGrowl"); if (!enabled) { return; } if (!shouldNotify()) { return; } if (mNotifier->needsThrottle()) { const U64 now = LLTimer::getTotalTime(); if (mTitleTimers.find(title) != mTitleTimers.end()) { if (mTitleTimers[title] > now - GROWL_THROTTLE_TIME) { LL_WARNS("GrowlNotify") << "Discarded notification with title '" << title << "' due to throttle" << LL_ENDL; mTitleTimers[title] = now; return; } } mTitleTimers[title] = now; } { LL_RECORD_BLOCK_TIME(FTM_GROWL_NOTIFY); mNotifier->showNotification(title, message.substr(0, GROWL_MAX_BODY_LENGTH), type); } } bool GrowlManager::tick() { mTitleTimers.clear(); return false; } bool GrowlManager::onLLNotification(const LLSD& notice) { if (notice["sigtype"].asString() != "add") { return false; } LLNotificationPtr notification = LLNotifications::instance().find(notice["id"].asUUID()); const std::string name = notification->getName(); LLSD substitutions = notification->getSubstitutions(); if (gGrowlManager->mNotifications.find(name) != gGrowlManager->mNotifications.end()) { GrowlNotification* growl_notification = &gGrowlManager->mNotifications[name]; std::string body = ""; std::string title = ""; if (growl_notification->useDefaultTextForTitle) { title = notification->getMessage(); } else if (!growl_notification->growlTitle.empty()) { title = growl_notification->growlTitle; LLStringUtil::format(title, substitutions); } if (growl_notification->useDefaultTextForBody) { body = notification->getMessage(); } else if (!growl_notification->growlBody.empty()) { body = growl_notification->growlBody; LLStringUtil::format(body, substitutions); } if (name == "ObjectGiveItem" || name == "OwnObjectGiveItem" || name == "ObjectGiveItemUnknownUser" || name == "UserGiveItem" || name == "SystemMessageTip" || LLStringUtil::startsWith(name, "TeleportOffered") || name == "TeleportRequest") { LLUrlMatch urlMatch; LLWString newLine = utf8str_to_wstring(body); LLWString workLine = utf8str_to_wstring(body); while (LLUrlRegistry::instance().findUrl(workLine, urlMatch) && !urlMatch.getUrl().empty()) { LLWStringUtil::replaceString(newLine, utf8str_to_wstring(urlMatch.getMatchedText()), utf8str_to_wstring(urlMatch.getLabel())); // Remove the URL from the work line so we don't end in a loop in case of regular URLs! // findUrl will always return the very first URL in a string workLine = workLine.erase(0, urlMatch.getEnd() + 1); } body = wstring_to_utf8str(newLine); } gGrowlManager->performNotification(title, body, growl_notification->growlName); } return false; } bool GrowlManager::filterOldNotifications(LLNotificationPtr pNotification) { // *HACK: I don't see any better way to avoid getting old, persisted messages... return (pNotification->getDate().secondsSinceEpoch() >= LLDate::now().secondsSinceEpoch() - 10); } void GrowlManager::onInstantMessage(const LLSD& im) { LLIMModel::LLIMSession* session = LLIMModel::instance().findIMSession(im["session_id"].asUUID()); if (session->isP2PSessionType() && // Must be P2P !im["is_announcement"].asBoolean() && // Not an announcement (incoming IM, autoresponse sent info...) (!im["keyword_alert_performed"].asBoolean() || !gSavedSettings.getBOOL("FSFilterGrowlKeywordDuplicateIMs"))) // Not keyword or show duplicate IMs due to keywords { // Don't show messages from ourselves or the system. const LLUUID from_id = im["from_id"].asUUID(); if (from_id.isNull() || from_id == gAgentID) { return; } std::string message = im["message"].asString(); if (FSCommon::is_irc_me_prefix(message)) { message = message.substr(3); } LLAvatarNameCache::get(from_id, boost::bind(&GrowlManager::onAvatarNameCache, _2, message, GROWL_IM_MESSAGE_TYPE)); } } //static void GrowlManager::onScriptDialog(const LLSD& data) { LLNotificationPtr notification = LLNotifications::instance().find(data["notification_id"].asUUID()); const std::string name = notification->getName(); LLSD substitutions = notification->getSubstitutions(); if (gGrowlManager->mNotifications.find(name) != gGrowlManager->mNotifications.end()) { GrowlNotification* growl_notification = &gGrowlManager->mNotifications[name]; std::string body = ""; std::string title = ""; if (growl_notification->useDefaultTextForTitle) { title = notification->getMessage(); } else if (!growl_notification->growlTitle.empty()) { title = growl_notification->growlTitle; LLStringUtil::format(title, substitutions); } if (growl_notification->useDefaultTextForBody) { body = notification->getMessage(); } else if (!growl_notification->growlBody.empty()) { body = growl_notification->growlBody; LLStringUtil::format(body, substitutions); } gGrowlManager->performNotification(title, body, growl_notification->growlName); } } void GrowlManager::onNearbyChatMessage(const LLSD& chat) { if ((EChatType)chat["chat_type"].asInteger() == CHAT_TYPE_IM) { std::string message = chat["message"].asString(); if (FSCommon::is_irc_me_prefix(message)) { message = message.substr(3); } if ((EChatSourceType)chat["source"].asInteger() == CHAT_SOURCE_AGENT) { LLAvatarNameCache::get(chat["from_id"].asUUID(), boost::bind(&GrowlManager::onAvatarNameCache, _2, message, GROWL_IM_MESSAGE_TYPE)); } else { gGrowlManager->performNotification(chat["from"].asString(), message, GROWL_IM_MESSAGE_TYPE); } } } //static void GrowlManager::onAvatarNameCache(const LLAvatarName& av_name, const std::string& message, const std::string& type) { const std::string sender = FSCommon::getAvatarNameByDisplaySettings(av_name); notify(sender, message, type); } bool GrowlManager::shouldNotify() { // This magic stolen from llappviewer.cpp. LLViewerWindow::getActive lies. static LLCachedControl activated(gSavedSettings, "FSGrowlWhenActive"); if (LLStartUp::getStartupState() < STATE_STARTED) { return false; } if (gAgent.isDoNotDisturb()) { return false; } return (activated || (!gViewerWindow->getWindow()->getVisible() || !gFocusMgr.getAppHasFocus())); } // static void GrowlManager::initiateManager() { gGrowlManager = new GrowlManager(); } // static void GrowlManager::destroyManager() { if (gGrowlManager) { delete gGrowlManager; gGrowlManager = NULL; } } // static bool GrowlManager::isUsable() { return (gGrowlManager && gGrowlManager->mNotifier && gGrowlManager->mNotifier->isUsable()); } // static void GrowlManager::notify(const std::string& title, const std::string& message, const std::string& type) { if (isUsable()) { gGrowlManager->performNotification(title, message, type); } }