]> git.netwichtig.de Git - user/henk/code/inspircd.git/blob - src/modules/m_repeat.cpp
Implemented configurable kick message for the repeat module (#1835).
[user/henk/code/inspircd.git] / src / modules / m_repeat.cpp
1 /*
2  * InspIRCd -- Internet Relay Chat Daemon
3  *
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>
11  *
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.
15  *
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
19  * details.
20  *
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/>.
23  */
24
25
26 #include "inspircd.h"
27 #include "modules/exemption.h"
28
29 class ChannelSettings
30 {
31  public:
32         enum RepeatAction
33         {
34                 ACT_KICK,
35                 ACT_BLOCK,
36                 ACT_BAN
37         };
38
39         RepeatAction Action;
40         unsigned int Backlog;
41         unsigned int Lines;
42         unsigned int Diff;
43         unsigned long Seconds;
44
45         void serialize(std::string& out) const
46         {
47                 if (Action == ACT_BAN)
48                         out.push_back('*');
49                 else if (Action == ACT_BLOCK)
50                         out.push_back('~');
51
52                 out.append(ConvToStr(Lines)).push_back(':');
53                 out.append(ConvToStr(Seconds));
54                 if (Diff)
55                 {
56                         out.push_back(':');
57                         out.append(ConvToStr(Diff));
58                         if (Backlog)
59                         {
60                                 out.push_back(':');
61                                 out.append(ConvToStr(Backlog));
62                         }
63                 }
64         }
65 };
66
67 class RepeatMode : public ParamMode<RepeatMode, SimpleExtItem<ChannelSettings> >
68 {
69  private:
70         struct RepeatItem
71         {
72                 time_t ts;
73                 std::string line;
74                 RepeatItem(time_t TS, const std::string& Line) : ts(TS), line(Line) { }
75         };
76
77         typedef std::deque<RepeatItem> RepeatItemList;
78
79         struct MemberInfo
80         {
81                 RepeatItemList ItemList;
82                 unsigned int Counter;
83                 MemberInfo() : Counter(0) {}
84         };
85
86         struct ModuleSettings
87         {
88                 unsigned int MaxLines;
89                 unsigned int MaxSecs;
90                 unsigned int MaxBacklog;
91                 unsigned int MaxDiff;
92                 unsigned int MaxMessageSize;
93                 std::string KickMessage;
94                 ModuleSettings() : MaxLines(0), MaxSecs(0), MaxBacklog(0), MaxDiff() { }
95         };
96
97         std::vector<unsigned int> mx[2];
98         ModuleSettings ms;
99
100         bool CompareLines(const std::string& message, const std::string& historyline, unsigned int trigger)
101         {
102                 if (message == historyline)
103                         return true;
104                 else if (trigger)
105                         return (Levenshtein(message, historyline) <= trigger);
106
107                 return false;
108         }
109
110         unsigned int Levenshtein(const std::string& s1, const std::string& s2)
111         {
112                 unsigned int l1 = s1.size();
113                 unsigned int l2 = s2.size();
114
115                 for (unsigned int i = 0; i < l2; i++)
116                         mx[0][i] = i;
117                 for (unsigned int i = 0; i < l1; i++)
118                 {
119                         mx[1][0] = i + 1;
120                         for (unsigned int j = 0; j < l2; j++)
121                                 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
123                         mx[0].swap(mx[1]);
124                 }
125                 return mx[0][l2];
126         }
127
128  public:
129         SimpleExtItem<MemberInfo> MemberInfoExt;
130
131         RepeatMode(Module* Creator)
132                 : ParamMode<RepeatMode, SimpleExtItem<ChannelSettings> >(Creator, "repeat", 'E')
133                 , MemberInfoExt("repeat_memb", ExtensionItem::EXT_MEMBERSHIP, Creator)
134         {
135                 syntax = "[~|*]<lines>:<sec>[:<difference>][:<backlog>]";
136         }
137
138         void OnUnset(User* source, Channel* chan) CXX11_OVERRIDE
139         {
140                 // Unset the per-membership extension when the mode is removed
141                 const Channel::MemberMap& users = chan->GetUsers();
142                 for (Channel::MemberMap::const_iterator i = users.begin(); i != users.end(); ++i)
143                         MemberInfoExt.unset(i->second);
144         }
145
146         ModeAction OnSet(User* source, Channel* channel, std::string& parameter) CXX11_OVERRIDE
147         {
148                 ChannelSettings settings;
149                 if (!ParseSettings(source, parameter, settings))
150                 {
151                         source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter));
152                         return MODEACTION_DENY;
153                 }
154
155                 if ((settings.Backlog > 0) && (settings.Lines > settings.Backlog))
156                 {
157                         source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter,
158                                 "You can't set lines higher than backlog."));
159                         return MODEACTION_DENY;
160                 }
161
162                 LocalUser* localsource = IS_LOCAL(source);
163                 if ((localsource) && (!ValidateSettings(localsource, channel, parameter, settings)))
164                         return MODEACTION_DENY;
165
166                 ext.set(channel, settings);
167
168                 return MODEACTION_ALLOW;
169         }
170
171         bool MatchLine(Membership* memb, ChannelSettings* rs, std::string message)
172         {
173                 // If the message is larger than whatever size it's set to,
174                 // let's pretend it isn't. If the first 512 (def. setting) match, it's probably spam.
175                 if (message.size() > ms.MaxMessageSize)
176                         message.erase(ms.MaxMessageSize);
177
178                 MemberInfo* rp = MemberInfoExt.get(memb);
179                 if (!rp)
180                 {
181                         rp = new MemberInfo;
182                         MemberInfoExt.set(memb, rp);
183                 }
184
185                 unsigned int matches = 0;
186                 if (!rs->Backlog)
187                         matches = rp->Counter;
188
189                 RepeatItemList& items = rp->ItemList;
190                 const unsigned int trigger = (message.size() * rs->Diff / 100);
191                 const time_t now = ServerInstance->Time();
192
193                 std::transform(message.begin(), message.end(), message.begin(), ::tolower);
194
195                 for (std::deque<RepeatItem>::iterator it = items.begin(); it != items.end(); ++it)
196                 {
197                         if (it->ts < now)
198                         {
199                                 items.erase(it, items.end());
200                                 matches = 0;
201                                 break;
202                         }
203
204                         if (CompareLines(message, it->line, trigger))
205                         {
206                                 if (++matches >= rs->Lines)
207                                 {
208                                         if (rs->Action != ChannelSettings::ACT_BLOCK)
209                                                 rp->Counter = 0;
210                                         return true;
211                                 }
212                         }
213                         else if ((ms.MaxBacklog == 0) || (rs->Backlog == 0))
214                         {
215                                 matches = 0;
216                                 items.clear();
217                                 break;
218                         }
219                 }
220
221                 unsigned int max_items = (rs->Backlog ? rs->Backlog : 1);
222                 if (items.size() >= max_items)
223                         items.pop_back();
224
225                 items.push_front(RepeatItem(now + rs->Seconds, message));
226                 rp->Counter = matches;
227                 return false;
228         }
229
230         void Resize(size_t size)
231         {
232                 size_t newsize = size+1;
233                 if (newsize <= mx[0].size())
234                         return;
235                 ms.MaxMessageSize = size;
236                 mx[0].resize(newsize);
237                 mx[1].resize(newsize);
238         }
239
240         void ReadConfig()
241         {
242                 ConfigTag* conf = ServerInstance->Config->ConfValue("repeat");
243                 ms.MaxLines = conf->getUInt("maxlines", 20);
244                 ms.MaxBacklog = conf->getUInt("maxbacklog", 20);
245                 ms.MaxSecs = conf->getDuration("maxtime", conf->getDuration("maxsecs", 0));
246
247                 ms.MaxDiff = conf->getUInt("maxdistance", 50);
248                 if (ms.MaxDiff > 100)
249                         ms.MaxDiff = 100;
250
251                 unsigned int newsize = conf->getUInt("size", 512);
252                 if (newsize > ServerInstance->Config->Limits.MaxLine)
253                         newsize = ServerInstance->Config->Limits.MaxLine;
254                 Resize(newsize);
255
256                 ms.KickMessage = conf->getString("kickmessage", "Repeat flood");
257         }
258
259         std::string GetModuleSettings() const
260         {
261                 return ConvToStr(ms.MaxLines) + ":" + ConvToStr(ms.MaxSecs) + ":" + ConvToStr(ms.MaxDiff) + ":" + ConvToStr(ms.MaxBacklog);
262         }
263
264         std::string GetKickMessage() const
265         {
266                 return ms.KickMessage;
267         }
268
269         void SerializeParam(Channel* chan, const ChannelSettings* chset, std::string& out)
270         {
271                 chset->serialize(out);
272         }
273
274  private:
275         bool ParseSettings(User* source, std::string& parameter, ChannelSettings& settings)
276         {
277                 irc::sepstream stream(parameter, ':');
278                 std::string     item;
279                 if (!stream.GetToken(item))
280                         // Required parameter missing
281                         return false;
282
283                 if ((item[0] == '*') || (item[0] == '~'))
284                 {
285                         settings.Action = ((item[0] == '*') ? ChannelSettings::ACT_BAN : ChannelSettings::ACT_BLOCK);
286                         item.erase(item.begin());
287                 }
288                 else
289                         settings.Action = ChannelSettings::ACT_KICK;
290
291                 if ((settings.Lines = ConvToNum<unsigned int>(item)) == 0)
292                         return false;
293
294                 if ((!stream.GetToken(item)) || !InspIRCd::Duration(item, settings.Seconds) || (settings.Seconds == 0))
295                         // Required parameter missing
296                         return false;
297
298                 // The diff and backlog parameters are optional
299                 settings.Diff = settings.Backlog = 0;
300                 if (stream.GetToken(item))
301                 {
302                         // There is a diff parameter, see if it's valid (> 0)
303                         if ((settings.Diff = ConvToNum<unsigned int>(item)) == 0)
304                                 return false;
305
306                         if (stream.GetToken(item))
307                         {
308                                 // There is a backlog parameter, see if it's valid
309                                 if ((settings.Backlog = ConvToNum<unsigned int>(item)) == 0)
310                                         return false;
311
312                                 // If there are still tokens, then it's invalid because we allow only 4
313                                 if (stream.GetToken(item))
314                                         return false;
315                         }
316                 }
317
318                 return true;
319         }
320
321         bool ValidateSettings(LocalUser* source, Channel* channel, const std::string& parameter, const ChannelSettings& settings)
322         {
323                 if (ms.MaxLines && settings.Lines > ms.MaxLines)
324                 {
325                         source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, InspIRCd::Format(
326                                 "The line number you specified is too big. Maximum allowed is %u.", ms.MaxLines)));
327                         return false;
328                 }
329
330                 if (ms.MaxSecs && settings.Seconds > ms.MaxSecs)
331                 {
332                         source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, InspIRCd::Format(
333                                 "The seconds you specified are too big. Maximum allowed is %u.", ms.MaxSecs)));
334                         return false;
335                 }
336
337                 if (settings.Diff && settings.Diff > ms.MaxDiff)
338                 {
339                         if (ms.MaxDiff == 0)
340                                 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter,
341                                         "The server administrator has disabled matching on edit distance."));
342                         else
343                                 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, InspIRCd::Format(
344                                         "The distance you specified is too big. Maximum allowed is %u.", ms.MaxDiff)));
345                         return false;
346                 }
347
348                 if (settings.Backlog && settings.Backlog > ms.MaxBacklog)
349                 {
350                         if (ms.MaxBacklog == 0)
351                                 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter,
352                                         "The server administrator has disabled backlog matching."));
353                         else
354                                 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, InspIRCd::Format(
355                                         "The backlog you specified is too big. Maximum allowed is %u.", ms.MaxBacklog)));
356                         return false;
357                 }
358
359                 return true;
360         }
361 };
362
363 class RepeatModule : public Module
364 {
365         CheckExemption::EventProvider exemptionprov;
366         RepeatMode rm;
367
368  public:
369         RepeatModule()
370                 : exemptionprov(this)
371                 , rm(this)
372         {
373         }
374
375         void ReadConfig(ConfigStatus& status) CXX11_OVERRIDE
376         {
377                 rm.ReadConfig();
378         }
379
380         ModResult OnUserPreMessage(User* user, const MessageTarget& target, MessageDetails& details) CXX11_OVERRIDE
381         {
382                 if (target.type != MessageTarget::TYPE_CHANNEL || !IS_LOCAL(user))
383                         return MOD_RES_PASSTHRU;
384
385                 Channel* chan = target.Get<Channel>();
386                 ChannelSettings* settings = rm.ext.get(chan);
387                 if (!settings)
388                         return MOD_RES_PASSTHRU;
389
390                 Membership* memb = chan->GetUser(user);
391                 if (!memb)
392                         return MOD_RES_PASSTHRU;
393
394                 ModResult res = CheckExemption::Call(exemptionprov, user, chan, "repeat");
395                 if (res == MOD_RES_ALLOW)
396                         return MOD_RES_PASSTHRU;
397
398                 if (rm.MatchLine(memb, settings, details.text))
399                 {
400                         if (settings->Action == ChannelSettings::ACT_BLOCK)
401                         {
402                                 user->WriteNotice("*** This line is too similar to one of your last lines.");
403                                 return MOD_RES_DENY;
404                         }
405
406                         if (settings->Action == ChannelSettings::ACT_BAN)
407                         {
408                                 Modes::ChangeList changelist;
409                                 changelist.push_add(ServerInstance->Modes->FindMode('b', MODETYPE_CHANNEL), "*!*@" + user->GetDisplayedHost());
410                                 ServerInstance->Modes->Process(ServerInstance->FakeClient, chan, NULL, changelist);
411                         }
412
413                         memb->chan->KickUser(ServerInstance->FakeClient, user, rm.GetKickMessage());
414                         return MOD_RES_DENY;
415                 }
416                 return MOD_RES_PASSTHRU;
417         }
418
419         void Prioritize() CXX11_OVERRIDE
420         {
421                 ServerInstance->Modules->SetPriority(this, I_OnUserPreMessage, PRIORITY_LAST);
422         }
423
424         Version GetVersion() CXX11_OVERRIDE
425         {
426                 return Version("Adds channel mode E (repeat) which helps protect against spammers which spam the same message repeatedly.", VF_COMMON|VF_VENDOR, rm.GetModuleSettings());
427         }
428 };
429
430 MODULE_INIT(RepeatModule)