2 * InspIRCd -- Internet Relay Chat Daemon
4 * Copyright (C) 2019 Robby <robby@chatbelgie.be>
5 * Copyright (C) 2018-2019 linuxdaemon <linuxdaemon.irc@gmail.com>
6 * Copyright (C) 2018 Matt Schatz <genius3000@g3k.solutions>
7 * Copyright (C) 2017-2019 Sadie Powell <sadie@witchery.services>
8 * Copyright (C) 2015 James Lu <GLolol@overdrivenetworks.com>
9 * Copyright (C) 2013-2015 Attila Molnar <attilamolnar@hush.com>
10 * Copyright (C) 2013 Daniel Vassdal <shutter@canternet.org>
12 * This file is part of InspIRCd. InspIRCd is free software: you can
13 * redistribute it and/or modify it under the terms of the GNU General Public
14 * License as published by the Free Software Foundation, version 2.
16 * This program is distributed in the hope that it will be useful, but WITHOUT
17 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
18 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
21 * You should have received a copy of the GNU General Public License
22 * along with this program. If not, see <http://www.gnu.org/licenses/>.
27 #include "modules/exemption.h"
43 unsigned long Seconds;
45 void serialize(std::string& out) const
47 if (Action == ACT_BAN)
49 else if (Action == ACT_BLOCK)
52 out.append(ConvToStr(Lines)).push_back(':');
53 out.append(ConvToStr(Seconds));
57 out.append(ConvToStr(Diff));
61 out.append(ConvToStr(Backlog));
67 class RepeatMode : public ParamMode<RepeatMode, SimpleExtItem<ChannelSettings> >
74 RepeatItem(time_t TS, const std::string& Line) : ts(TS), line(Line) { }
77 typedef std::deque<RepeatItem> RepeatItemList;
81 RepeatItemList ItemList;
83 MemberInfo() : Counter(0) {}
88 unsigned int MaxLines;
90 unsigned int MaxBacklog;
92 unsigned int MaxMessageSize;
93 ModuleSettings() : MaxLines(0), MaxSecs(0), MaxBacklog(0), MaxDiff() { }
96 std::vector<unsigned int> mx[2];
99 bool CompareLines(const std::string& message, const std::string& historyline, unsigned int trigger)
101 if (message == historyline)
104 return (Levenshtein(message, historyline) <= trigger);
109 unsigned int Levenshtein(const std::string& s1, const std::string& s2)
111 unsigned int l1 = s1.size();
112 unsigned int l2 = s2.size();
114 for (unsigned int i = 0; i < l2; i++)
116 for (unsigned int i = 0; i < l1; i++)
119 for (unsigned int j = 0; j < l2; j++)
120 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));
128 SimpleExtItem<MemberInfo> MemberInfoExt;
130 RepeatMode(Module* Creator)
131 : ParamMode<RepeatMode, SimpleExtItem<ChannelSettings> >(Creator, "repeat", 'E')
132 , MemberInfoExt("repeat_memb", ExtensionItem::EXT_MEMBERSHIP, Creator)
134 syntax = "[~|*]<lines>:<sec>[:<difference>][:<backlog>]";
137 void OnUnset(User* source, Channel* chan) CXX11_OVERRIDE
139 // Unset the per-membership extension when the mode is removed
140 const Channel::MemberMap& users = chan->GetUsers();
141 for (Channel::MemberMap::const_iterator i = users.begin(); i != users.end(); ++i)
142 MemberInfoExt.unset(i->second);
145 ModeAction OnSet(User* source, Channel* channel, std::string& parameter) CXX11_OVERRIDE
147 ChannelSettings settings;
148 if (!ParseSettings(source, parameter, settings))
150 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter));
151 return MODEACTION_DENY;
154 if ((settings.Backlog > 0) && (settings.Lines > settings.Backlog))
156 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter,
157 "You can't set lines higher than backlog."));
158 return MODEACTION_DENY;
161 LocalUser* localsource = IS_LOCAL(source);
162 if ((localsource) && (!ValidateSettings(localsource, channel, parameter, settings)))
163 return MODEACTION_DENY;
165 ext.set(channel, settings);
167 return MODEACTION_ALLOW;
170 bool MatchLine(Membership* memb, ChannelSettings* rs, std::string message)
172 // If the message is larger than whatever size it's set to,
173 // let's pretend it isn't. If the first 512 (def. setting) match, it's probably spam.
174 if (message.size() > ms.MaxMessageSize)
175 message.erase(ms.MaxMessageSize);
177 MemberInfo* rp = MemberInfoExt.get(memb);
181 MemberInfoExt.set(memb, rp);
184 unsigned int matches = 0;
186 matches = rp->Counter;
188 RepeatItemList& items = rp->ItemList;
189 const unsigned int trigger = (message.size() * rs->Diff / 100);
190 const time_t now = ServerInstance->Time();
192 std::transform(message.begin(), message.end(), message.begin(), ::tolower);
194 for (std::deque<RepeatItem>::iterator it = items.begin(); it != items.end(); ++it)
198 items.erase(it, items.end());
203 if (CompareLines(message, it->line, trigger))
205 if (++matches >= rs->Lines)
207 if (rs->Action != ChannelSettings::ACT_BLOCK)
212 else if ((ms.MaxBacklog == 0) || (rs->Backlog == 0))
220 unsigned int max_items = (rs->Backlog ? rs->Backlog : 1);
221 if (items.size() >= max_items)
224 items.push_front(RepeatItem(now + rs->Seconds, message));
225 rp->Counter = matches;
229 void Resize(size_t size)
231 size_t newsize = size+1;
232 if (newsize <= mx[0].size())
234 ms.MaxMessageSize = size;
235 mx[0].resize(newsize);
236 mx[1].resize(newsize);
241 ConfigTag* conf = ServerInstance->Config->ConfValue("repeat");
242 ms.MaxLines = conf->getUInt("maxlines", 20);
243 ms.MaxBacklog = conf->getUInt("maxbacklog", 20);
244 ms.MaxSecs = conf->getDuration("maxtime", conf->getDuration("maxsecs", 0));
246 ms.MaxDiff = conf->getUInt("maxdistance", 50);
247 if (ms.MaxDiff > 100)
250 unsigned int newsize = conf->getUInt("size", 512);
251 if (newsize > ServerInstance->Config->Limits.MaxLine)
252 newsize = ServerInstance->Config->Limits.MaxLine;
256 std::string GetModuleSettings() const
258 return ConvToStr(ms.MaxLines) + ":" + ConvToStr(ms.MaxSecs) + ":" + ConvToStr(ms.MaxDiff) + ":" + ConvToStr(ms.MaxBacklog);
261 void SerializeParam(Channel* chan, const ChannelSettings* chset, std::string& out)
263 chset->serialize(out);
267 bool ParseSettings(User* source, std::string& parameter, ChannelSettings& settings)
269 irc::sepstream stream(parameter, ':');
271 if (!stream.GetToken(item))
272 // Required parameter missing
275 if ((item[0] == '*') || (item[0] == '~'))
277 settings.Action = ((item[0] == '*') ? ChannelSettings::ACT_BAN : ChannelSettings::ACT_BLOCK);
278 item.erase(item.begin());
281 settings.Action = ChannelSettings::ACT_KICK;
283 if ((settings.Lines = ConvToNum<unsigned int>(item)) == 0)
286 if ((!stream.GetToken(item)) || !InspIRCd::Duration(item, settings.Seconds) || (settings.Seconds == 0))
287 // Required parameter missing
290 // The diff and backlog parameters are optional
291 settings.Diff = settings.Backlog = 0;
292 if (stream.GetToken(item))
294 // There is a diff parameter, see if it's valid (> 0)
295 if ((settings.Diff = ConvToNum<unsigned int>(item)) == 0)
298 if (stream.GetToken(item))
300 // There is a backlog parameter, see if it's valid
301 if ((settings.Backlog = ConvToNum<unsigned int>(item)) == 0)
304 // If there are still tokens, then it's invalid because we allow only 4
305 if (stream.GetToken(item))
313 bool ValidateSettings(LocalUser* source, Channel* channel, const std::string& parameter, const ChannelSettings& settings)
315 if (ms.MaxLines && settings.Lines > ms.MaxLines)
317 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, InspIRCd::Format(
318 "The line number you specified is too big. Maximum allowed is %u.", ms.MaxLines)));
322 if (ms.MaxSecs && settings.Seconds > ms.MaxSecs)
324 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, InspIRCd::Format(
325 "The seconds you specified are too big. Maximum allowed is %u.", ms.MaxSecs)));
329 if (settings.Diff && settings.Diff > ms.MaxDiff)
332 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter,
333 "The server administrator has disabled matching on edit distance."));
335 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, InspIRCd::Format(
336 "The distance you specified is too big. Maximum allowed is %u.", ms.MaxDiff)));
340 if (settings.Backlog && settings.Backlog > ms.MaxBacklog)
342 if (ms.MaxBacklog == 0)
343 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter,
344 "The server administrator has disabled backlog matching."));
346 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, InspIRCd::Format(
347 "The backlog you specified is too big. Maximum allowed is %u.", ms.MaxBacklog)));
355 class RepeatModule : public Module
357 CheckExemption::EventProvider exemptionprov;
362 : exemptionprov(this)
367 void ReadConfig(ConfigStatus& status) CXX11_OVERRIDE
372 ModResult OnUserPreMessage(User* user, const MessageTarget& target, MessageDetails& details) CXX11_OVERRIDE
374 if (target.type != MessageTarget::TYPE_CHANNEL || !IS_LOCAL(user))
375 return MOD_RES_PASSTHRU;
377 Channel* chan = target.Get<Channel>();
378 ChannelSettings* settings = rm.ext.get(chan);
380 return MOD_RES_PASSTHRU;
382 Membership* memb = chan->GetUser(user);
384 return MOD_RES_PASSTHRU;
386 ModResult res = CheckExemption::Call(exemptionprov, user, chan, "repeat");
387 if (res == MOD_RES_ALLOW)
388 return MOD_RES_PASSTHRU;
390 if (rm.MatchLine(memb, settings, details.text))
392 if (settings->Action == ChannelSettings::ACT_BLOCK)
394 user->WriteNotice("*** This line is too similar to one of your last lines.");
398 if (settings->Action == ChannelSettings::ACT_BAN)
400 Modes::ChangeList changelist;
401 changelist.push_add(ServerInstance->Modes->FindMode('b', MODETYPE_CHANNEL), "*!*@" + user->GetDisplayedHost());
402 ServerInstance->Modes->Process(ServerInstance->FakeClient, chan, NULL, changelist);
405 memb->chan->KickUser(ServerInstance->FakeClient, user, "Repeat flood");
408 return MOD_RES_PASSTHRU;
411 void Prioritize() CXX11_OVERRIDE
413 ServerInstance->Modules->SetPriority(this, I_OnUserPreMessage, PRIORITY_LAST);
416 Version GetVersion() CXX11_OVERRIDE
418 return Version("Provides channel mode +E, blocking of similar messages", VF_COMMON|VF_VENDOR, rm.GetModuleSettings());
422 MODULE_INIT(RepeatModule)