]> git.netwichtig.de Git - user/henk/code/inspircd.git/commitdiff
Added m_repeat - Allows for blocking of similiar messages
authorDaniel Vassdal <shutter@canternet.org>
Thu, 25 Apr 2013 11:36:48 +0000 (13:36 +0200)
committerattilamolnar <attilamolnar@hush.com>
Mon, 10 Jun 2013 21:20:31 +0000 (23:20 +0200)
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

docs/conf/helpop-full.conf.example
docs/conf/helpop.conf.example
docs/conf/modules.conf.example
src/modules/m_repeat.cpp [new file with mode: 0644]

index c972d04ddde8dd3d102fdf3047da19b328c155c3..0cabfccd0f718d9ca05792f2ffbc317c9d87b1e5 100644 (file)
@@ -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
index d54752cfb79275228199563224a6fe8f472853bf..b4c1e7d67b0e5a7d7000b9a79f49bda2c0c92987 100644 (file)
@@ -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
index d5a5d24c131d18f85b52f79053c92dea390422df..9dfdc37c723a64f382344586642936febb4cd3ba 100644 (file)
 # alternative to /KICK
 #<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.
 #
diff --git a/src/modules/m_repeat.cpp b/src/modules/m_repeat.cpp
new file mode 100644 (file)
index 0000000..5be0fd6
--- /dev/null
@@ -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)