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/>.
21 #include "modules/exemption.h"
39 void serialize(std::string& out) const
41 if (Action == ACT_BAN)
43 else if (Action == ACT_BLOCK)
46 out.append(ConvToStr(Lines)).push_back(':');
47 out.append(ConvToStr(Seconds));
51 out.append(ConvToStr(Diff));
55 out.append(ConvToStr(Backlog));
61 class RepeatMode : public ParamMode<RepeatMode, SimpleExtItem<ChannelSettings> >
68 RepeatItem(time_t TS, const std::string& Line) : ts(TS), line(Line) { }
71 typedef std::deque<RepeatItem> RepeatItemList;
75 RepeatItemList ItemList;
77 MemberInfo() : Counter(0) {}
82 unsigned int MaxLines;
84 unsigned int MaxBacklog;
86 unsigned int MaxMessageSize;
87 ModuleSettings() : MaxLines(0), MaxSecs(0), MaxBacklog(0), MaxDiff() { }
90 std::vector<unsigned int> mx[2];
93 bool CompareLines(const std::string& message, const std::string& historyline, unsigned int trigger)
95 if (message == historyline)
98 return (Levenshtein(message, historyline) <= trigger);
103 unsigned int Levenshtein(const std::string& s1, const std::string& s2)
105 unsigned int l1 = s1.size();
106 unsigned int l2 = s2.size();
108 for (unsigned int i = 0; i < l2; i++)
110 for (unsigned int i = 0; i < l1; i++)
113 for (unsigned int j = 0; j < l2; j++)
114 mx[1][j + 1] = std::min(std::min(mx[1][j] + 1, mx[0][j + 1] + 1), mx[0][j] + ((s1[i] == s2[j]) ? 0 : 1));
122 SimpleExtItem<MemberInfo> MemberInfoExt;
124 RepeatMode(Module* Creator)
125 : ParamMode<RepeatMode, SimpleExtItem<ChannelSettings> >(Creator, "repeat", 'E')
126 , MemberInfoExt("repeat_memb", ExtensionItem::EXT_MEMBERSHIP, Creator)
130 void OnUnset(User* source, Channel* chan)
132 // Unset the per-membership extension when the mode is removed
133 const Channel::MemberMap& users = chan->GetUsers();
134 for (Channel::MemberMap::const_iterator i = users.begin(); i != users.end(); ++i)
135 MemberInfoExt.unset(i->second);
138 ModeAction OnSet(User* source, Channel* channel, std::string& parameter)
140 ChannelSettings settings;
141 if (!ParseSettings(source, parameter, settings))
143 source->WriteNotice("*** Invalid syntax. Syntax is {[~*]}[lines]:[time]{:[difference]}{:[backlog]}");
144 return MODEACTION_DENY;
147 if ((settings.Backlog > 0) && (settings.Lines > settings.Backlog))
149 source->WriteNotice("*** You can't set needed lines higher than backlog");
150 return MODEACTION_DENY;
153 LocalUser* localsource = IS_LOCAL(source);
154 if ((localsource) && (!ValidateSettings(localsource, settings)))
155 return MODEACTION_DENY;
157 ext.set(channel, settings);
159 return MODEACTION_ALLOW;
162 bool MatchLine(Membership* memb, ChannelSettings* rs, std::string message)
164 // If the message is larger than whatever size it's set to,
165 // let's pretend it isn't. If the first 512 (def. setting) match, it's probably spam.
166 if (message.size() > ms.MaxMessageSize)
167 message.erase(ms.MaxMessageSize);
169 MemberInfo* rp = MemberInfoExt.get(memb);
173 MemberInfoExt.set(memb, rp);
176 unsigned int matches = 0;
178 matches = rp->Counter;
180 RepeatItemList& items = rp->ItemList;
181 const unsigned int trigger = (message.size() * rs->Diff / 100);
182 const time_t now = ServerInstance->Time();
184 std::transform(message.begin(), message.end(), message.begin(), ::tolower);
186 for (std::deque<RepeatItem>::iterator it = items.begin(); it != items.end(); ++it)
190 items.erase(it, items.end());
195 if (CompareLines(message, it->line, trigger))
197 if (++matches >= rs->Lines)
199 if (rs->Action != ChannelSettings::ACT_BLOCK)
204 else if ((ms.MaxBacklog == 0) || (rs->Backlog == 0))
212 unsigned int max_items = (rs->Backlog ? rs->Backlog : 1);
213 if (items.size() >= max_items)
216 items.push_front(RepeatItem(now + rs->Seconds, message));
217 rp->Counter = matches;
221 void Resize(size_t size)
223 size_t newsize = size+1;
224 if (newsize <= mx[0].size())
226 ms.MaxMessageSize = size;
227 mx[0].resize(newsize);
228 mx[1].resize(newsize);
233 ConfigTag* conf = ServerInstance->Config->ConfValue("repeat");
234 ms.MaxLines = conf->getInt("maxlines", 20);
235 ms.MaxBacklog = conf->getInt("maxbacklog", 20);
236 ms.MaxSecs = conf->getDuration("maxtime", conf->getInt("maxsecs", 0));
238 ms.MaxDiff = conf->getInt("maxdistance", 50);
239 if (ms.MaxDiff > 100)
242 unsigned int newsize = conf->getInt("size", 512);
243 if (newsize > ServerInstance->Config->Limits.MaxLine)
244 newsize = ServerInstance->Config->Limits.MaxLine;
248 std::string GetModuleSettings() const
250 return ConvToStr(ms.MaxLines) + ":" + ConvToStr(ms.MaxSecs) + ":" + ConvToStr(ms.MaxDiff) + ":" + ConvToStr(ms.MaxBacklog);
253 void SerializeParam(Channel* chan, const ChannelSettings* chset, std::string& out)
255 chset->serialize(out);
259 bool ParseSettings(User* source, std::string& parameter, ChannelSettings& settings)
261 irc::sepstream stream(parameter, ':');
263 if (!stream.GetToken(item))
264 // Required parameter missing
267 if ((item[0] == '*') || (item[0] == '~'))
269 settings.Action = ((item[0] == '*') ? ChannelSettings::ACT_BAN : ChannelSettings::ACT_BLOCK);
270 item.erase(item.begin());
273 settings.Action = ChannelSettings::ACT_KICK;
275 if ((settings.Lines = ConvToInt(item)) == 0)
278 if ((!stream.GetToken(item)) || ((settings.Seconds = InspIRCd::Duration(item)) == 0))
279 // Required parameter missing
282 // The diff and backlog parameters are optional
283 settings.Diff = settings.Backlog = 0;
284 if (stream.GetToken(item))
286 // There is a diff parameter, see if it's valid (> 0)
287 if ((settings.Diff = ConvToInt(item)) == 0)
290 if (stream.GetToken(item))
292 // There is a backlog parameter, see if it's valid
293 if ((settings.Backlog = ConvToInt(item)) == 0)
296 // If there are still tokens, then it's invalid because we allow only 4
297 if (stream.GetToken(item))
305 bool ValidateSettings(LocalUser* source, const ChannelSettings& settings)
307 if (settings.Backlog && !ms.MaxBacklog)
309 source->WriteNotice("*** The server administrator has disabled backlog matching");
315 if (settings.Diff > ms.MaxDiff)
318 source->WriteNotice("*** The server administrator has disabled matching on edit distance");
320 source->WriteNotice("*** The distance you specified is too great. Maximum allowed is " + ConvToStr(ms.MaxDiff));
324 if (ms.MaxLines && settings.Lines > ms.MaxLines)
326 source->WriteNotice("*** The line number you specified is too great. Maximum allowed is " + ConvToStr(ms.MaxLines));
330 if (ms.MaxSecs && settings.Seconds > ms.MaxSecs)
332 source->WriteNotice("*** The seconds you specified is too great. Maximum allowed is " + ConvToStr(ms.MaxSecs));
341 class RepeatModule : public Module
343 CheckExemption::EventProvider exemptionprov;
348 : exemptionprov(this)
353 void ReadConfig(ConfigStatus& status) CXX11_OVERRIDE
358 ModResult OnUserPreMessage(User* user, void* dest, int target_type, std::string& text, char status, CUList& exempt_list, MessageType msgtype) CXX11_OVERRIDE
360 if (target_type != TYPE_CHANNEL || !IS_LOCAL(user))
361 return MOD_RES_PASSTHRU;
363 Channel* chan = reinterpret_cast<Channel*>(dest);
364 ChannelSettings* settings = rm.ext.get(chan);
366 return MOD_RES_PASSTHRU;
368 Membership* memb = chan->GetUser(user);
370 return MOD_RES_PASSTHRU;
372 ModResult res = CheckExemption::Call(exemptionprov, user, chan, "repeat");
373 if (res == MOD_RES_ALLOW)
374 return MOD_RES_PASSTHRU;
376 if (rm.MatchLine(memb, settings, text))
378 if (settings->Action == ChannelSettings::ACT_BLOCK)
380 user->WriteNotice("*** This line is too similar to one of your last lines.");
384 if (settings->Action == ChannelSettings::ACT_BAN)
386 Modes::ChangeList changelist;
387 changelist.push_add(ServerInstance->Modes->FindMode('b', MODETYPE_CHANNEL), "*!*@" + user->GetDisplayedHost());
388 ServerInstance->Modes->Process(ServerInstance->FakeClient, chan, NULL, changelist);
391 memb->chan->KickUser(ServerInstance->FakeClient, user, "Repeat flood");
394 return MOD_RES_PASSTHRU;
397 void Prioritize() CXX11_OVERRIDE
399 ServerInstance->Modules->SetPriority(this, I_OnUserPreMessage, PRIORITY_LAST);
402 Version GetVersion() CXX11_OVERRIDE
404 return Version("Provides the +E channel mode - for blocking of similar messages", VF_COMMON|VF_VENDOR, rm.GetModuleSettings());
408 MODULE_INIT(RepeatModule)