2 * InspIRCd -- Internet Relay Chat Daemon
4 * Copyright (C) 2013 Daniel Vassdal <shutter@canternet.org>
6 * This file is part of InspIRCd. InspIRCd is free software: you can
7 * redistribute it and/or modify it under the terms of the GNU General Public
8 * License as published by the Free Software Foundation, version 2.
10 * This program is distributed in the hope that it will be useful, but WITHOUT
11 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
23 // windows.h defines this
27 class RepeatMode : public ModeHandler
34 RepeatItem(time_t TS, const std::string& Line) : ts(TS), line(Line) { }
37 typedef std::deque<RepeatItem> RepeatItemList;
41 RepeatItemList ItemList;
43 MemberInfo() : Counter(0) {}
48 unsigned int MaxLines;
50 unsigned int MaxBacklog;
52 ModuleSettings() : MaxLines(0), MaxSecs(0), MaxBacklog(0), MaxDiff() { }
55 std::vector<std::vector<unsigned int> > mx;
58 bool CompareLines(const std::string& message, const std::string& historyline, unsigned int trigger)
61 return (Levenshtein(message, historyline) <= trigger);
63 return (message == historyline);
66 unsigned int Levenshtein(const std::string& s1, const std::string& s2)
68 unsigned int l1 = s1.size();
69 unsigned int l2 = s2.size();
71 for (unsigned int i = 0; i <= l1; i++)
73 for (unsigned int i = 0; i <= l2; i++)
75 for (unsigned int i = 1; i <= l1; i++)
76 for (unsigned int j = 1; j <= l2; j++)
77 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));
98 std::string serialize()
100 std::string ret = ((Action == ACT_BAN) ? "*" : (Action == ACT_BLOCK ? "~" : "")) + ConvToStr(Lines) + ":" + ConvToStr(Seconds);
103 ret += ":" + ConvToStr(Diff);
105 ret += ":" + ConvToStr(Backlog);
111 SimpleExtItem<MemberInfo> MemberInfoExt;
112 SimpleExtItem<ChannelSettings> ChanSet;
114 RepeatMode(Module* Creator)
115 : ModeHandler(Creator, "repeat", 'E', PARAM_SETONLY, MODETYPE_CHANNEL)
116 , MemberInfoExt("repeat_memb", Creator)
117 , ChanSet("repeat", Creator)
121 ModeAction OnModeChange(User* source, User* dest, Channel* channel, std::string& parameter, bool adding)
125 if (!channel->IsModeSet(this))
126 return MODEACTION_DENY;
128 // Unset the per-membership extension when the mode is removed
129 const UserMembList* users = channel->GetUsers();
130 for (UserMembCIter i = users->begin(); i != users->end(); ++i)
131 MemberInfoExt.unset(i->second);
133 ChanSet.unset(channel);
134 channel->SetModeParam(this, "");
135 return MODEACTION_ALLOW;
138 if (channel->GetModeParameter(this) == parameter)
139 return MODEACTION_DENY;
141 ChannelSettings settings;
142 if (!ParseSettings(source, parameter, settings))
144 source->WriteNotice("*** Invalid syntax. Syntax is {[~*]}[lines]:[time]{:[difference]}{:[backlog]}");
145 return MODEACTION_DENY;
148 if ((settings.Backlog > 0) && (settings.Lines > settings.Backlog))
150 source->WriteNotice("*** You can't set needed lines higher than backlog");
151 return MODEACTION_DENY;
154 LocalUser* localsource = IS_LOCAL(source);
155 if ((localsource) && (!ValidateSettings(localsource, settings)))
156 return MODEACTION_DENY;
158 ChanSet.set(channel, settings);
159 channel->SetModeParam(this, parameter);
161 return MODEACTION_ALLOW;
164 bool MatchLine(Membership* memb, ChannelSettings* rs, std::string message)
166 // If the message is larger than whatever size it's set to,
167 // let's pretend it isn't. If the first 512 (def. setting) match, it's probably spam.
168 if (message.size() > mx.size())
169 message.erase(mx.size());
171 MemberInfo* rp = MemberInfoExt.get(memb);
175 MemberInfoExt.set(memb, rp);
178 unsigned int matches = 0;
180 matches = rp->Counter;
182 RepeatItemList& items = rp->ItemList;
183 const unsigned int trigger = (message.size() * rs->Diff / 100);
184 const time_t now = ServerInstance->Time();
186 std::transform(message.begin(), message.end(), message.begin(), ::tolower);
188 for (std::deque<RepeatItem>::iterator it = items.begin(); it != items.end(); ++it)
192 items.erase(it, items.end());
197 if (CompareLines(message, it->line, trigger))
199 if (++matches >= rs->Lines)
201 if (rs->Action != ACT_BLOCK)
206 else if ((ms.MaxBacklog == 0) || (rs->Backlog == 0))
214 unsigned int max_items = (rs->Backlog ? rs->Backlog : 1);
215 if (items.size() >= max_items)
218 items.push_front(RepeatItem(now + rs->Seconds, message));
219 rp->Counter = matches;
223 void Resize(size_t size)
225 if (size == mx.size())
229 if (mx.size() > size)
232 for (unsigned int i = 0; i < mx.size(); i++)
237 for (unsigned int i = 0; i < mx.size(); i++)
240 std::vector<unsigned int>(mx[i]).swap(mx[i]);
242 std::vector<std::vector<unsigned int> >(mx).swap(mx);
248 ConfigTag* conf = ServerInstance->Config->ConfValue("repeat");
249 ms.MaxLines = conf->getInt("maxlines", 20);
250 ms.MaxBacklog = conf->getInt("maxbacklog", 20);
251 ms.MaxSecs = conf->getInt("maxsecs", 0);
253 ms.MaxDiff = conf->getInt("maxdistance", 50);
254 if (ms.MaxDiff > 100)
257 unsigned int newsize = conf->getInt("size", 512);
258 if (newsize > ServerInstance->Config->Limits.MaxLine)
259 newsize = ServerInstance->Config->Limits.MaxLine;
263 std::string GetModuleSettings() const
265 return ConvToStr(ms.MaxLines) + ":" + ConvToStr(ms.MaxSecs) + ":" + ConvToStr(ms.MaxDiff) + ":" + ConvToStr(ms.MaxBacklog);
269 bool ParseSettings(User* source, std::string& parameter, ChannelSettings& settings)
271 irc::sepstream stream(parameter, ':');
273 if (!stream.GetToken(item))
274 // Required parameter missing
277 if ((item[0] == '*') || (item[0] == '~'))
279 settings.Action = ((item[0] == '*') ? ACT_BAN : ACT_BLOCK);
280 item.erase(item.begin());
283 settings.Action = ACT_KICK;
285 if ((settings.Lines = ConvToInt(item)) == 0)
288 if ((!stream.GetToken(item)) || ((settings.Seconds = InspIRCd::Duration(item)) == 0))
289 // Required parameter missing
292 // The diff and backlog parameters are optional
293 settings.Diff = settings.Backlog = 0;
294 if (stream.GetToken(item))
296 // There is a diff parameter, see if it's valid (> 0)
297 if ((settings.Diff = ConvToInt(item)) == 0)
300 if (stream.GetToken(item))
302 // There is a backlog parameter, see if it's valid
303 if ((settings.Backlog = ConvToInt(item)) == 0)
306 // If there are still tokens, then it's invalid because we allow only 4
307 if (stream.GetToken(item))
312 parameter = settings.serialize();
316 bool ValidateSettings(LocalUser* source, const ChannelSettings& settings)
318 if (settings.Backlog && !ms.MaxBacklog)
320 source->WriteNotice("*** The server administrator has disabled backlog matching");
326 if (settings.Diff > ms.MaxDiff)
329 source->WriteNotice("*** The server administrator has disabled matching on edit distance");
331 source->WriteNotice("*** The distance you specified is too great. Maximum allowed is " + ConvToStr(ms.MaxDiff));
335 if (ms.MaxLines && settings.Lines > ms.MaxLines)
337 source->WriteNotice("*** The line number you specified is too great. Maximum allowed is " + ConvToStr(ms.MaxLines));
341 if (ms.MaxSecs && settings.Seconds > ms.MaxSecs)
343 source->WriteNotice("*** The seconds you specified is too great. Maximum allowed is " + ConvToStr(ms.MaxSecs));
352 class RepeatModule : public Module
357 RepeatModule() : rm(this) {}
359 void init() CXX11_OVERRIDE
361 ServerInstance->Modules->AddService(rm);
362 ServerInstance->Modules->AddService(rm.ChanSet);
363 ServerInstance->Modules->AddService(rm.MemberInfoExt);
364 Implementation eventlist[] = { I_OnUserPreMessage, I_OnRehash };
365 ServerInstance->Modules->Attach(eventlist, this, sizeof(eventlist)/sizeof(Implementation));
369 void OnRehash(User* user) CXX11_OVERRIDE
374 ModResult OnUserPreMessage(User* user, void* dest, int target_type, std::string& text, char status, CUList& exempt_list, MessageType msgtype) CXX11_OVERRIDE
376 if (target_type != TYPE_CHANNEL || !IS_LOCAL(user))
377 return MOD_RES_PASSTHRU;
379 Membership* memb = ((Channel*)dest)->GetUser(user);
380 if (!memb || !memb->chan->IsModeSet(&rm))
381 return MOD_RES_PASSTHRU;
383 if (ServerInstance->OnCheckExemption(user, memb->chan, "repeat") == MOD_RES_ALLOW)
384 return MOD_RES_PASSTHRU;
386 RepeatMode::ChannelSettings* settings = rm.ChanSet.get(memb->chan);
388 return MOD_RES_PASSTHRU;
390 if (rm.MatchLine(memb, settings, text))
392 if (settings->Action == RepeatMode::ACT_BLOCK)
394 user->WriteNotice("*** This line is too similiar to one of your last lines.");
398 if (settings->Action == RepeatMode::ACT_BAN)
400 std::vector<std::string> parameters;
401 parameters.push_back(memb->chan->name);
402 parameters.push_back("+b");
403 parameters.push_back("*!*@" + user->dhost);
404 ServerInstance->Modes->Process(parameters, ServerInstance->FakeClient);
407 memb->chan->KickUser(ServerInstance->FakeClient, user, "Repeat flood");
410 return MOD_RES_PASSTHRU;
413 void Prioritize() CXX11_OVERRIDE
415 ServerInstance->Modules->SetPriority(this, I_OnUserPreMessage, PRIORITY_LAST);
418 Version GetVersion() CXX11_OVERRIDE
420 return Version("Provides the +E channel mode - for blocking of similiar messages", VF_COMMON|VF_VENDOR, rm.GetModuleSettings());
424 MODULE_INIT(RepeatModule)