First Omnifilter implementation - Can block chat, dialogs, teleport offers, friendship, etc. based on content rather than just owner or sender.

master
Zi Ree 2025-12-10 18:51:15 +01:00
parent 4b4e3df6de
commit 67813450af
19 changed files with 1492 additions and 1 deletions

View File

@ -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
# <FS:Ansariel> [Legacy Bake]
llagentwearablesfetch.h
omnifilter.h
omnifilterengine.h
vjlocalmesh.h
vjfloaterlocalmesh.h
vjlocalmeshimportdae.h

View File

@ -680,4 +680,15 @@
is_running_parameters="performance"
checkbox_control="AutoTuneFPS"
/>
<command name="omnifilter"
available_in_toybox="true"
icon="Command_Omnifilter_Icon"
label_ref="Command_Omnifilter_Label"
tooltip_ref="Command_Omnifilter_Tooltip"
execute_function="Floater.ToggleOrBringToFront"
execute_parameters="omnifilter"
is_running_function="Floater.IsOpen"
is_running_parameters="omnifilter"
checkbox_control="OmnifilterEnabled"
/>
</commands>

View File

@ -27193,5 +27193,16 @@ Change of this parameter will affect the layout of buttons in notification toast
<key>Value</key>
<integer>0</integer>
</map>
<key>OmnifilterEnabled</key>
<map>
<key>Comment</key>
<string>The operating state of the Omnifilter.</string>
<key>Persist</key>
<integer>1</integer>
<key>Type</key>
<string>Boolean</string>
<key>Value</key>
<integer>1</integer>
</map>
</map>
</llsd>

View File

@ -79,6 +79,7 @@
#include "llgiveinventory.h"
#include "lllandmarkactions.h"
#include "llviewernetwork.h"
#include "omnifilterengine.h" // <FS:Zi> Omnifilter support
#include "sound_ids.h"
#include "NACLantispam.h"
@ -721,6 +722,100 @@ void LLIMProcessing::processNewMessage(LLUUID from_id,
*-----------------------------------------------------
*/
// <FS:Zi> 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;
}
// </FS:Zi>
std::string notice_name;
LLSD notice_args;
if (metadata.has("notice"))

View File

@ -50,6 +50,7 @@
#include "dialogstack.h"
#include "llbutton.h"
#include "llnavigationbar.h"
#include "omnifilterengine.h" // Omnifilter support
// </FS:Zi>
//////////////////////////////////////////////////////////////////////////
@ -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);
// <FS:Zi> 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<std::string> 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;
}
// </FS:Zi>
// 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;

View File

@ -254,6 +254,7 @@
#include "llprogressview.h"
#include "lltoolbarview.h"
#include "NACLantispam.h"
#include "omnifilterengine.h" // <FS:Zi> Omnifilter support
#include "streamtitledisplay.h"
#include "tea.h"
@ -1281,6 +1282,8 @@ bool idle_startup()
}
show_release_notes_if_required();
OmnifilterEngine::getInstance()->init(); // <FS:Zi> Omnifilter support
if (show_connect_box)
{
LL_DEBUGS("AppInit") << "show_connect_box on" << LL_ENDL;

View File

@ -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<lggBeamMapFloater>);
LLFloaterReg::add("media_lists", "floater_media_lists.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<FloaterMediaLists>);
LLFloaterReg::add("money_tracker", "floater_fs_money_tracker.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<FSMoneyTracker>);
LLFloaterReg::add("omnifilter", "floater_omnifilter.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<Omnifilter>); // <FS:Zi> Omnifilter support
LLFloaterReg::add("particle_editor","floater_particle_editor.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<ParticleEditor>);
LLFloaterReg::add("performance", "floater_fs_performance.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<FSFloaterPerformance>);
// <FS:William_W:FixPhototoolsTypo> [PhotoTools] Corrected typo in Phototools floater registration - using string literal instead of PHOTOTOOLS_FLOATER constant (likely intended).

View File

@ -8559,6 +8559,13 @@ void handle_edit_shape()
LLFloaterSidePanelContainer::showPanel("appearance", LLSD().with("type", "edit_shape"));
}
// <FS:Zi> Omnifilter support
void handle_omnifilter()
{
LLFloaterReg::showInstance("omnifilter");
}
// </FS:Zi>
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)); // <FS:Zi> Omnifilter support
commit.add("HoverHeight", boost::bind(&handle_hover_height));
commit.add("EditPhysics", boost::bind(&handle_edit_physics));
// <FS:TT> Client LSL Bridge

View File

@ -152,6 +152,7 @@
#include "llfloaterbump.h"
#include "llfloaterreg.h"
#include "llfriendcard.h"
#include "omnifilterengine.h" // <FS:Zi> Omnifilter support
#include "permissionstracker.h" // <FS:Zi> Permissions Tracker
#include "tea.h" // <FS:AW opensim currency support>
#include "NACLantispam.h"
@ -3251,6 +3252,69 @@ void process_chat_from_simulator(LLMessageSystem *msg, void **user_data)
chat.mText += mesg;
}
// <FS:Zi> 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;
}
}
// </FS:Zi>
// 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; // <FS:Zi> Omnifilter support
// <FS:Ansariel> FIRE-17158: Remove "block" button for script dialog of own objects
bool own_object = false;

View File

@ -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<LLScrollListCheck*>(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<OmnifilterEngine::eMatchType>(mSenderMatchTypeCombo->getValue().asInteger());
needle->mContent = mContentCtrl->getValue().asString();
needle->mContentCaseInsensitive = !mContentCaseSensitiveCheck->getValue();
needle->mContentMatchType = static_cast<OmnifilterEngine::eMatchType>(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<LLCheckBoxCtrl*>(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<FSScrollListCtrl>("needle_list");
mAddNeedleBtn = getChild<LLButton>("add_needle");
mRemoveNeedleBtn = getChild<LLButton>("remove_needle");
mFilterLogCtrl = getChild<FSScrollListCtrl>("filter_log");
mPanelDetails = getChild<LLPanel>("panel_details");
mNeedleNameCtrl = getChild<LLLineEditor>("needle_name");
mSenderNameCtrl = getChild<LLLineEditor>("sender_name");
mSenderCaseSensitiveCheck = getChild<LLCheckBoxCtrl>("sender_case");
mSenderMatchTypeCombo = getChild<LLComboBox>("sender_match_type");
mContentCtrl = getChild<LLTextEditor>("content");
mContentCaseSensitiveCheck = getChild<LLCheckBoxCtrl>("content_case");
mContentMatchTypeCombo = getChild<LLComboBox>("content_match_type");
mRegionNameCtrl = getChild<LLLineEditor>("region_name");
mOwnerCtrl = getChild<LLLineEditor>("owner_uuid");
mTypeNearbyBtn = getChild<LLButton>("type_nearby");
mTypeIMBtn = getChild<LLButton>("type_im");
mTypeGroupIMBtn = getChild<LLButton>("type_group_im");
mTypeObjectChatBtn = getChild<LLButton>("type_object_chat");
mTypeObjectIMBtn = getChild<LLButton>("type_object_im");
mTypeScriptErrorBtn = getChild<LLButton>("type_script_error");
mTypeDialogBtn = getChild<LLButton>("type_dialog");
mTypeOfferBtn = getChild<LLButton>("type_inventory_offer");
mTypeInviteBtn = getChild<LLButton>("type_invite");
mTypeLureBtn = getChild<LLButton>("type_lure");
mTypeLoadURLBtn = getChild<LLButton>("type_load_url");
mTypeFriendshipOfferBtn = getChild<LLButton>("type_friendship");
mTypeTeleportRequestBtn = getChild<LLButton>("type_tp_request");
mTypeGroupNoticeBtn = getChild<LLButton>("type_group_notice");
mChatReplaceCtrl = getChild<LLLineEditor>("chat_replace");
mButtonReplyCtrl = getChild<LLLineEditor>("button_reply");
mTextBoxReplyCtrl = getChild<LLTextEditor>("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;
}

View File

@ -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

View File

@ -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 <fstream>
#include <filesystem>
OmnifilterEngine::OmnifilterEngine()
: LLSingleton<OmnifilterEngine>()
, 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<bool> 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<OmnifilterEngine::eMatchType>(needle_data["sender_name_match_type"].asInteger());
new_needle.mContentMatchType = static_cast<OmnifilterEngine::eMatchType>(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<OmnifilterEngine::eType>((*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<S32>(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;
}

View File

@ -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 <boost/bind.hpp>
#include <boost/signals2.hpp>
class OmnifilterEngine
: public LLSingleton<OmnifilterEngine>
, 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<eType> mTypes;
bool mEnabled;
bool mSenderNameCaseInsensitive = false;
bool mContentCaseInsensitive = false;
};
typedef std::map<std::string, OmnifilterEngine::OmnifilterEngine::Needle> 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<void(time_t, const std::string&)> log_signal_t;
log_signal_t mLogSignal;
std::list<std::pair<time_t, std::string>> 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

View File

@ -165,6 +165,7 @@ with the same filename but different name
<texture name="Command_MiniMap_Icon" file_name="toolbar_icons/mini_map.png" preload="true" />
<texture name="Command_Move_Icon" file_name="toolbar_icons/move.png" preload="true" />
<texture name="Command_Environments_Icon" file_name="toolbar_icons/environments.png" preload="true" />
<texture name="Command_Omnifilter_Icon" file_name="toolbar_icons/omnifilter.png" preload="true" /> <!-- FS:Zi: Omnifilter -->
<texture name="Command_People_Icon" file_name="toolbar_icons/people.png" preload="true" />
<texture name="Command_Performance_Icon" file_name="toolbar_icons/performance.png" preload="true" />
<texture name="Command_Picks_Icon" file_name="toolbar_icons/picks.png" preload="true" />
@ -886,6 +887,7 @@ with the same filename but different name
<texture name="Wearable_Favorites_Icon" file_name="toolbar_icons/wearable_favorites.png" preload="true" />
<texture name="beacons" file_name="toolbar_icons/beacons.png" preload="true" />
<texture name="Stop_Animations_Icon" file_name="toolbar_icons/stop_animations.png" preload="true" />
<texture name="omnifilter" file_name="toolbar_icons/omnifilter.png" preload="true" />
<texture name="skin ansastorm blood" file_name="skinspreview/ansa_blood.jpg" preload="true" />
<texture name="skin ansastorm bright blue" file_name="skinspreview/ansa_blue.jpg" preload="true" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 B

View File

@ -0,0 +1,171 @@
<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<floater
legacy_header_height="18"
can_resize="true"
can_minimize="true"
can_close="true"
height="400"
min_height="348"
min_width="668"
layout="topleft"
name="omnifilter"
help_topic="omnifilter"
save_rect="true"
save_visibility="true"
single_instance="true"
reuse_instance="true"
title="Omnifilter Rules Editor"
width="700">
<layout_stack name="needle_list_stack" layout="topleft" follows="left|top|bottom" height="374" top="20" left="8" width="190" orientation="vertical" show_drag_handle="true">
<layout_panel name="needle_list_layout" layout="topleft" height="150" min_height="94" user_resize="true">
<check_box name="enable_omnifilter" layout="topleft" follows="left|top" height="20" top="0" right="-1" label="Enable Omnifilter" control_name="OmnifilterEnabled" />
<fs_scroll_list name="needle_list" layout="topleft" follows="all" height="100" top_pad="4" draw_heading="true" draw_stripes="true" desired_line_height="18">
<fs_scroll_list.columns name="enabled" label="On" width="24" />
<fs_scroll_list.columns name="needle_name" label="Rule" width="175" />
<!-- example entries for UI editing -->
<row>
<column name="enabled" column="enabled" type="checkbox" value="true" />
<column name="needle_name" column="needle_name">Example Rule 1</column>
</row>
<row>
<column name="enabled" column="enabled" type="checkbox" />
<column name="needle_name" column="needle_name">Example Rule 2</column>
</row>
</fs_scroll_list>
<button name="add_needle" layout="topleft" follows="left|bottom" height="20" top_pad="4" width="96" label="Add" />
<button name="remove_needle" layout="topleft" follows="right|bottom" height="20" left_pad="0" width="96" label="Remove" />
</layout_panel>
<layout_panel name="filter_log_layout" layout="topleft" height="130" min_height="50">
<fs_scroll_list name="filter_log" layout="topleft" follows="all" height="150" top="0" draw_heading="true" draw_stripes="true" can_sort="false">
<fs_scroll_list.columns name="timestamp" label="Time (SLT)" width="64" />
<fs_scroll_list.columns name="log_entry" label="Rule" />
<!-- example entries for UI editing -->
<row>
<column name="timestamp" column="timestamp" tool_tip="2015-11-24 00:01:02">00:01:02</column>
<column name="log_entry" column="log_entry">Example Rule 1</column>
</row>
<row>
<column name="timestamp" column="timestamp" tool_tip="2015-11-24 01:02:03">01:02:03</column>
<column name="log_entry" column="log_entry">Example Rule 2</column>
</row>
</fs_scroll_list>
</layout_panel>
</layout_stack>
<panel name="panel_details" layout="topleft" follows="all" height="374" left_pad="4" right="-4">
<text name="needle_name_label" layout="topleft" follows="left|top" height="20" width="100" left="0" top_pad="0" valign="center" value="Rule Name:" />
<line_editor name="needle_name" layout="topleft" follows="left|right|top" height="20" left_pad="4" right="-4" />
<text name="sender_name_label" layout="topleft" follows="left|top" height="20" width="100" left="0" top_pad="2" valign="center" value="Sender Name:" />
<line_editor name="sender_name" layout="topleft" follows="left|right|top" height="20" left_pad="4" right="-110" />
<check_box name="sender_case" layout="topleft" follows="right|top" height="20" left_pad="4" width="30" label="Aa" />
<combo_box name="sender_match_type" layout="topleft" follows="right|top" height="20" left_pad="8" right="-4" >
<combo_item name="sender_exact" value="0">
<column width="40" name="label" label="|Abc|" />
<column name="longtext" label="Match full text" />
</combo_item>
<combo_item name="sender_substring" value="1">
<column name="label" label="*Abc*" />
<column name="longtext" label="Match substring" />
</combo_item>
<combo_item name="sender_regex" value="2">
<column name="label" label="/ *. /" />
<column name="longtext" label="Match regular expression" width.width="400" width.pixel_width="300" />
</combo_item>
</combo_box>
<text name="content_label" layout="topleft" follows="left|top" height="20" left="0" width="100" top_pad="3" valign="center" value="Content:" />
<text_editor name="content" layout="topleft" follows="all" height="74" left_pad="4" right="-4" top_delta="0" />
<check_box name="content_case" layout="topleft" follows="right|bottom" height="20" width="30" right="-76" top_pad="4" label="Aa" />
<combo_box name="content_match_type" layout="topleft" follows="right|bottom" height="20" left_pad="8" right="-4" >
<combo_item name="content_exact" value="0">
<column width="40" name="label" label="|Abc|" />
<column name="longtext" label="Match full text" />
</combo_item>
<combo_item name="content_substring" value="1">
<column name="label" label="*Abc*" />
<column name="longtext" label="Match substring" />
</combo_item>
<combo_item name="content_regex" value="2">
<column name="label" label="/ *. /" />
<column name="longtext" label="Match regular expression" />
</combo_item>
</combo_box>
<text name="region_name_label" layout="topleft" follows="left|bottom" height="20" width="100" left="0" top_pad="2" valign="center" value="Region Name:" />
<line_editor name="region_name" layout="topleft" follows="left|right|bottom" height="20" left_pad="4" right="-4" />
<text name="owner_label" layout="topleft" follows="left|bottom" height="20" width="100" left="0" top_pad="2" valign="center" value="Owner:" />
<line_editor name="owner_uuid" layout="topleft" follows="left|right|bottom" height="20" left_pad="4" right="-4" />
<text name="match_source_label" layout="topleft" follows="left|bottom" width="100" height="20" top_pad="4" left="0" valign="center" value="Match Source:" />
<layout_stack orientation="horizontal" left="104" top_delta="0" right="-4" height="68" follows="left|right|bottom" layout="topleft">
<layout_panel width="72" layout="topleft" follows="all">
<button name="type_nearby" layout="topleft" follows="left|right|top" width= "72" height="20" is_toggle="true" top_pad="0" label="Nearby Chat" tool_tip="Regular public avatar chat nearby." />
<button name="type_im" layout="topleft" follows="left|right|top" width= "72" height="20" is_toggle="true" top_pad="4" label="IM" tool_tip="Instant messages from users." />
<button name="type_group_im" layout="topleft" follows="left|right|top" width= "72" height="20" is_toggle="true" top_pad="4" label="Group Chat" tool_tip="Group chat from users." />
</layout_panel>
<layout_panel>
<button name="type_group_notice" layout="topleft" follows="left|right|top" width= "72" height="20" is_toggle="true" top="0" label="Notice" tool_tip="Group notices." />
<button name="type_invite" layout="topleft" follows="left|right|top" width= "72" height="20" is_toggle="true" top_pad="4" label="Invite" tool_tip="Group invitations." />
<button name="type_object_chat" layout="topleft" follows="left|right|top" width= "72" height="20" is_toggle="true" top_pad="4" label="Object Chat" tool_tip="Nearby chat from objects." />
</layout_panel>
<layout_panel>
<button name="type_object_im" layout="topleft" follows="left|right|top" width= "72" height="20" is_toggle="true" top="0" label="Object IM" tool_tip="Instant messages from objects." />
<button name="type_script_error" layout="topleft" follows="left|right|top" width= "72" height="20" is_toggle="true" top_pad="4" label="Error" tool_tip="Debug chat coming from scripts." />
<button name="type_dialog" layout="topleft" follows="left|right|top" width= "72" height="20" is_toggle="true" top_pad="4" label="Dialog" tool_tip="Matches fields in script dialogs." />
</layout_panel>
<layout_panel>
<button name="type_inventory_offer" layout="topleft" follows="left|right|top" width= "72" height="20" is_toggle="true" top="0" label="Offer" tool_tip="Matches fields in inventory offers." />
<button name="type_friendship" layout="topleft" follows="left|right|top" width= "72" height="20" is_toggle="true" top_pad="4" label="Friendship" tool_tip="Friend requests from users." />
<button name="type_lure" layout="topleft" follows="left|right|top" width= "72" height="20" is_toggle="true" top_pad="4" label="TP Lure" tool_tip="Teleport offers from users." />
</layout_panel>
<layout_panel>
<button name="type_tp_request" layout="topleft" follows="left|right|top" width= "72" height="20" is_toggle="true" top="0" label="TP Request" tool_tip="Requests from users to send them a teleport offer." />
<button name="type_load_url" layout="topleft" follows="left|right|top" width= "72" height="20" is_toggle="true" top_pad="4" label="Load URL" tool_tip="Scripted requests to visit a website." />
</layout_panel>
</layout_stack>
<view_border layout="topleft" right="-4" follows="left|bottom|right" left="0" height="1" top_pad="4" />
<text name="chat_replace_label" layout="topleft" follows="left|bottom" left="0" height="20" width="100" top_pad="2" value="Chat Replace:" valign="center" />
<line_editor name="chat_replace" layout="topleft" follows="left|right|bottom" left_pad="4" height="20" right="-4" />
<text name="button_reply_label" layout="topleft" follows="left|bottom" left="0" height="20" width="100" top_pad="2" value="Button Reply:" valign="center" />
<line_editor name="button_reply" layout="topleft" follows="left|right|bottom" left_pad="4" height="20" right="-4" />
<text name="text_box_reply_label" layout="topleft" follows="left|bottom" left="0" height="20" width="100" top_pad="3" value="Text Box Reply:" valign="center" />
<text_editor name="text_box_reply" layout="topleft" follows="left|right|bottom" left_pad="4" height="60" right="-4" top_delta="0" />
</panel>
</floater>

View File

@ -105,6 +105,17 @@
parameter="experiences"/>
</menu_item_call>
<menu_item_check
label="Omnifilter"
name="Omnifilter">
<menu_item_check.on_click
function="Floater.Toggle"
parameter="omnifilter" />
<menu_item_check.on_check
function="Floater.Visible"
parameter="omnifilter" />
</menu_item_check>
<menu_item_separator/>
<menu_item_call

View File

@ -171,6 +171,11 @@ Settings groups to be restored (backup will always save all):
<column name="restore_global_files_label">Controls</column>
<column name="value">key_bindings.xml</column>
</row>
<row name="restore_global_row_controls">
<column type="checkbox" name="restore_global_files_check" value="true" />
<column name="restore_global_files_label">Omnifilter</column>
<column name="value">omnifilter.xml</column>
</row>
</scroll_list>
</layout_panel>

View File

@ -2782,6 +2782,7 @@ name="Command_360_Capture_Label">360° Snapshot</string>
<string name="Command_Poser_Tooltip">Pose your avatar and animated objects</string>
<string name="Command_Primfeed_Label">Primfeed</string>
<string name="Command_Primfeed_Tooltip">Post directly to your Primfeed account.</string>
<string name="Command_Omnifilter_Label">Omnifilter</string>
<string
name="Command_360_Capture_Tooltip">Capture a 360° equirectangular image</string>
@ -2847,6 +2848,7 @@ name="Command_360_Capture_Tooltip">Capture a 360° equirectangular image</string
<string name="Command_RFO_Tooltip">Show only your friends in the viewer, all other avatars will be removed. Once enabled a TP is required to restore visibility of others.</string>
<string name="Command_DAO_Tooltip">Derender Animated Objects (aka Animesh) - Temporarily derenders all currently visible Animesh (attached and free roaming). Derendered animesh will reappear after a TP</string>
<string name="Command_Beacons_Tooltip">Show beacons</string>
<string name="Command_Omnifilter_Tooltip">Edit the Omnifilter rules</string>
<string name="Toolbar_Bottom_Tooltip">currently in your bottom toolbar</string>
<string name="Toolbar_Left_Tooltip" >currently in your left toolbar</string>
@ -3341,4 +3343,5 @@ Your current position: [AVATAR_POS]
<string name="Ultra">Ultra</string>
<string name="Maximum">Maximum</string>
<string name="OmnifilterNewNeedle">NEW RULE</string> <!-- <FS:Zi> Omnifilter -->
</strings>