]> git.netwichtig.de Git - user/henk/code/inspircd.git/blob - src/modules/m_repeat.cpp
Replace all abstract usages of his/he/her with they/their/it.
[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 ((!stream.GetToken(item)) || !InspIRCd::Duration(item, settings.Seconds) || (settings.Seconds == 0))
281                         // Required parameter missing
282                         return false;
283
284                 // The diff and backlog parameters are optional
285                 settings.Diff = settings.Backlog = 0;
286                 if (stream.GetToken(item))
287                 {
288                         // There is a diff parameter, see if it's valid (> 0)
289                         if ((settings.Diff = ConvToNum<unsigned int>(item)) == 0)
290                                 return false;
291
292                         if (stream.GetToken(item))
293                         {
294                                 // There is a backlog parameter, see if it's valid
295                                 if ((settings.Backlog = ConvToNum<unsigned int>(item)) == 0)
296                                         return false;
297
298                                 // If there are still tokens, then it's invalid because we allow only 4
299                                 if (stream.GetToken(item))
300                                         return false;
301                         }
302                 }
303
304                 return true;
305         }
306
307         bool ValidateSettings(LocalUser* source, Channel* channel, const std::string& parameter, const ChannelSettings& settings)
308         {
309                 if (ms.MaxLines && settings.Lines > ms.MaxLines)
310                 {
311                         source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, InspIRCd::Format(
312                                 "Invalid repeat parameter. The line number you specified is too great. Maximum allowed is %u.", ms.MaxLines)));
313                         return false;
314                 }
315
316                 if (ms.MaxSecs && settings.Seconds > ms.MaxSecs)
317                 {
318                         source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, InspIRCd::Format(
319                                 "Invalid repeat parameter. The seconds you specified are too great. Maximum allowed is %u.", ms.MaxSecs)));
320                         return false;
321                 }
322
323                 if (settings.Diff && settings.Diff > ms.MaxDiff)
324                 {
325                         if (ms.MaxDiff == 0)
326                                 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter,
327                                         "Invalid repeat parameter. The server administrator has disabled matching on edit distance."));
328                         else
329                                 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, InspIRCd::Format(
330                                         "Invalid repeat parameter. The distance you specified is too great. Maximum allowed is %u.", ms.MaxDiff)));
331                         return false;
332                 }
333
334                 if (settings.Backlog && settings.Backlog > ms.MaxBacklog)
335                 {
336                         if (ms.MaxBacklog == 0)
337                                 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter,
338                                         "Invalid repeat parameter. The server administrator has disabled backlog matching."));
339                         else
340                                 source->WriteNumeric(Numerics::InvalidModeParameter(channel, this, parameter, InspIRCd::Format(
341                                         "Invalid repeat paramter. The backlog you specified is too great. Maximum allowed is %u.", ms.MaxBacklog)));
342                         return false;
343                 }
344
345                 return true;
346         }
347 };
348
349 class RepeatModule : public Module
350 {
351         CheckExemption::EventProvider exemptionprov;
352         RepeatMode rm;
353
354  public:
355         RepeatModule()
356                 : exemptionprov(this)
357                 , rm(this)
358         {
359         }
360
361         void ReadConfig(ConfigStatus& status) CXX11_OVERRIDE
362         {
363                 rm.ReadConfig();
364         }
365
366         ModResult OnUserPreMessage(User* user, const MessageTarget& target, MessageDetails& details) CXX11_OVERRIDE
367         {
368                 if (target.type != MessageTarget::TYPE_CHANNEL || !IS_LOCAL(user))
369                         return MOD_RES_PASSTHRU;
370
371                 Channel* chan = target.Get<Channel>();
372                 ChannelSettings* settings = rm.ext.get(chan);
373                 if (!settings)
374                         return MOD_RES_PASSTHRU;
375
376                 Membership* memb = chan->GetUser(user);
377                 if (!memb)
378                         return MOD_RES_PASSTHRU;
379
380                 ModResult res = CheckExemption::Call(exemptionprov, user, chan, "repeat");
381                 if (res == MOD_RES_ALLOW)
382                         return MOD_RES_PASSTHRU;
383
384                 if (rm.MatchLine(memb, settings, details.text))
385                 {
386                         if (settings->Action == ChannelSettings::ACT_BLOCK)
387                         {
388                                 user->WriteNotice("*** This line is too similar to one of your last lines.");
389                                 return MOD_RES_DENY;
390                         }
391
392                         if (settings->Action == ChannelSettings::ACT_BAN)
393                         {
394                                 Modes::ChangeList changelist;
395                                 changelist.push_add(ServerInstance->Modes->FindMode('b', MODETYPE_CHANNEL), "*!*@" + user->GetDisplayedHost());
396                                 ServerInstance->Modes->Process(ServerInstance->FakeClient, chan, NULL, changelist);
397                         }
398
399                         memb->chan->KickUser(ServerInstance->FakeClient, user, "Repeat flood");
400                         return MOD_RES_DENY;
401                 }
402                 return MOD_RES_PASSTHRU;
403         }
404
405         void Prioritize() CXX11_OVERRIDE
406         {
407                 ServerInstance->Modules->SetPriority(this, I_OnUserPreMessage, PRIORITY_LAST);
408         }
409
410         Version GetVersion() CXX11_OVERRIDE
411         {
412                 return Version("Provides channel mode +E, blocking of similar messages", VF_COMMON|VF_VENDOR, rm.GetModuleSettings());
413         }
414 };
415
416 MODULE_INIT(RepeatModule)