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