diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 1da8b911af..9fa29cc175 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -187,6 +187,8 @@ set(viewer_SOURCE_FILES lfsimfeaturehandler.cpp llflickrconnect.cpp llfloaterflickr.cpp + omnifilter.cpp + omnifilterengine.cpp fsprimfeedconnect.cpp fsfloaterprimfeed.cpp llpanelopenregionsettings.cpp @@ -1036,6 +1038,8 @@ set(viewer_HEADER_FILES fsfloaterprimfeed.h # [Legacy Bake] llagentwearablesfetch.h + omnifilter.h + omnifilterengine.h vjlocalmesh.h vjfloaterlocalmesh.h vjlocalmeshimportdae.h diff --git a/indra/newview/app_settings/commands.xml b/indra/newview/app_settings/commands.xml index 57d357f33a..ce287d289d 100644 --- a/indra/newview/app_settings/commands.xml +++ b/indra/newview/app_settings/commands.xml @@ -680,4 +680,15 @@ is_running_parameters="performance" checkbox_control="AutoTuneFPS" /> + diff --git a/indra/newview/app_settings/settings.xml b/indra/newview/app_settings/settings.xml index fcb09ca7c3..8de0df9d1d 100644 --- a/indra/newview/app_settings/settings.xml +++ b/indra/newview/app_settings/settings.xml @@ -27193,5 +27193,16 @@ Change of this parameter will affect the layout of buttons in notification toast Value 0 + OmnifilterEnabled + + Comment + The operating state of the Omnifilter. + Persist + 1 + Type + Boolean + Value + 1 + diff --git a/indra/newview/llimprocessing.cpp b/indra/newview/llimprocessing.cpp index f9c2919a79..21d0da1e93 100644 --- a/indra/newview/llimprocessing.cpp +++ b/indra/newview/llimprocessing.cpp @@ -79,6 +79,7 @@ #include "llgiveinventory.h" #include "lllandmarkactions.h" #include "llviewernetwork.h" +#include "omnifilterengine.h" // Omnifilter support #include "sound_ids.h" #include "NACLantispam.h" @@ -721,6 +722,100 @@ void LLIMProcessing::processNewMessage(LLUUID from_id, *----------------------------------------------------- */ + // Omnifilter support + OmnifilterEngine::Haystack haystack; + haystack.mContent = message; + haystack.mSenderName = agentName; + haystack.mOwnerID = from_id; + + switch (dialog) + { + case IM_NOTHING_SPECIAL: // this is the type for regular IMs + { + haystack.mType = OmnifilterEngine::eType::InstantMessage; + break; + } + case IM_SESSION_SEND: // this is the type for regular group IMs + case IM_SESSION_INVITE: // this is the type for group IM sessions started by ourselves or conferences + case IM_DO_NOT_DISTURB_AUTO_RESPONSE: + { + haystack.mType = OmnifilterEngine::eType::GroupChat; + break; + } + case IM_FROM_TASK: + { + // haystack.mSenderName = agentName; // Object name + // haystack.mOwnerID = from_id; // object owner UUID + // haystack.mContent = message; // IM content + haystack.mType = OmnifilterEngine::eType::ObjectInstantMessage; + break; + } + case IM_GROUP_INVITATION: + { + // haystack.mSenderName = agentName; // Inviter "first.last" + // haystack.mOwnerID = from_id; // group ID + haystack.mType = OmnifilterEngine::eType::GroupInvite; + break; + } + case IM_INVENTORY_OFFERED: + { + // haystack.mSenderName = agentName; // Sender "First Last" + // haystack.mOwnerID = from_id; // Sender UUID + // haystack.mContent = message; // item/folder name + haystack.mType = OmnifilterEngine::eType::InventoryOffer; + break; + } + case IM_FRIENDSHIP_OFFERED: + { + // haystack.mSenderName = agentName; // Sender "First Last" + // haystack.mOwnerID = from_id; // Sender UUID + // haystack.mContent = message; // friendship message + haystack.mType = OmnifilterEngine::eType::FriendshipOffer; + break; + } + case IM_LURE_USER: + { + // haystack.mSenderName = agentName; // Sender "First Last" + // haystack.mOwnerID = from_id; // Sender UUID + // haystack.mContent = message; // teleport message + haystack.mType = OmnifilterEngine::eType::Lure; + break; + } + case IM_TELEPORT_REQUEST: + { + // haystack.mSenderName = agentName; // Sender "First Last" + // haystack.mOwnerID = from_id; // Sender UUID + // haystack.mContent = message; // teleport message + haystack.mType = OmnifilterEngine::eType::TeleportRequest; + break; + } + case IM_GROUP_NOTICE: + { + // haystack.mSenderName = agentName; // Notice Sender "First Last" + // haystack.mOwnerID = from_id; // Notice Sender UUID + // haystack.mContent = message; // notice message + haystack.mType = OmnifilterEngine::eType::GroupNotice; + break; + } + default: + { + LL_DEBUGS("Omnifilter") << "unhandled IM type " << (U32)dialog << LL_ENDL; + break; + } + } + + const OmnifilterEngine::Needle* needle = OmnifilterEngine::getInstance()->match(haystack); + if (needle) + { + if (needle->mChatReplace.empty()) + { + return; + } + + message = needle->mChatReplace; + } + // + std::string notice_name; LLSD notice_args; if (metadata.has("notice")) diff --git a/indra/newview/llscriptfloater.cpp b/indra/newview/llscriptfloater.cpp index 1234964792..679c068575 100644 --- a/indra/newview/llscriptfloater.cpp +++ b/indra/newview/llscriptfloater.cpp @@ -50,6 +50,7 @@ #include "dialogstack.h" #include "llbutton.h" #include "llnavigationbar.h" +#include "omnifilterengine.h" // Omnifilter support // ////////////////////////////////////////////////////////////////////////// @@ -473,6 +474,74 @@ void LLScriptFloaterManager::onAddNotification(const LLUUID& notification_id) // get scripted Object's ID LLUUID object_id = notification_id_to_object_id(notification_id); + // Omnifilter support + LLNotificationPtr notification = LLNotifications::instance().find(notification_id); + + OmnifilterEngine::Haystack haystack; + haystack.mContent = notification->getMessage(); + + if(notification->getName() == "ScriptDialog") // ScriptDialogGroup seems not to be in use anymore? + { + haystack.mType = OmnifilterEngine::eType::ScriptDialog; + haystack.mSenderName = notification->getPayload()["object_name"].asString(); + haystack.mOwnerID = notification->getPayload()["owner_id"]; + } + else if(notification->getName() == "ObjectGiveItem") // what about OwnObjectGiveItem? + { + // "description":"\'Object Name\' ( http://slurl.com/secondlife/Region%20Name/x/y/z )" + std::vector params; + std::string description = notification->asLLSD()["responder_sd"]["description"].asString(); + if (!description.empty()) + { + LLStringUtil::getTokens(description, params, "/"); + haystack.mRegionName = LLURI::unescape(params.at(params.size() - 4)); + } + + haystack.mType = OmnifilterEngine::eType::URLRequest; + haystack.mSenderName = notification->asLLSD()["responder_sd"]["from_name"].asString(); + haystack.mOwnerID = notification->getPayload()["from_id"]; + } + else if(notification->getName() == "LoadWebPage") + { + haystack.mType = OmnifilterEngine::eType::ScriptDialog; + haystack.mSenderName = notification->getPayload()["object_name"].asString(); + haystack.mOwnerID = notification->getPayload()["owner_id"]; + } + else + { + LL_WARNS("Omnifilter") << "unknown notification name: " << notification->getName() << LL_ENDL; + } + + const OmnifilterEngine::Needle* needle = OmnifilterEngine::getInstance()->match(haystack); + if(needle) + { + LLSD response = notification->getResponseTemplate(); + + if(response.has(TEXTBOX_MAGIC_TOKEN)) + { + response[TEXTBOX_MAGIC_TOKEN] = needle->mTextBoxReply; + if (response[TEXTBOX_MAGIC_TOKEN].asString().empty()) + { + // so we can distinguish between a successfully + // submitted blank textbox, and an ignored toast + response[TEXTBOX_MAGIC_TOKEN] = true; + } + } + else + { + if (!needle->mButtonReply.empty()) + { + response[needle->mButtonReply] = true; + } + } + + // this will result in DialogStack complaining that there is no matching dialog to remove + // but that should not break anything + notification->respond(response); + return; + } + // + // Need to indicate of "new message" for object chiclets according to requirements // specified in the Message Bar design specification. See EXT-3142. bool set_new_message = false; diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index be5d645850..7cf5089c16 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -254,6 +254,7 @@ #include "llprogressview.h" #include "lltoolbarview.h" #include "NACLantispam.h" +#include "omnifilterengine.h" // Omnifilter support #include "streamtitledisplay.h" #include "tea.h" @@ -1281,6 +1282,8 @@ bool idle_startup() } show_release_notes_if_required(); + OmnifilterEngine::getInstance()->init(); // Omnifilter support + if (show_connect_box) { LL_DEBUGS("AppInit") << "show_connect_box on" << LL_ENDL; diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp index c07019f4af..06992a98eb 100644 --- a/indra/newview/llviewerfloaterreg.cpp +++ b/indra/newview/llviewerfloaterreg.cpp @@ -240,7 +240,7 @@ #include "quickprefs.h" #include "vjfloaterlocalmesh.h" // local mesh #include "fsfloaterwhitelisthelper.h" // fs whitelist helper - +#include "omnifilter.h" // Omnifilter support // handle secondlife:///app/openfloater/{NAME} URLs const std::string FLOATER_PROFILE("profile"); @@ -667,6 +667,7 @@ void LLViewerFloaterReg::registerFloaters() LLFloaterReg::add("lgg_beamshape", "floater_beamshape.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("media_lists", "floater_media_lists.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("money_tracker", "floater_fs_money_tracker.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); + LLFloaterReg::add("omnifilter", "floater_omnifilter.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); // Omnifilter support LLFloaterReg::add("particle_editor","floater_particle_editor.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("performance", "floater_fs_performance.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); // [PhotoTools] Corrected typo in Phototools floater registration - using string literal instead of PHOTOTOOLS_FLOATER constant (likely intended). diff --git a/indra/newview/llviewermenu.cpp b/indra/newview/llviewermenu.cpp index 3c4c560702..99b06047ea 100644 --- a/indra/newview/llviewermenu.cpp +++ b/indra/newview/llviewermenu.cpp @@ -8559,6 +8559,13 @@ void handle_edit_shape() LLFloaterSidePanelContainer::showPanel("appearance", LLSD().with("type", "edit_shape")); } +// Omnifilter support +void handle_omnifilter() +{ + LLFloaterReg::showInstance("omnifilter"); +} +// + void handle_hover_height() { LLFloaterReg::showInstance("edit_hover_height"); @@ -12638,6 +12645,7 @@ void initialize_menus() commit.add("NowWearing", boost::bind(&handle_now_wearing)); commit.add("EditOutfit", boost::bind(&handle_edit_outfit)); commit.add("EditShape", boost::bind(&handle_edit_shape)); + commit.add("Omnifilter", boost::bind(&handle_omnifilter)); // Omnifilter support commit.add("HoverHeight", boost::bind(&handle_hover_height)); commit.add("EditPhysics", boost::bind(&handle_edit_physics)); // Client LSL Bridge diff --git a/indra/newview/llviewermessage.cpp b/indra/newview/llviewermessage.cpp index 4f32383bd4..fef4b2582c 100644 --- a/indra/newview/llviewermessage.cpp +++ b/indra/newview/llviewermessage.cpp @@ -152,6 +152,7 @@ #include "llfloaterbump.h" #include "llfloaterreg.h" #include "llfriendcard.h" +#include "omnifilterengine.h" // Omnifilter support #include "permissionstracker.h" // Permissions Tracker #include "tea.h" // #include "NACLantispam.h" @@ -3251,6 +3252,69 @@ void process_chat_from_simulator(LLMessageSystem *msg, void **user_data) chat.mText += mesg; } + // Omnifilter support + OmnifilterEngine::Haystack haystack; + haystack.mContent = chat.mText; + haystack.mSenderName = from_name; // we don't use chat.mFromName here because that will include display names etc. + haystack.mOwnerID = chat.mFromID; + + switch (chat.mChatType) + { + case CHAT_TYPE_WHISPER: + case CHAT_TYPE_NORMAL: + case CHAT_TYPE_SHOUT: + case CHAT_TYPE_DEBUG_MSG: + case CHAT_TYPE_REGION: + case CHAT_TYPE_OWNER: + case CHAT_TYPE_DIRECT: + { + switch (chat.mSourceType) + { + case CHAT_SOURCE_AGENT: // this is the type for regular chat + { + haystack.mType = OmnifilterEngine::eType::NearbyChat; + break; + } + case CHAT_SOURCE_OBJECT: // this is the type for object chat + { + haystack.mType = OmnifilterEngine::eType::ObjectChat; + break; + } + default: + { + LL_DEBUGS("Omnifilter") << "unhandled source type " << (U32)chat.mSourceType << LL_ENDL; + break; + } + } + const OmnifilterEngine::Needle* needle = OmnifilterEngine::getInstance()->match(haystack); + + if (needle) + { + // we need to make sure to put the typing stopped flag on the chatting avatar (if it is an avatar) + if (chatter && chatter->isAvatar()) + { + LLLocalSpeakerMgr::getInstance()->setSpeakerTyping(from_id, false); + ((LLVOAvatar*)chatter)->stopTyping(); + } + + if (needle->mChatReplace.empty()) + { + return; + } + + chat.mText = needle->mChatReplace; + } + + break; + } + default: + { + LL_DEBUGS("Omnifilter") << "unhandled chat type " << (U32)chat.mChatType << LL_ENDL; + break; + } + } + // + // We have a real utterance now, so can stop showing "..." and proceed. if (chatter && chatter->isAvatar()) { @@ -8284,6 +8348,7 @@ void process_script_dialog(LLMessageSystem* msg, void**) payload["object_id"] = object_id; payload["chat_channel"] = chat_channel; payload["object_name"] = object_name; + payload["owner_id"] = owner_id; // Omnifilter support // FIRE-17158: Remove "block" button for script dialog of own objects bool own_object = false; diff --git a/indra/newview/omnifilter.cpp b/indra/newview/omnifilter.cpp new file mode 100644 index 0000000000..c5a32fd3d5 --- /dev/null +++ b/indra/newview/omnifilter.cpp @@ -0,0 +1,395 @@ +/** + * @file omnifilter.cpp + * @brief The Omnifilter editor + * + * $LicenseInfo:firstyear=2001&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2025, Zi Ree @ Second Life + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "omnifilter.h" + +#include "llbutton.h" +#include "llcheckboxctrl.h" +#include "llcombobox.h" +#include "lllineeditor.h" +#include "lltexteditor.h" +#include "lltrans.h" + +#include "fsscrolllistctrl.h" + +static constexpr S32 NEEDLE_CHECK_COLUMN = 0; +static constexpr S32 NEEDLE_NAME_COLUMN = 1; + +static constexpr S32 LOG_DATE_COLUMN = 0; +static constexpr S32 LOG_CONTENT_COLUMN = 1; + +Omnifilter::Omnifilter(const LLSD& key) : + LLFloater(key) +{ +} + +Omnifilter::~Omnifilter() +{ +} + +LLScrollListItem* Omnifilter::addNeedle(const std::string& needle_name, const OmnifilterEngine::Needle& needle) +{ + LLSD row; + row["columns"][NEEDLE_CHECK_COLUMN]["column"] = "enabled"; + row["columns"][NEEDLE_CHECK_COLUMN]["type"] = "checkbox"; + row["columns"][NEEDLE_CHECK_COLUMN]["value"] = false; + + row["columns"][NEEDLE_NAME_COLUMN]["column"] = "needle_name"; + row["columns"][NEEDLE_NAME_COLUMN]["type"] = "text"; + row["columns"][NEEDLE_NAME_COLUMN]["value"] = needle_name; + + LLScrollListItem* item = mNeedleListCtrl->addElement(row); + item->getColumn(NEEDLE_CHECK_COLUMN)->setValue(needle.mEnabled); + + LLScrollListCheck* scroll_list_check = dynamic_cast(item->getColumn(NEEDLE_CHECK_COLUMN)); + if (scroll_list_check) + { + LLCheckBoxCtrl* check_box = scroll_list_check->getCheckBox(); + check_box->setCommitCallback(boost::bind(&Omnifilter::onNeedleCheckboxChanged, this, _1)); + } + + mNeedleListCtrl->refreshLineHeight(); + + return item; +} + +OmnifilterEngine::Needle* Omnifilter::getSelectedNeedle() +{ + const LLScrollListItem* needle_item = mNeedleListCtrl->getFirstSelected(); + if (needle_item) + { + const LLScrollListCell* needle_name_cell = needle_item->getColumn(NEEDLE_NAME_COLUMN); + if (needle_name_cell) + { + const std::string& needle_name = needle_name_cell->getValue().asString(); + if (!needle_name.empty()) + { + return &OmnifilterEngine::getInstance()->getNeedleList().at(needle_name); + } + } + } + + mPanelDetails->setVisible(false); + mRemoveNeedleBtn->setEnabled(false); + return nullptr; +} + +void Omnifilter::onSelectNeedle() +{ + const OmnifilterEngine::Needle* needle = getSelectedNeedle(); + if (!needle) + { + mPanelDetails->setVisible(false); + mRemoveNeedleBtn->setEnabled(false); + return; + } + + if (!mNeedleListCtrl->getItemCount()) + { + mPanelDetails->setVisible(false); + mRemoveNeedleBtn->setEnabled(false); + return; + } + + mPanelDetails->setVisible(true); + mRemoveNeedleBtn->setEnabled(true); + + mNeedleNameCtrl->setText(mNeedleListCtrl->getSelectedItemLabel(NEEDLE_NAME_COLUMN)); + mSenderNameCtrl->setText(needle->mSenderName); + mSenderCaseSensitiveCheck->setValue(!needle->mSenderNameCaseInsensitive); + mSenderMatchTypeCombo->selectByValue(needle->mSenderNameMatchType); + mContentCtrl->setText(needle->mContent); + mContentCaseSensitiveCheck->setValue(!needle->mContentCaseInsensitive); + mContentMatchTypeCombo->selectByValue(needle->mContentMatchType); + mRegionNameCtrl->setText(needle->mRegionName); + mOwnerCtrl->clear(); + if (needle->mOwnerID.notNull()) + { + mOwnerCtrl->setText(needle->mOwnerID.asString()); + } + + mTypeNearbyBtn->setToggleState(needle->mTypes.find(OmnifilterEngine::eType::NearbyChat) != needle->mTypes.end()); + mTypeIMBtn->setToggleState(needle->mTypes.find(OmnifilterEngine::eType::InstantMessage) != needle->mTypes.end()); + mTypeGroupIMBtn->setToggleState(needle->mTypes.find(OmnifilterEngine::eType::GroupChat) != needle->mTypes.end()); + mTypeObjectChatBtn->setToggleState(needle->mTypes.find(OmnifilterEngine::eType::ObjectChat) != needle->mTypes.end()); + mTypeObjectIMBtn->setToggleState(needle->mTypes.find(OmnifilterEngine::eType::ObjectInstantMessage) != needle->mTypes.end()); + mTypeScriptErrorBtn->setToggleState(needle->mTypes.find(OmnifilterEngine::eType::ScriptError) != needle->mTypes.end()); + mTypeDialogBtn->setToggleState(needle->mTypes.find(OmnifilterEngine::eType::ScriptDialog) != needle->mTypes.end()); + mTypeOfferBtn->setToggleState(needle->mTypes.find(OmnifilterEngine::eType::InventoryOffer) != needle->mTypes.end()); + mTypeInviteBtn->setToggleState(needle->mTypes.find(OmnifilterEngine::eType::GroupInvite) != needle->mTypes.end()); + mTypeLureBtn->setToggleState(needle->mTypes.find(OmnifilterEngine::eType::Lure) != needle->mTypes.end()); + mTypeLoadURLBtn->setToggleState(needle->mTypes.find(OmnifilterEngine::eType::URLRequest) != needle->mTypes.end()); + mTypeFriendshipOfferBtn->setToggleState(needle->mTypes.find(OmnifilterEngine::eType::FriendshipOffer) != needle->mTypes.end()); + mTypeTeleportRequestBtn->setToggleState(needle->mTypes.find(OmnifilterEngine::eType::TeleportRequest) != needle->mTypes.end()); + mTypeGroupNoticeBtn->setToggleState(needle->mTypes.find(OmnifilterEngine::eType::GroupNotice) != needle->mTypes.end()); + + mChatReplaceCtrl->setText(needle->mChatReplace); + mButtonReplyCtrl->setText(needle->mButtonReply); + mTextBoxReplyCtrl->setText(needle->mTextBoxReply); + + mContentCtrl->setFocus(true); +} + +void Omnifilter::onNeedleChanged() +{ + OmnifilterEngine::Needle* needle = getSelectedNeedle(); + if (!needle) + { + return; + } + + needle->mSenderName = mSenderNameCtrl->getValue().asString(); + needle->mSenderNameCaseInsensitive = !mSenderCaseSensitiveCheck->getValue(); + needle->mSenderNameMatchType = static_cast(mSenderMatchTypeCombo->getValue().asInteger()); + needle->mContent = mContentCtrl->getValue().asString(); + needle->mContentCaseInsensitive = !mContentCaseSensitiveCheck->getValue(); + needle->mContentMatchType = static_cast(mContentMatchTypeCombo->getValue().asInteger()); + needle->mRegionName = mRegionNameCtrl->getValue().asString(); + needle->mOwnerID.set(mOwnerCtrl->getValue().asString()); + + needle->mTypes.clear(); + + if (mTypeNearbyBtn->getValue()) needle->mTypes.insert(OmnifilterEngine::eType::NearbyChat); + if (mTypeIMBtn->getValue()) needle->mTypes.insert(OmnifilterEngine::eType::InstantMessage); + if (mTypeGroupIMBtn->getValue()) needle->mTypes.insert(OmnifilterEngine::eType::GroupChat); + if (mTypeObjectChatBtn->getValue()) needle->mTypes.insert(OmnifilterEngine::eType::ObjectChat); + if (mTypeObjectIMBtn->getValue()) needle->mTypes.insert(OmnifilterEngine::eType::ObjectInstantMessage); + if (mTypeScriptErrorBtn->getValue()) needle->mTypes.insert(OmnifilterEngine::eType::ScriptError); + if (mTypeDialogBtn->getValue()) needle->mTypes.insert(OmnifilterEngine::eType::ScriptDialog); + if (mTypeOfferBtn->getValue()) needle->mTypes.insert(OmnifilterEngine::eType::InventoryOffer); + if (mTypeInviteBtn->getValue()) needle->mTypes.insert(OmnifilterEngine::eType::GroupInvite); + if (mTypeLureBtn->getToggleState()) needle->mTypes.insert(OmnifilterEngine::eType::Lure); + if (mTypeLoadURLBtn->getValue()) needle->mTypes.insert(OmnifilterEngine::eType::URLRequest); + if (mTypeFriendshipOfferBtn->getValue()) needle->mTypes.insert(OmnifilterEngine::eType::FriendshipOffer); + if (mTypeTeleportRequestBtn->getValue()) needle->mTypes.insert(OmnifilterEngine::eType::TeleportRequest); + if (mTypeGroupNoticeBtn->getValue()) needle->mTypes.insert(OmnifilterEngine::eType::GroupNotice); + + needle->mChatReplace = mChatReplaceCtrl->getValue().asString(); + needle->mButtonReply = mButtonReplyCtrl->getValue().asString(); + needle->mTextBoxReply = mTextBoxReplyCtrl->getValue().asString(); + + OmnifilterEngine::getInstance()->setDirty(true); +} + +void Omnifilter::onAddNeedleClicked() +{ + std::string new_needle_string = LLTrans::getString("OmnifilterNewNeedle"); + if (!mNeedleListCtrl->selectItemByLabel(new_needle_string, true, NEEDLE_NAME_COLUMN)) + { + OmnifilterEngine::Needle& new_needle = OmnifilterEngine::getInstance()->newNeedle(new_needle_string); + addNeedle(new_needle_string, new_needle)->setSelected(true); + } + + onSelectNeedle(); + mNeedleNameCtrl->setFocus(true); +} + +void Omnifilter::onRemoveNeedleClicked() +{ + S32 index = mNeedleListCtrl->getItemIndex(mNeedleListCtrl->getFirstSelected()); + OmnifilterEngine::getInstance()->deleteNeedle(mNeedleListCtrl->getSelectedItemLabel(NEEDLE_NAME_COLUMN)); + + mNeedleListCtrl->selectPrevItem(); + mNeedleListCtrl->deleteSingleItem(index); + + if (!mNeedleListCtrl->getNumSelected()) + { + mNeedleListCtrl->selectFirstItem(); + } + + onSelectNeedle(); +} + +void Omnifilter::onNeedleNameChanged() +{ + const std::string& old_name = mNeedleListCtrl->getSelectedItemLabel(NEEDLE_NAME_COLUMN); + const std::string& new_name = mNeedleNameCtrl->getValue().asString(); + + if (old_name == new_name) + { + return; + } + + const LLScrollListItem* needle_item = mNeedleListCtrl->getFirstSelected(); + + const LLScrollListItem* name_check_item = mNeedleListCtrl->getItemByLabel(new_name, true, NEEDLE_NAME_COLUMN); + if (name_check_item && name_check_item != needle_item) + { + mNeedleNameCtrl->setValue(old_name); + return; + } + + OmnifilterEngine::getInstance()->renameNeedle(old_name, new_name); + + LLScrollListCell* needle_name_column = needle_item->getColumn(NEEDLE_NAME_COLUMN); + needle_name_column->setValue(new_name); + + onSelectNeedle(); +} + +void Omnifilter::onNeedleCheckboxChanged(LLUICtrl* ctrl) +{ + const LLCheckBoxCtrl* check = static_cast(ctrl); + OmnifilterEngine::Needle* needle = getSelectedNeedle(); + if (!needle) + { + return; + } + + needle->mEnabled = check->getValue(); + OmnifilterEngine::getInstance()->setDirty(true); +} + +void Omnifilter::onLogLine(time_t time, const std::string& log_line) +{ + LLDate date(time); + + LLSD substitution; + substitution["datetime"] = date; + + std::string time_str = "[hour,datetime,slt]:[min,datetime,slt]:[second,datetime,slt]"; + LLStringUtil::format(time_str, substitution); + + LLSD row; + row["columns"][LOG_DATE_COLUMN]["column"] = "timestamp"; + row["columns"][LOG_DATE_COLUMN]["type"] = "text"; + row["columns"][LOG_DATE_COLUMN]["value"] = time_str; + + row["columns"][LOG_CONTENT_COLUMN]["column"] = "log_entry"; + row["columns"][LOG_CONTENT_COLUMN]["type"] = "text"; + row["columns"][LOG_CONTENT_COLUMN]["value"] = log_line; + + bool scroll_to_end; + scroll_to_end = mFilterLogCtrl->getScrollbar()->isAtEnd(); + + LLScrollListItem* item = mFilterLogCtrl->addElement(row); + if (scroll_to_end) + { + mFilterLogCtrl->setScrollPos(INT32_MAX); + } + + time_str = "[year, datetime, slt]-[mthnum, datetime, slt]-[day, datetime, slt] [hour,datetime,slt]:[min,datetime,slt]:[second,datetime,slt] SLT"; + LLStringUtil::format(time_str, substitution); + item->getColumn(LOG_DATE_COLUMN)->setToolTip(time_str); +} + +bool Omnifilter::postBuild() +{ + mNeedleListCtrl = getChild("needle_list"); + mAddNeedleBtn = getChild("add_needle"); + mRemoveNeedleBtn = getChild("remove_needle"); + mFilterLogCtrl = getChild("filter_log"); + mPanelDetails = getChild("panel_details"); + mNeedleNameCtrl = getChild("needle_name"); + mSenderNameCtrl = getChild("sender_name"); + mSenderCaseSensitiveCheck = getChild("sender_case"); + mSenderMatchTypeCombo = getChild("sender_match_type"); + mContentCtrl = getChild("content"); + mContentCaseSensitiveCheck = getChild("content_case"); + mContentMatchTypeCombo = getChild("content_match_type"); + mRegionNameCtrl = getChild("region_name"); + mOwnerCtrl = getChild("owner_uuid"); + + mTypeNearbyBtn = getChild("type_nearby"); + mTypeIMBtn = getChild("type_im"); + mTypeGroupIMBtn = getChild("type_group_im"); + mTypeObjectChatBtn = getChild("type_object_chat"); + mTypeObjectIMBtn = getChild("type_object_im"); + mTypeScriptErrorBtn = getChild("type_script_error"); + mTypeDialogBtn = getChild("type_dialog"); + mTypeOfferBtn = getChild("type_inventory_offer"); + mTypeInviteBtn = getChild("type_invite"); + mTypeLureBtn = getChild("type_lure"); + mTypeLoadURLBtn = getChild("type_load_url"); + mTypeFriendshipOfferBtn = getChild("type_friendship"); + mTypeTeleportRequestBtn = getChild("type_tp_request"); + mTypeGroupNoticeBtn = getChild("type_group_notice"); + + mChatReplaceCtrl = getChild("chat_replace"); + mButtonReplyCtrl = getChild("button_reply"); + mTextBoxReplyCtrl = getChild("text_box_reply"); + + mNeedleListCtrl->setSearchColumn(NEEDLE_NAME_COLUMN); + mNeedleListCtrl->deleteAllItems(); + mNeedleListCtrl->setCommitOnSelectionChange(true); + + mFilterLogCtrl->deleteAllItems(); + + for (auto const& needle_entry : OmnifilterEngine::getInstance()->getNeedleList()) + { + const std::string& needle_name = needle_entry.first; + const OmnifilterEngine::Needle& needle = needle_entry.second; + addNeedle(needle_name, needle); + } + + for (auto logLine : OmnifilterEngine::getInstance()->mLog) + { + onLogLine(logLine.first, logLine.second); + } + + OmnifilterEngine::getInstance()->mLogSignal.connect(boost::bind(&Omnifilter::onLogLine, this, _1, _2)); + + if (mNeedleListCtrl->getItemCount()) + { + mNeedleListCtrl->selectFirstItem(); + } + + mContentCtrl->setCommitOnFocusLost(true); + mTextBoxReplyCtrl->setCommitOnFocusLost(true); + + mNeedleListCtrl->setCommitCallback(boost::bind(&Omnifilter::onSelectNeedle, this)); + mAddNeedleBtn->setCommitCallback(boost::bind(&Omnifilter::onAddNeedleClicked, this)); + mRemoveNeedleBtn->setCommitCallback(boost::bind(&Omnifilter::onRemoveNeedleClicked, this)); + mNeedleNameCtrl->setCommitCallback(boost::bind(&Omnifilter::onNeedleNameChanged, this)); + mSenderNameCtrl->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mContentCtrl->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mSenderCaseSensitiveCheck->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mSenderMatchTypeCombo->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mContentCtrl->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mContentCaseSensitiveCheck->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mContentMatchTypeCombo->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mRegionNameCtrl->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mChatReplaceCtrl->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mButtonReplyCtrl->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mTextBoxReplyCtrl->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mOwnerCtrl->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mTypeNearbyBtn->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mTypeIMBtn->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mTypeGroupIMBtn->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mTypeObjectChatBtn->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mTypeObjectIMBtn->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mTypeScriptErrorBtn->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mTypeDialogBtn->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mTypeOfferBtn->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mTypeInviteBtn->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mTypeLureBtn->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mTypeLoadURLBtn->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mTypeFriendshipOfferBtn->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mTypeTeleportRequestBtn->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + mTypeGroupNoticeBtn->setCommitCallback(boost::bind(&Omnifilter::onNeedleChanged, this)); + + onSelectNeedle(); + + return true; +} diff --git a/indra/newview/omnifilter.h b/indra/newview/omnifilter.h new file mode 100644 index 0000000000..a012b92b9d --- /dev/null +++ b/indra/newview/omnifilter.h @@ -0,0 +1,99 @@ +/** + * @file omnifilter.h + * @brief The Omnifilter editor + * + * $LicenseInfo:firstyear=2001&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2025, Zi Ree @ Second Life + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#ifndef OMNIFILTER_H +#define OMNIFILTER_H + +#include "omnifilterengine.h" + +#include "llfloater.h" + +class FSScrollListCtrl; +class LLButton; +class LLCheckBoxCtrl; +class LLComboBox; +class LLLineEditor; +class LLPanel; +class LLTextEditor; + +class Omnifilter +: public LLFloater +{ + friend class LLFloaterReg; + + private: + Omnifilter(const LLSD& key); + ~Omnifilter(); + + public: + bool postBuild() override final; + LLScrollListItem* addNeedle(const std::string& name, const OmnifilterEngine::Needle& needle); + + protected: + OmnifilterEngine::Needle* getSelectedNeedle(); + + void onSelectNeedle(); + void onNeedleChanged(); + void onAddNeedleClicked(); + void onRemoveNeedleClicked(); + void onNeedleNameChanged(); + void onNeedleCheckboxChanged(LLUICtrl* ctrl); + + void onLogLine(time_t time, const std::string& logLine); + + FSScrollListCtrl* mNeedleListCtrl; + LLButton* mAddNeedleBtn; + LLButton* mRemoveNeedleBtn; + FSScrollListCtrl* mFilterLogCtrl; + LLPanel* mPanelDetails; + LLLineEditor* mNeedleNameCtrl; + LLLineEditor* mSenderNameCtrl; + LLCheckBoxCtrl* mSenderCaseSensitiveCheck; + LLComboBox* mSenderMatchTypeCombo; + LLTextEditor* mContentCtrl; + LLCheckBoxCtrl* mContentCaseSensitiveCheck; + LLComboBox* mContentMatchTypeCombo; + LLLineEditor* mRegionNameCtrl; + LLLineEditor* mOwnerCtrl; + + LLButton* mTypeNearbyBtn; + LLButton* mTypeIMBtn; + LLButton* mTypeGroupIMBtn; + LLButton* mTypeObjectChatBtn; + LLButton* mTypeObjectIMBtn; + LLButton* mTypeScriptErrorBtn; + LLButton* mTypeDialogBtn; + LLButton* mTypeOfferBtn; + LLButton* mTypeInviteBtn; + LLButton* mTypeLureBtn; + LLButton* mTypeLoadURLBtn; + LLButton* mTypeFriendshipOfferBtn; + LLButton* mTypeTeleportRequestBtn; + LLButton* mTypeGroupNoticeBtn; + + LLLineEditor* mChatReplaceCtrl; + LLLineEditor* mButtonReplyCtrl; + LLTextEditor* mTextBoxReplyCtrl; +}; +#endif // OMNIFILTER_H diff --git a/indra/newview/omnifilterengine.cpp b/indra/newview/omnifilterengine.cpp new file mode 100644 index 0000000000..c968a68a29 --- /dev/null +++ b/indra/newview/omnifilterengine.cpp @@ -0,0 +1,400 @@ +/** + * @file omnifilterengine.cpp + * @brief The core Omnifilter engine + * + * $LicenseInfo:firstyear=2001&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2025, Zi Ree @ Second Life + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#include "llviewerprecompiledheaders.h" + +#include "omnifilterengine.h" + +#include "llnotificationsutil.h" +#include "llsdserialize.h" + +#include +#include + +OmnifilterEngine::OmnifilterEngine() + : LLSingleton() + , LLEventTimer(5.0f) + , mDirty(false) +{ + mEventTimer.stop(); +} + +OmnifilterEngine::~OmnifilterEngine() +{ + // delete Xxx; +} + +void OmnifilterEngine::init() +{ + mNeedlesXMLPath = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, "omnifilter.xml"); + if (mNeedlesXMLPath.empty()) + { + LL_DEBUGS("Omnifilter") << "Got empty file name for omnifilter XML storage." << LL_ENDL; + } + + loadNeedles(); +} + +const OmnifilterEngine::Needle* OmnifilterEngine::logMatch(const std::string& needle_name, const Needle& needle) +{ + time_t now = LLDate::now().secondsSinceEpoch(); + mLog.push_back(std::make_pair(now, needle_name)); + mLogSignal(now, needle_name); + + return &needle; +} + +bool OmnifilterEngine::matchStrings(const std::string& needle_string, const std::string& haystack_string, eMatchType match_type, bool case_insensitive) +{ + static LLCachedControl use_omnifilter(*LLControlGroup::getInstance("Global"), "OmnifilterEnabled"); + if (!use_omnifilter) + { + return false; + } + + std::string_view needle_content = needle_string; + std::string_view haystack_content = haystack_string; + + std::string needle_lc_content; + std::string haystack_lc_content; + if (case_insensitive) + { + needle_lc_content = needle_string; + haystack_lc_content = haystack_string; + + LLStringUtil::toLower(needle_lc_content); + LLStringUtil::toLower(haystack_lc_content); + needle_content = needle_lc_content; + haystack_content = haystack_lc_content; + } + + switch (match_type) + { + case eMatchType::Regex: + { + boost::regbase::flag_type re_flags = boost::regex::normal; + if (case_insensitive) + { + re_flags |= boost::regex::icase; + } + boost::regex re(needle_string, re_flags); + if (boost::regex_match(haystack_string, re)) + { + return true; + } + break; + } + case eMatchType::Substring: + { + if (haystack_content.find(needle_content) != haystack_content.npos) + { + return true; + } + break; + } + case eMatchType::Exact: + { + if (haystack_content == needle_content) + { + return true; + } + break; + } + default: + { + LL_DEBUGS("Omnifilter") << "match type " << match_type << " unknown!" << LL_ENDL; + break; + } + } + return false; +} + +const OmnifilterEngine::Needle* OmnifilterEngine::match(const Haystack& haystack) +{ + for (auto const& needle_entry : mNeedles) + { + const Needle& needle = needle_entry.second; + const std::string& needle_name = needle_entry.first; + + if (!needle.mEnabled) + { + continue; + } + + if (!needle.mSenderName.empty()) + { + if (!matchStrings(needle.mSenderName, haystack.mSenderName, needle.mSenderNameMatchType, needle.mSenderNameCaseInsensitive)) + { + continue; + } + } + + if (needle.mOwnerID.notNull()) + { + if (needle.mOwnerID != haystack.mOwnerID) + { + continue; + } + } + + if (!needle.mRegionName.empty()) + { + if (needle.mRegionName != haystack.mRegionName) + { + continue; + } + } + + if (!needle.mTypes.empty() && needle.mTypes.find(haystack.mType) == needle.mTypes.end()) + { + continue; + } + + if (matchStrings(needle.mContent, haystack.mContent, needle.mContentMatchType, needle.mContentCaseInsensitive)) + { + return logMatch(needle_name, needle); + } + } + + return nullptr; +} + +OmnifilterEngine::Needle& OmnifilterEngine::newNeedle(const std::string& needle_name) +{ + if (mNeedles.find(needle_name) != mNeedles.end()) + { + Needle new_needle; + new_needle.mEnabled = false; + mNeedles[needle_name] = new_needle; + } + setDirty(true); + return mNeedles[needle_name]; +} + +void OmnifilterEngine::renameNeedle(const std::string& old_name, const std::string& new_name) +{ + // https://stackoverflow.com/a/44883472 + auto node_handler = mNeedles.extract(old_name); + node_handler.key() = new_name; + mNeedles.insert(std::move(node_handler)); + + setDirty(true); +} + +void OmnifilterEngine::deleteNeedle(const std::string& needle_name) +{ + mNeedles.erase(needle_name); + setDirty(true); +} + +OmnifilterEngine::needle_list_t& OmnifilterEngine::getNeedleList() +{ + return mNeedles; +} + +void OmnifilterEngine::setDirty(bool dirty) +{ + mDirty = dirty; + + if (mDirty) + { + mEventTimer.start(); + } + else + { + mEventTimer.stop(); + } +} + +void OmnifilterEngine::loadNeedles() +{ + if (mNeedlesXMLPath.empty()) + { + return; + } + + if (!std::filesystem::exists(mNeedlesXMLPath)) + { + // file does not exist (yet), just return empty + return; + } + + if (!std::filesystem::is_regular_file(mNeedlesXMLPath)) + { + LL_DEBUGS("Omnifilter") << "Omnifilter storage at '" << mNeedlesXMLPath << "' is not a regular file." << LL_ENDL; + LLSD args; + args["FILE_NAME"] = mNeedlesXMLPath; + LLNotificationsUtil::add("NotRegularFileError", args, LLSD()); + return; + } + + if (std::filesystem::file_size(mNeedlesXMLPath) == 0) + { + // file exists but is empty, this should not happen, so alert the user, they might have lost their needles + LL_DEBUGS("Omnifilter") << "Omnifilter storage file is empty." << mNeedlesXMLPath << LL_ENDL; + LLSD args; + args["ERROR_CODE"] = errno; + args["ERROR_MESSAGE"] = strerror(errno); + args["FILE_NAME"] = mNeedlesXMLPath; + LLNotificationsUtil::add("GenericFileEmptyError", args, LLSD()); + return; + } + + LL_DEBUGS("Omnifilter") << "Loading needles" << mNeedlesXMLPath << LL_ENDL; + + std::ifstream file; + file.open(mNeedlesXMLPath.c_str()); + if (file.fail()) + { + LL_DEBUGS("Omnifilter") << "Unable to open Omnifilter storage at '" << mNeedlesXMLPath << "' for reading." << LL_ENDL; + LLSD args; + args["ERROR_CODE"] = errno; + args["ERROR_MESSAGE"] = strerror(errno); + args["FILE_NAME"] = mNeedlesXMLPath; + LLNotificationsUtil::add("GenericFileOpenReadError", args, LLSD()); + return; + } + + LLSD needles_llsd; + if (file.is_open()) + { + LLSDSerialize::fromXML(needles_llsd, file); + file.close(); + } + + if (file.fail()) + { + LL_DEBUGS("Omnifilter") << "Unable to read Omnifilter needles from '" << mNeedlesXMLPath << "'." << LL_ENDL; + LLSD args; + args["ERROR_CODE"] = errno; + args["ERROR_MESSAGE"] = strerror(errno); + args["FILE_NAME"] = mNeedlesXMLPath; + LLNotificationsUtil::add("GenericFileReadError", args, LLSD()); + return; + } + + for (LLSD::map_iterator iter = needles_llsd.beginMap(); iter != needles_llsd.endMap(); ++iter) + { + const std::string& new_needle_name = (*iter).first; + LLSD needle_data = (*iter).second; + + Needle new_needle; + new_needle.mSenderName = needle_data["sender_name"].asString(); + new_needle.mContent = needle_data["content"].asString(); + new_needle.mRegionName = needle_data["region_name"].asString(); + new_needle.mChatReplace = needle_data["chat_replace"].asString(); + new_needle.mButtonReply = needle_data["button_reply"].asString(); + new_needle.mTextBoxReply = needle_data["textbox_reply"].asString(); + new_needle.mSenderNameMatchType = static_cast(needle_data["sender_name_match_type"].asInteger()); + new_needle.mContentMatchType = static_cast(needle_data["content_match_type"].asInteger()); + + LLSD types_llsd = needle_data["types"]; + + for (LLSD::array_iterator aiter = types_llsd.beginArray(); aiter != types_llsd.endArray(); ++aiter) + { + new_needle.mTypes.insert(static_cast((*aiter).asInteger())); + } + + new_needle.mEnabled = needle_data["enabled"].asBoolean(); + new_needle.mSenderNameCaseInsensitive = needle_data["sender_name_case_insensitive"].asBoolean(); + new_needle.mContentCaseInsensitive = needle_data["content_case_insensitive"].asBoolean(); + + mNeedles[new_needle_name] = new_needle; + } +} + +void OmnifilterEngine::saveNeedles() +{ + if (mNeedlesXMLPath.empty()) + { + return; + } + + LL_DEBUGS("Omnifilter") << "Saving needles" << mNeedlesXMLPath << LL_ENDL; + + std::ofstream file; + + file.open(mNeedlesXMLPath.c_str()); + if (file.fail()) + { + LL_DEBUGS("Omnifilter") << "Unable to open Omnifilter storage at '" << mNeedlesXMLPath << "' for writing." << LL_ENDL; + LLSD args; + args["ERROR_CODE"] = errno; + args["ERROR_MESSAGE"] = strerror(errno); + args["FILE_NAME"] = mNeedlesXMLPath; + LLNotificationsUtil::add("GenericFileOpenWriteError", args, LLSD()); + return; + } + + LLSD needles_llsd; + + for (auto const& needle_entry : mNeedles) + { + const std::string& needle_name = needle_entry.first; + const Needle& needle = needle_entry.second; + + needles_llsd[needle_name]["sender_name"] = needle.mSenderName; + needles_llsd[needle_name]["content"] = needle.mContent; + needles_llsd[needle_name]["region_name"] = needle.mRegionName; + needles_llsd[needle_name]["chat_replace"] = needle.mChatReplace; + needles_llsd[needle_name]["button_reply"] = needle.mButtonReply; + needles_llsd[needle_name]["textbox_reply"] = needle.mTextBoxReply; + needles_llsd[needle_name]["sender_name_match_type"] = needle.mSenderNameMatchType; + needles_llsd[needle_name]["content_match_type"] = needle.mContentMatchType; + needles_llsd[needle_name]["owner_id"] = needle.mOwnerID; + + LLSD types_llsd; + for (auto type : needle.mTypes) + { + types_llsd.append(static_cast(type)); + } + + needles_llsd[needle_name]["types"] = types_llsd; + needles_llsd[needle_name]["enabled"] = needle.mEnabled; + needles_llsd[needle_name]["sender_name_case_insensitive"] = needle.mSenderNameCaseInsensitive; + needles_llsd[needle_name]["content_case_insensitive"] = needle.mContentCaseInsensitive; + } + + file.close(); + + if (file.fail()) + { + LL_DEBUGS("Omnifilter") << "Unable to save Omnifilter needles at '" << mNeedlesXMLPath << "'." << LL_ENDL; + LLSD args; + args["ERROR_CODE"] = errno; + args["ERROR_MESSAGE"] = strerror(errno); + args["FILE_NAME"] = mNeedlesXMLPath; + LLNotificationsUtil::add("GenericFileWriteError", args, LLSD()); + } + + setDirty(false); +} + +// virtual +bool OmnifilterEngine::tick() +{ + saveNeedles(); + setDirty(false); + return false; +} diff --git a/indra/newview/omnifilterengine.h b/indra/newview/omnifilterengine.h new file mode 100644 index 0000000000..92cdb589ad --- /dev/null +++ b/indra/newview/omnifilterengine.h @@ -0,0 +1,138 @@ +/** + * @file omnifilterengine.h + * @brief The core Omnifilter engine + * + * $LicenseInfo:firstyear=2001&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2025, Zi Ree @ Second Life + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * $/LicenseInfo$ + */ + +#ifndef OMNIFILTERENGINE_H +#define OMNIFILTERENGINE_H + +#include "lleventtimer.h" +#include "llsingleton.h" + +#include +#include + +class OmnifilterEngine +: public LLSingleton +, public LLEventTimer +{ + LLSINGLETON(OmnifilterEngine); + ~OmnifilterEngine(); + + public: + enum eType + { + NearbyChat, + GroupChat, + GroupNotice, + GroupInvite, + InstantMessage, + ObjectChat, + ObjectInstantMessage, + ScriptError, + ScriptDialog, + FriendshipOffer, + InventoryOffer, + Lure, + TeleportRequest, + URLRequest, + TYPES_MAX + }; + + enum eMatchType + { + Exact, + Substring, + Regex, + MATCH_TYPES_MAX + }; + + class Haystack + { + public: + std::string mSenderName; + std::string mContent; + std::string mRegionName; + + LLUUID mOwnerID; + + eType mType; + }; + + class Needle + { + public: + std::string mSenderName; + std::string mContent; + std::string mRegionName; + + std::string mChatReplace; + std::string mButtonReply; + std::string mTextBoxReply; + + eMatchType mSenderNameMatchType; + eMatchType mContentMatchType; + + LLUUID mOwnerID; + + std::set mTypes; + + bool mEnabled; + bool mSenderNameCaseInsensitive = false; + bool mContentCaseInsensitive = false; + }; + + typedef std::map needle_list_t; + needle_list_t& getNeedleList(); + + Needle& newNeedle(const std::string& needle_name); + void renameNeedle(const std::string& old_name, const std::string& new_name); + void deleteNeedle(const std::string& needle_name); + + const Needle* match(const Haystack& haystack); + void setDirty(bool dirty); + + void init(); + + typedef boost::signals2::signal log_signal_t; + log_signal_t mLogSignal; + + std::list> mLog; + + protected: + const Needle* logMatch(const std::string& needle_name, const Needle& needle); + bool matchStrings(const std::string& needle_string, const std::string& haystack_string, eMatchType match_type, bool case_insensitive); + + void loadNeedles(); + void saveNeedles(); + + virtual bool tick(); + + protected: + needle_list_t mNeedles; + + std::string mNeedlesXMLPath; + + bool mDirty; +}; + +#endif // OMNIFILTERENGINE_H diff --git a/indra/newview/skins/default/textures/textures.xml b/indra/newview/skins/default/textures/textures.xml index f72efb9fb2..3687f72952 100644 --- a/indra/newview/skins/default/textures/textures.xml +++ b/indra/newview/skins/default/textures/textures.xml @@ -165,6 +165,7 @@ with the same filename but different name + @@ -886,6 +887,7 @@ with the same filename but different name + diff --git a/indra/newview/skins/default/textures/toolbar_icons/omnifilter.png b/indra/newview/skins/default/textures/toolbar_icons/omnifilter.png new file mode 100644 index 0000000000..163bbfe98d Binary files /dev/null and b/indra/newview/skins/default/textures/toolbar_icons/omnifilter.png differ diff --git a/indra/newview/skins/default/xui/en/floater_omnifilter.xml b/indra/newview/skins/default/xui/en/floater_omnifilter.xml new file mode 100644 index 0000000000..847676c8b8 --- /dev/null +++ b/indra/newview/skins/default/xui/en/floater_omnifilter.xml @@ -0,0 +1,171 @@ + + + + + + + + + + + + + + + +Example Rule 1 + + + +Example Rule 2 + + + + +