461 lines
14 KiB
C++
461 lines
14 KiB
C++
/* 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<std::string> 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<bool> 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 (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 (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<bool> 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);
|
|
}
|
|
}
|