summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDaniel Vassdal <shutter@canternet.org>2013-04-25 13:36:48 +0200
committerattilamolnar <attilamolnar@hush.com>2013-06-10 23:20:31 +0200
commit9a470c5863f796308c8761457cd3e66b8f836380 (patch)
tree543e307286821bee5009b736d467cb21128746e3
parent78900eaa5ea5c02488b7d38b477912c2815f7faf (diff)
Added m_repeat - Allows for blocking of similiar messages
Changes to the original module: - Parse settings using a sepstream, accept remote mode changes regardless of our config - Refuse to link when config settings differ - Style changes All ideas and features are the brainchild and work of Daniel Vassdal
-rw-r--r--docs/conf/helpop-full.conf.example3
-rw-r--r--docs/conf/helpop.conf.example3
-rw-r--r--docs/conf/modules.conf.example20
-rw-r--r--src/modules/m_repeat.cpp424
4 files changed, 450 insertions, 0 deletions
diff --git a/docs/conf/helpop-full.conf.example b/docs/conf/helpop-full.conf.example
index c972d04dd..0cabfccd0 100644
--- a/docs/conf/helpop-full.conf.example
+++ b/docs/conf/helpop-full.conf.example
@@ -874,6 +874,9 @@ Closes all unregistered connections to the local server.">
module).
D Delays join messages from users until they
message the channel (requires delayjoin module).
+ E [~*][lines]:[sec]{[:difference]}{[:backlog]} Allows blocking of similiar messages.
+ Kicks as default, blocks with ~ and bans with *
+ The last two parameters are optional.
F [changes]:[sec] Blocks nick changes when they equal or exceed the
specified rate (requires nickflood module).
G Censors messages to the channel based on the
diff --git a/docs/conf/helpop.conf.example b/docs/conf/helpop.conf.example
index d54752cfb..b4c1e7d67 100644
--- a/docs/conf/helpop.conf.example
+++ b/docs/conf/helpop.conf.example
@@ -181,6 +181,9 @@ LOCKSERV UNLOCKSERV JUMPSERVER">
module).
D Delays join messages from users until they
message the channel (requires delayjoin module).
+ E [~*][lines]:[sec]{[:difference]}{[:backlog]} Allows blocking of similiar messages.
+ Kicks as default, blocks with ~ and bans with *
+ The last two parameters are optional.
F [changes]:[sec] Blocks nick changes when they equal or exceed the
specified rate (requires nickflood module).
G Censors messages to the channel based on the
diff --git a/docs/conf/modules.conf.example b/docs/conf/modules.conf.example
index d5a5d24c1..9dfdc37c7 100644
--- a/docs/conf/modules.conf.example
+++ b/docs/conf/modules.conf.example
@@ -1424,6 +1424,26 @@
#<module name="m_remove.so">
#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
+# A module to block, kick or ban upon similiar messages being uttered several times.
+# Syntax [~*][lines]:[sec]{[:difference]}{[:matchlines]}
+# ~ is to block, * is to ban, default is kick.
+# lines - In mode 1 the amount of lines that has to match consecutively - In mode 2 the size of the backlog to keep for matching
+# seconds - How old the message has to be before it's invalidated.
+# distance - Edit distance, in percent, between two strings to trigger on.
+# matchlines - When set, the function goes into mode 2. In this mode the function will trigger if this many of the last <lines> matches.
+#
+# As this module can be rather CPU-intensive, it comes with some options.
+# maxbacklog - Maximum size that can be specified for backlog. 0 disables multiline matching.
+# maxdistance - Max percentage of difference between two lines we'll allow to match. Set to 0 to disable edit-distance matching.
+# maxlines - Max lines of backlog to match against.
+# maxsecs - Maximum value of seconds a user can set. 0 to allow any.
+# size - Maximum number of characters to check for, can be used to truncate messages
+# before they are checked, resulting in less CPU usage. Increasing this beyond 512
+# doesn't have any effect, as the maximum length of a message on IRC cannot exceed that.
+#<repeat maxbacklog="20" maxlines="20" maxdistance="50" maxsecs="0" size="512">
+#<module name="m_repeat.so">
+
+#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#-#
# Restricted channels module: Allows only opers to create channels.
#
# You probably *DO NOT* want to load this module on a public network.
diff --git a/src/modules/m_repeat.cpp b/src/modules/m_repeat.cpp
new file mode 100644
index 000000000..5be0fd622
--- /dev/null
+++ b/src/modules/m_repeat.cpp
@@ -0,0 +1,424 @@
+/*
+ * InspIRCd -- Internet Relay Chat Daemon
+ *
+ * Copyright (C) 2013 Daniel Vassdal <shutter@canternet.org>
+ *
+ * This file is part of InspIRCd. InspIRCd is free software: you can
+ * redistribute it and/or modify it under the terms of the GNU General Public
+ * License as published by the Free Software Foundation, version 2.
+ *
+ * This program 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 General Public License for more
+ * details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+
+#include "inspircd.h"
+
+#ifdef _WIN32
+// windows.h defines this
+#undef min
+#endif
+
+class RepeatMode : public ModeHandler
+{
+ private:
+ struct RepeatItem
+ {
+ time_t ts;
+ std::string line;
+ RepeatItem(time_t TS, const std::string& Line) : ts(TS), line(Line) { }
+ };
+
+ typedef std::deque<RepeatItem> RepeatItemList;
+
+ struct MemberInfo
+ {
+ RepeatItemList ItemList;
+ unsigned int Counter;
+ MemberInfo() : Counter(0) {}
+ };
+
+ struct ModuleSettings
+ {
+ unsigned int MaxLines;
+ unsigned int MaxSecs;
+ unsigned int MaxBacklog;
+ unsigned int MaxDiff;
+ ModuleSettings() : MaxLines(0), MaxSecs(0), MaxBacklog(0), MaxDiff() { }
+ };
+
+ std::vector<std::vector<unsigned int> > mx;
+ ModuleSettings ms;
+
+ bool CompareLines(const std::string& message, const std::string& historyline, unsigned int trigger)
+ {
+ if (trigger)
+ return (Levenshtein(message, historyline) <= trigger);
+ else
+ return (message == historyline);
+ }
+
+ unsigned int Levenshtein(const std::string& s1, const std::string& s2)
+ {
+ unsigned int l1 = s1.size();
+ unsigned int l2 = s2.size();
+
+ for (unsigned int i = 0; i <= l1; i++)
+ mx[i][0] = i;
+ for (unsigned int i = 0; i <= l2; i++)
+ mx[0][i] = i;
+ for (unsigned int i = 1; i <= l1; i++)
+ for (unsigned int j = 1; j <= l2; j++)
+ mx[i][j] = std::min(std::min(mx[i - 1][j] + 1, mx[i][j - 1] + 1), mx[i - 1][j - 1] + (s1[i - 1] == s2[j - 1] ? 0 : 1));
+ return (mx[l1][l2]);
+ }
+
+ public:
+ enum RepeatAction
+ {
+ ACT_KICK,
+ ACT_BLOCK,
+ ACT_BAN
+ };
+
+ class ChannelSettings
+ {
+ public:
+ RepeatAction Action;
+ unsigned int Backlog;
+ unsigned int Lines;
+ unsigned int Diff;
+ unsigned int Seconds;
+
+ std::string serialize()
+ {
+ std::string ret = ((Action == ACT_BAN) ? "*" : (Action == ACT_BLOCK ? "~" : "")) + ConvToStr(Lines) + ":" + ConvToStr(Seconds);
+ if (Diff)
+ {
+ ret += ":" + ConvToStr(Diff);
+ if (Backlog)
+ ret += ":" + ConvToStr(Backlog);
+ }
+ return ret;
+ }
+ };
+
+ SimpleExtItem<MemberInfo> MemberInfoExt;
+ SimpleExtItem<ChannelSettings> ChanSet;
+
+ RepeatMode(Module* Creator)
+ : ModeHandler(Creator, "repeat", 'E', PARAM_SETONLY, MODETYPE_CHANNEL)
+ , MemberInfoExt("repeat_memb", Creator)
+ , ChanSet("repeat", Creator)
+ {
+ }
+
+ ModeAction OnModeChange(User* source, User* dest, Channel* channel, std::string& parameter, bool adding)
+ {
+ if (!adding)
+ {
+ if (!channel->IsModeSet(this))
+ return MODEACTION_DENY;
+
+ // Unset the per-membership extension when the mode is removed
+ const UserMembList* users = channel->GetUsers();
+ for (UserMembCIter i = users->begin(); i != users->end(); ++i)
+ MemberInfoExt.unset(i->second);
+
+ ChanSet.unset(channel);
+ channel->SetModeParam(this, "");
+ return MODEACTION_ALLOW;
+ }
+
+ if (channel->GetModeParameter(this) == parameter)
+ return MODEACTION_DENY;
+
+ ChannelSettings settings;
+ if (!ParseSettings(source, parameter, settings))
+ {
+ source->WriteNotice("*** Invalid syntax. Syntax is {[~*]}[lines]:[time]{:[difference]}{:[backlog]}");
+ return MODEACTION_DENY;
+ }
+
+ if ((settings.Backlog > 0) && (settings.Lines > settings.Backlog))
+ {
+ source->WriteNotice("*** You can't set needed lines higher than backlog");
+ return MODEACTION_DENY;
+ }
+
+ LocalUser* localsource = IS_LOCAL(source);
+ if ((localsource) && (!ValidateSettings(localsource, settings)))
+ return MODEACTION_DENY;
+
+ ChanSet.set(channel, settings);
+ channel->SetModeParam(this, parameter);
+
+ return MODEACTION_ALLOW;
+ }
+
+ bool MatchLine(Membership* memb, ChannelSettings* rs, std::string message)
+ {
+ // If the message is larger than whatever size it's set to,
+ // let's pretend it isn't. If the first 512 (def. setting) match, it's probably spam.
+ if (message.size() > mx.size())
+ message.erase(mx.size());
+
+ MemberInfo* rp = MemberInfoExt.get(memb);
+ if (!rp)
+ {
+ rp = new MemberInfo;
+ MemberInfoExt.set(memb, rp);
+ }
+
+ unsigned int matches = 0;
+ if (!rs->Backlog)
+ matches = rp->Counter;
+
+ RepeatItemList& items = rp->ItemList;
+ const unsigned int trigger = (message.size() * rs->Diff / 100);
+ const time_t now = ServerInstance->Time();
+
+ std::transform(message.begin(), message.end(), message.begin(), ::tolower);
+
+ for (std::deque<RepeatItem>::iterator it = items.begin(); it != items.end(); ++it)
+ {
+ if (it->ts < now)
+ {
+ items.erase(it, items.end());
+ matches = 0;
+ break;
+ }
+
+ if (CompareLines(message, it->line, trigger))
+ {
+ if (++matches >= rs->Lines)
+ {
+ if (rs->Action != ACT_BLOCK)
+ rp->Counter = 0;
+ return true;
+ }
+ }
+ else if ((ms.MaxBacklog == 0) || (rs->Backlog == 0))
+ {
+ matches = 0;
+ items.clear();
+ break;
+ }
+ }
+
+ unsigned int max_items = (rs->Backlog ? rs->Backlog : 1);
+ if (items.size() >= max_items)
+ items.pop_back();
+
+ items.push_front(RepeatItem(now + rs->Seconds, message));
+ rp->Counter = matches;
+ return false;
+ }
+
+ void Resize(size_t size)
+ {
+ if (size == mx.size())
+ return;
+ mx.resize(size);
+
+ if (mx.size() > size)
+ {
+ mx.resize(size);
+ for (unsigned int i = 0; i < mx.size(); i++)
+ mx[i].resize(size);
+ }
+ else
+ {
+ for (unsigned int i = 0; i < mx.size(); i++)
+ {
+ mx[i].resize(size);
+ std::vector<unsigned int>(mx[i]).swap(mx[i]);
+ }
+ std::vector<std::vector<unsigned int> >(mx).swap(mx);
+ }
+ }
+
+ void ReadConfig()
+ {
+ ConfigTag* conf = ServerInstance->Config->ConfValue("repeat");
+ ms.MaxLines = conf->getInt("maxlines", 20);
+ ms.MaxBacklog = conf->getInt("maxbacklog", 20);
+ ms.MaxSecs = conf->getInt("maxsecs", 0);
+
+ ms.MaxDiff = conf->getInt("maxdistance", 50);
+ if (ms.MaxDiff > 100)
+ ms.MaxDiff = 100;
+
+ unsigned int newsize = conf->getInt("size", 512);
+ if (newsize > ServerInstance->Config->Limits.MaxLine)
+ newsize = ServerInstance->Config->Limits.MaxLine;
+ Resize(newsize);
+ }
+
+ std::string GetModuleSettings() const
+ {
+ return ConvToStr(ms.MaxLines) + ":" + ConvToStr(ms.MaxSecs) + ":" + ConvToStr(ms.MaxDiff) + ":" + ConvToStr(ms.MaxBacklog);
+ }
+
+ private:
+ bool ParseSettings(User* source, std::string& parameter, ChannelSettings& settings)
+ {
+ irc::sepstream stream(parameter, ':');
+ std::string item;
+ if (!stream.GetToken(item))
+ // Required parameter missing
+ return false;
+
+ if ((item[0] == '*') || (item[0] == '~'))
+ {
+ settings.Action = ((item[0] == '*') ? ACT_BAN : ACT_BLOCK);
+ item.erase(item.begin());
+ }
+ else
+ settings.Action = ACT_KICK;
+
+ if ((settings.Lines = ConvToInt(item)) == 0)
+ return false;
+
+ if ((!stream.GetToken(item)) || ((settings.Seconds = InspIRCd::Duration(item)) == 0))
+ // Required parameter missing
+ return false;
+
+ // The diff and backlog parameters are optional
+ settings.Diff = settings.Backlog = 0;
+ if (stream.GetToken(item))
+ {
+ // There is a diff parameter, see if it's valid (> 0)
+ if ((settings.Diff = ConvToInt(item)) == 0)
+ return false;
+
+ if (stream.GetToken(item))
+ {
+ // There is a backlog parameter, see if it's valid
+ if ((settings.Backlog = ConvToInt(item)) == 0)
+ return false;
+
+ // If there are still tokens, then it's invalid because we allow only 4
+ if (stream.GetToken(item))
+ return false;
+ }
+ }
+
+ parameter = settings.serialize();
+ return true;
+ }
+
+ bool ValidateSettings(LocalUser* source, const ChannelSettings& settings)
+ {
+ if (settings.Backlog && !ms.MaxBacklog)
+ {
+ source->WriteNotice("*** The server administrator has disabled backlog matching");
+ return false;
+ }
+
+ if (settings.Diff)
+ {
+ if (settings.Diff > ms.MaxDiff)
+ {
+ if (ms.MaxDiff == 0)
+ source->WriteNotice("*** The server administrator has disabled matching on edit distance");
+ else
+ source->WriteNotice("*** The distance you specified is too great. Maximum allowed is " + ConvToStr(ms.MaxDiff));
+ return false;
+ }
+
+ if (ms.MaxLines && settings.Lines > ms.MaxLines)
+ {
+ source->WriteNotice("*** The line number you specified is too great. Maximum allowed is " + ConvToStr(ms.MaxLines));
+ return false;
+ }
+
+ if (ms.MaxSecs && settings.Seconds > ms.MaxSecs)
+ {
+ source->WriteNotice("*** The seconds you specified is too great. Maximum allowed is " + ConvToStr(ms.MaxSecs));
+ return false;
+ }
+ }
+
+ return true;
+ }
+};
+
+class RepeatModule : public Module
+{
+ RepeatMode rm;
+
+ public:
+ RepeatModule() : rm(this) {}
+
+ void init() CXX11_OVERRIDE
+ {
+ ServerInstance->Modules->AddService(rm);
+ ServerInstance->Modules->AddService(rm.ChanSet);
+ ServerInstance->Modules->AddService(rm.MemberInfoExt);
+ Implementation eventlist[] = { I_OnUserPreMessage, I_OnRehash };
+ ServerInstance->Modules->Attach(eventlist, this, sizeof(eventlist)/sizeof(Implementation));
+ rm.ReadConfig();
+ }
+
+ void OnRehash(User* user) CXX11_OVERRIDE
+ {
+ rm.ReadConfig();
+ }
+
+ ModResult OnUserPreMessage(User* user, void* dest, int target_type, std::string& text, char status, CUList& exempt_list, MessageType msgtype) CXX11_OVERRIDE
+ {
+ if (target_type != TYPE_CHANNEL || !IS_LOCAL(user))
+ return MOD_RES_PASSTHRU;
+
+ Membership* memb = ((Channel*)dest)->GetUser(user);
+ if (!memb || !memb->chan->IsModeSet(&rm))
+ return MOD_RES_PASSTHRU;
+
+ if (ServerInstance->OnCheckExemption(user, memb->chan, "repeat") == MOD_RES_ALLOW)
+ return MOD_RES_PASSTHRU;
+
+ RepeatMode::ChannelSettings* settings = rm.ChanSet.get(memb->chan);
+ if (!settings)
+ return MOD_RES_PASSTHRU;
+
+ if (rm.MatchLine(memb, settings, text))
+ {
+ if (settings->Action == RepeatMode::ACT_BLOCK)
+ {
+ user->WriteNotice("*** This line is too similiar to one of your last lines.");
+ return MOD_RES_DENY;
+ }
+
+ if (settings->Action == RepeatMode::ACT_BAN)
+ {
+ std::vector<std::string> parameters;
+ parameters.push_back(memb->chan->name);
+ parameters.push_back("+b");
+ parameters.push_back("*!*@" + user->dhost);
+ ServerInstance->SendGlobalMode(parameters, ServerInstance->FakeClient);
+ }
+
+ memb->chan->KickUser(ServerInstance->FakeClient, user, "Repeat flood");
+ return MOD_RES_DENY;
+ }
+ return MOD_RES_PASSTHRU;
+ }
+
+ void Prioritize() CXX11_OVERRIDE
+ {
+ ServerInstance->Modules->SetPriority(this, I_OnUserPreMessage, PRIORITY_LAST);
+ }
+
+ Version GetVersion() CXX11_OVERRIDE
+ {
+ return Version("Provides the +E channel mode - for blocking of similiar messages", VF_COMMON|VF_VENDOR, rm.GetModuleSettings());
+ }
+};
+
+MODULE_INIT(RepeatModule)