From 703ed5d35aab199c63e6a7425907a70f7f02117e Mon Sep 17 00:00:00 2001 From: GLolol Date: Thu, 3 Jul 2014 14:09:10 -0700 Subject: [PATCH] RelayLink: add experimental flood protection as per #3 (still needs to be tested!) --- RelayLink/config.py | 295 ++++++++++++++++++++++++-------------------- RelayLink/plugin.py | 96 ++++++++++++-- 2 files changed, 248 insertions(+), 143 deletions(-) diff --git a/RelayLink/config.py b/RelayLink/config.py index 6a4f77b..2440fc8 100644 --- a/RelayLink/config.py +++ b/RelayLink/config.py @@ -1,133 +1,162 @@ -### -# Copyright (c) 2010, quantumlemur -# Copyright (c) 2013-2014, James Lu (GLolol) -# All rights reserved. -# -# Redistribution and use in source and binary forms, with or without -# modification, are permitted provided that the following conditions are met: -# -# * Redistributions of source code must retain the above copyright notice, -# this list of conditions, and the following disclaimer. -# * Redistributions in binary form must reproduce the above copyright notice, -# this list of conditions, and the following disclaimer in the -# documentation and/or other materials provided with the distribution. -# * Neither the name of the author of this software nor the name of -# contributors to this software may be used to endorse or promote products -# derived from this software without specific prior written consent. -# -# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" -# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE -# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE -# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE -# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR -# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF -# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS -# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN -# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) -# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE -# POSSIBILITY OF SUCH DAMAGE. -### - -import supybot.conf as conf -import supybot.ircutils as ircutils -import supybot.registry as registry -try: - from supybot.i18n import PluginInternationalization - from supybot.i18n import internationalizeDocstring - _ = PluginInternationalization('RelayLink') -except: - # This are useless functions that's allow to run the plugin on a bot - # without the i18n plugin - _ = lambda x:x - internationalizeDocstring = lambda x:x - -def configure(advanced): - from supybot.questions import output, expect, anything, something, yn - conf.registerPlugin('RelayLink', True) - - -class ColorNumber(registry.String): - """Value must be a valid color number (01, 02, 03, 04, ..., 16)""" - def set(self, s): - if s not in ('01', '02', '03', '04', '05', '06', '07', '08', '09', - '10', '11', '12', '13', '14', '15', '16'): - self.error() - return - self.setValue(s) -ColorNumber = internationalizeDocstring(ColorNumber) - - -RelayLink = conf.registerPlugin('RelayLink') -conf.registerChannelValue(RelayLink, 'color', - registry.Boolean(True, _("""Determines whether the bot will color relayed - PRIVMSGs so as to make the messages easier to read."""))) -conf.registerChannelValue(RelayLink, 'hostmasks', - registry.Boolean(True, _("""Determines whether the bot will relay the - hostmask of the person joining or parting the channel when he or she joins - or parts."""))) -conf.registerChannelValue(RelayLink, 'noHighlight', - registry.Boolean(False, _("""Determines whether the bot should prefix nicks - with a hyphen (-) to prevent excess highlights (in PRIVMSGs and actions)."""))) -conf.registerChannelValue(RelayLink, 'nicks', - registry.Boolean(True, _("""Determines whether the bot will relay the - nick of the person sending a message (you probably want this turned on)."""))) -conf.registerChannelValue(RelayLink, 'includeNetwork', - registry.Boolean(True, _("""Determines whether the bot will include the - network in relayed PRIVMSGs; if you're only relaying between two networks, - it's somewhat redundant, and you may wish to save the space."""))) - -conf.registerGroup(RelayLink, 'ignore') -conf.registerChannelValue(RelayLink.ignore, 'nicks', - registry.SpaceSeparatedListOfStrings('', _("""Determines a list of nicks for the bot to - ignore (takes a space-seperated list)."""))) -conf.registerChannelValue(RelayLink.ignore, 'affectPrivmsgs', - registry.Boolean(True, _("""Determines whether the bot will ignore PRIVMSGs - from the nicks listed in ignore. If set to False, the bot will only - ignore joins/parts/nicks/modes/quits (not kicks) from those nicks."""))) - -# conf.registerGroup(RelayLink, 'sepTags') -# conf.registerChannelValue(RelayLink.sepTags, 'channels', - # registry.String('@', _("""Determines the separator string used for the - # bot for channels (when both nicks and IncludeNetwork are on)."""))) -# conf.registerChannelValue(RelayLink.sepTags, 'nicks', - # registry.String('/', _("""Determines the separator string used for the - # bot for nicks (when both nicks and IncludeNetwork are on)."""))) - -class ValidNonPrivmsgsHandling(registry.OnlySomeStrings): - validStrings = ('privmsg', 'notice', 'nothing') -conf.registerChannelValue(RelayLink, 'nonPrivmsgs', - ValidNonPrivmsgsHandling('privmsg', _("""Determines whether the - bot will use PRIVMSGs (privmsg), NOTICEs (notice), for non-PRIVMSG Relay - messages (i.e., joins, parts, nicks, quits, modes, etc.), or whether it - won't relay such messages (nothing)"""))) - -conf.registerGlobalValue(RelayLink, 'relays', - registry.String('', _("""You shouldn't edit this configuration variable - yourself unless you know what you do. Use 'relaylink {add|remove}' instead."""))) - -conf.registerGlobalValue(RelayLink, 'substitutes', - registry.String('', _("""You shouldn't edit this configuration variable - yourself unless you know what you do. Use 'relaylink (no)substitute' instead."""))) - -conf.registerGlobalValue(RelayLink, 'logFailedChanges', - registry.Boolean(False, _("""Determines whether the bot should log failed config changes."""))) - -# conf.registerGroup(RelayLink, 'colors') -# for name, color in {'info': '02', - # 'truncated': '14', - # 'mode': '06', - # 'join': '03', - # 'part': '12', - # 'kick': '04', - # 'nick': '10', - # 'quit': '07'}.items(): - # conf.registerChannelValue(RelayLink.colors, name, - # ColorNumber(color, _("""Color used for relaying %s messages.""") % name)) - -conf.registerGroup(RelayLink, 'addall') -conf.registerGlobalValue(RelayLink.addall, 'max', - registry.NonNegativeInteger(20, _("""Defines the maximum number of channels addall/removeall - will try to process at once. Setting this below 1 will effectively disable the command. - A value too high can freeze the bot, so be careful!"""))) - -# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: +### +# Copyright (c) 2010, quantumlemur +# Copyright (c) 2013-2014, James Lu (GLolol) +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions, and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions, and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# * Neither the name of the author of this software nor the name of +# contributors to this software may be used to endorse or promote products +# derived from this software without specific prior written consent. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. +### + +import supybot.conf as conf +import supybot.ircutils as ircutils +import supybot.registry as registry +try: + from supybot.i18n import PluginInternationalization + from supybot.i18n import internationalizeDocstring + _ = PluginInternationalization('RelayLink') +except: + # This are useless functions that's allow to run the plugin on a bot + # without the i18n plugin + _ = lambda x:x + internationalizeDocstring = lambda x:x + +def configure(advanced): + from supybot.questions import output, expect, anything, something, yn + conf.registerPlugin('RelayLink', True) + + +class ColorNumber(registry.String): + """Value must be a valid color number (01, 02, 03, 04, ..., 16)""" + def set(self, s): + if s not in ('01', '02', '03', '04', '05', '06', '07', '08', '09', + '10', '11', '12', '13', '14', '15', '16'): + self.error() + return + self.setValue(s) +ColorNumber = internationalizeDocstring(ColorNumber) + + +RelayLink = conf.registerPlugin('RelayLink') +conf.registerChannelValue(RelayLink, 'color', + registry.Boolean(True, _("""Determines whether the bot will color relayed + PRIVMSGs so as to make the messages easier to read."""))) +conf.registerChannelValue(RelayLink, 'hostmasks', + registry.Boolean(True, _("""Determines whether the bot will relay the + hostmask of the person joining or parting the channel when he or she joins + or parts."""))) +conf.registerChannelValue(RelayLink, 'noHighlight', + registry.Boolean(False, _("""Determines whether the bot should prefix nicks + with a hyphen (-) to prevent excess highlights (in PRIVMSGs and actions)."""))) +conf.registerChannelValue(RelayLink, 'nicks', + registry.Boolean(True, _("""Determines whether the bot will relay the + nick of the person sending a message (you probably want this turned on)."""))) +conf.registerChannelValue(RelayLink, 'includeNetwork', + registry.Boolean(True, _("""Determines whether the bot will include the + network in relayed PRIVMSGs; if you're only relaying between two networks, + it's somewhat redundant, and you may wish to save the space."""))) + +conf.registerGroup(RelayLink, 'ignore') +conf.registerChannelValue(RelayLink.ignore, 'nicks', + registry.SpaceSeparatedListOfStrings('', _("""Determines a list of nicks for the bot to + ignore (takes a space-seperated list)."""))) +conf.registerChannelValue(RelayLink.ignore, 'affectPrivmsgs', + registry.Boolean(True, _("""Determines whether the bot will ignore PRIVMSGs + from the nicks listed in ignore. If set to False, the bot will only + ignore joins/parts/nicks/modes/quits (not kicks) from those nicks."""))) + +# class FloodPreventionConfigHandler(registry.String): + # """Invalid input. This value should be given in the form 'positiveInt:positiveInt' + # (amount:seconds)""" + # def setValue(self, v): + # try: + # i = [int(n) for n in v.split(":")] + # except ValueError: + # self.error() + # return + # if len(i) < 2 or i[0] < 0 or i[1] < 0: + # self.error() + # return + # else: + # registry.String.setValue(self, v) + +conf.registerGroup(RelayLink, 'antiflood') +conf.registerGlobalValue(RelayLink.antiflood, 'enable', + registry.Boolean(False, _("""Determines whether flood protection should + be used by the relayer."""))) +conf.registerGlobalValue(RelayLink.antiflood, 'privmsgs', + registry.NonNegativeInteger(0, _("""Determines how many PRIVMSGs the bot will allow + before flood protection is triggered. This setting should be set based on how much + traffic a channel gets, so a default is not included. Setting this' to 0 + effectively disables flood prevention."""))) +conf.registerGlobalValue(RelayLink.antiflood, 'nonPrivmsgs', + registry.NonNegativeInteger(0, _("""Determines how many non-PRIVMSG + events (joins, parts, nicks, etc.) the bot will allow before flood + protection is triggered. This setting should be set based on how much + traffic a channel gets, so a default is not included. Setting this to + 0 effectively disables flood prevention."""))) +conf.registerGlobalValue(RelayLink.antiflood, 'seconds', + registry.PositiveInteger(30, _("""Determines how many seconds the bot + should wait before relaying if flood prevention is triggered."""))) +conf.registerGlobalValue(RelayLink.antiflood, 'announce', + registry.Boolean(True, _("""Determines whether the bot should announce + flood alerts to the channel."""))) + +class ValidNonPrivmsgsHandling(registry.OnlySomeStrings): + validStrings = ('privmsg', 'notice', 'nothing') +conf.registerChannelValue(RelayLink, 'nonPrivmsgs', + ValidNonPrivmsgsHandling('privmsg', _("""Determines whether the + bot will use PRIVMSGs (privmsg), NOTICEs (notice), for non-PRIVMSG Relay + messages (i.e., joins, parts, nicks, quits, modes, etc.), or whether it + won't relay such messages (nothing)"""))) + +conf.registerGlobalValue(RelayLink, 'relays', + registry.String('', _("""You shouldn't edit this configuration variable + yourself unless you know what you do. Use 'relaylink {add|remove}' instead."""))) + +conf.registerGlobalValue(RelayLink, 'substitutes', + registry.String('', _("""You shouldn't edit this configuration variable + yourself unless you know what you do. Use 'relaylink (no)substitute' instead."""))) + +conf.registerGlobalValue(RelayLink, 'logFailedChanges', + registry.Boolean(False, _("""Determines whether the bot should log failed config changes."""))) + +# conf.registerGroup(RelayLink, 'colors') +# for name, color in {'info': '02', + # 'truncated': '14', + # 'mode': '06', + # 'join': '03', + # 'part': '12', + # 'kick': '04', + # 'nick': '10', + # 'quit': '07'}.items(): + # conf.registerChannelValue(RelayLink.colors, name, + # ColorNumber(color, _("""Color used for relaying %s messages.""") % name)) + +conf.registerGroup(RelayLink, 'addall') +conf.registerGlobalValue(RelayLink.addall, 'max', + registry.NonNegativeInteger(20, _("""Defines the maximum number of channels addall/removeall + will try to process at once. Setting this below 1 will effectively disable the command. + A value too high can freeze the bot, so be careful!"""))) + +# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: diff --git a/RelayLink/plugin.py b/RelayLink/plugin.py index e59693a..092b9a6 100755 --- a/RelayLink/plugin.py +++ b/RelayLink/plugin.py @@ -44,6 +44,7 @@ import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.registry as registry import supybot.callbacks as callbacks +from supybot.utils.structures import TimeoutQueue try: from supybot.i18n import PluginInternationalization from supybot.i18n import internationalizeDocstring @@ -79,6 +80,11 @@ class RelayLink(callbacks.Plugin): self.ircstates = {} for IRC in world.ircs: self.addIRC(IRC) + floodProtectTimeout = conf.supybot.plugins.RelayLink.antiflood.seconds + self.nonPrivmsgCounter = TimeoutQueue(floodProtectTimeout) + self.privmsgCounter = TimeoutQueue(floodProtectTimeout) + self.floodActivated = False + # self.nonPrivmsgsCounter = self.privmsgsCounter = 0 try: conf.supybot.plugins.RelayLink.substitutes.addCallback( self._loadFromConfig) @@ -127,7 +133,31 @@ class RelayLink(callbacks.Plugin): num = num % 11 return colors[num] + def floodDetect(self): + if self.registryValue("antiflood.announce") and not self.floodActivated: + msgs = self.registryValue("antiflood.nonPrivmsgs") + secs = self.registryValue("antiflood.seconds") + s = ("%(network)s*** Flood detected ({msgs} non-PRIVMSG messages in {secs} seconds). Not relaying messages" + " for {secs} seconds!".format(secs=secs, msgs=msgs)) + self.floodActivated = True + return s + else: + return + def getPrivmsgData(self, channel, nick, text, colored): + if self.registryValue("antiflood.enable") and \ + self.registryValue("antiflood.privmsgs") > 0 and \ + (len(self.privmsgCounter) > self.registryValue("antiflood.privmsgs")): + if self.registryValue("antiflood.announce") and not self.floodActivated: + msgs = self.registryValue("antiflood.privmsgs") + secs = self.registryValue("antiflood.seconds") + s = ("%(network)s*** Flood detected ({msgs} messages in {secs} seconds). Not relaying messages" + " for {secs} seconds!".format(secs=secs, msgs=msgs)), {} + self.floodActivated = True + return s + else: + return + self.floodActivated = False color = self.simpleHash(nick) nickprefix = '' if nick in self.nickSubstitutions: @@ -184,6 +214,7 @@ class RelayLink(callbacks.Plugin): def doPrivmsg(self, irc, msg): self.addIRC(irc) + self.privmsgCounter.enqueue([0]) channel = msg.args[0] s = msg.args[1] s, args = self.getPrivmsgData(channel, msg.nick, s, @@ -191,7 +222,7 @@ class RelayLink(callbacks.Plugin): ignoreNicks = [ircutils.toLower(item) for item in \ self.registryValue('ignore.nicks', msg.args[0])] if self.registryValue('ignore.affectPrivmsgs', msg.args[0]) \ - == 1 and ircutils.toLower(msg.nick) in ignoreNicks: + and ircutils.toLower(msg.nick) in ignoreNicks: return elif channel not in irc.state.channels: # in private # cuts off the end of commands, so that passwords @@ -222,10 +253,19 @@ class RelayLink(callbacks.Plugin): def doMode(self, irc, msg): ignoreNicks = [ircutils.toLower(item) for item in \ self.registryValue('ignore.nicks', msg.args[0])] - if ircutils.toLower(msg.nick) not in ignoreNicks: - self.addIRC(irc) - args = {'nick': msg.nick, 'channel': msg.args[0], - 'mode': ' '.join(msg.args[1:]), 'userhost': ''} + self.addIRC(irc) + self.nonPrivmsgCounter.enqueue([0]) + args = {'nick': msg.nick, 'channel': msg.args[0], + 'mode': ' '.join(msg.args[1:]), 'userhost': ''} + if self.registryValue("antiflood.enable") and \ + self.registryValue("antiflood.nonprivmsgs") > 0 and \ + (len(self.nonPrivmsgCounter) > self.registryValue("antiflood.nonprivmsgs")): + s = self.floodDetect() + if s: + self.sendToOthers(irc, msg.args[0], s, args) + else: return + elif ircutils.toLower(msg.nick) not in ignoreNicks: + self.floodActivated = False if self.registryValue('color', msg.args[0]): # args['color'] = '\x03%s' % self.registryValue('colors.mode', msg.args[0]) args['nick'] = '\x03%s%s\x03' % (self.simpleHash(msg.nick), msg.nick) @@ -245,7 +285,16 @@ class RelayLink(callbacks.Plugin): s = '%(network)s\x0309*** Relay joined to %(channel)s' else: s = '%(network)s*** Relay joined to %(channel)s' + self.nonPrivmsgCounter.enqueue([0]) + if self.registryValue("antiflood.enable") and \ + self.registryValue("antiflood.nonprivmsgs") > 0 and \ + (len(self.nonPrivmsgCounter) > self.registryValue("antiflood.nonprivmsgs")): + s = self.floodDetect() + if s: + self.sendToOthers(irc, msg.args[0], s, args) + else: return elif ircutils.toLower(msg.nick) not in ignoreNicks: + self.floodActivated = False if self.registryValue('color', msg.args[0]): args['nick'] = '\x03%s%s\x03' % (self.simpleHash(msg.nick), msg.nick) if self.registryValue('hostmasks', msg.args[0]): @@ -258,10 +307,19 @@ class RelayLink(callbacks.Plugin): def doPart(self, irc, msg): ignoreNicks = [ircutils.toLower(item) for item in \ self.registryValue('ignore.nicks', msg.args[0])] - if ircutils.toLower(msg.nick) not in ignoreNicks: + self.nonPrivmsgCounter.enqueue([0]) + args = {'nick': msg.nick, 'channel': msg.args[0], 'message': '', + 'userhost': ''} + if self.registryValue("antiflood.enable") and \ + self.registryValue("antiflood.nonprivmsgs") > 0 and \ + (len(self.nonPrivmsgCounter) > self.registryValue("antiflood.nonprivmsgs")): + s = self.floodDetect() + if s: + self.sendToOthers(irc, msg.args[0], s, args) + else: return + elif ircutils.toLower(msg.nick) not in ignoreNicks: self.addIRC(irc) - args = {'nick': msg.nick, 'channel': msg.args[0], 'message': '', - 'userhost': ''} + self.floodActivated = False if self.registryValue('color', msg.args[0]): args['nick'] = '\x03%s%s\x03' % (self.simpleHash(msg.nick), msg.nick) if self.registryValue('hostmasks', msg.args[0]): @@ -277,6 +335,15 @@ class RelayLink(callbacks.Plugin): self.addIRC(irc) args = {'kicked': msg.args[1], 'channel': msg.args[0], 'kicker': msg.nick, 'message': msg.args[2], 'userhost': ''} + self.nonPrivmsgCounter.enqueue([0]) + if self.registryValue("antiflood.enable") and \ + self.registryValue("antiflood.nonprivmsgs") > 0 and \ + (len(self.nonPrivmsgCounter) > self.registryValue("antiflood.nonprivmsgs")): + s = self.floodDetect() + if s: + self.sendToOthers(irc, msg.args[0], s, args) + else: return + self.floodActivated = False if self.registryValue('color', msg.args[0]): args['kicked'] = '\x03%s%s\x03' % (self.simpleHash(msg.args[1]), msg.args[1]) if self.registryValue('hostmasks', msg.args[0]): @@ -291,9 +358,18 @@ class RelayLink(callbacks.Plugin): def doNick(self, irc, msg): ignoreNicks = [ircutils.toLower(item) for item in \ self.registryValue('ignore.nicks')] + self.addIRC(irc) + args = {'oldnick': msg.nick, 'newnick': msg.args[0]} + self.nonPrivmsgCounter.enqueue([0]) + if self.registryValue("antiflood.enable") and \ + self.registryValue("antiflood.nonprivmsgs") > 0 and \ + (len(self.nonPrivmsgCounter) > self.registryValue("antiflood.nonprivmsgs")): + s = self.floodDetect() + if s: + self.sendToOthers(irc, msg.args[0], s, args) + else: return + self.floodActivated = False if ircutils.toLower(msg.nick) not in ignoreNicks: - self.addIRC(irc) - args = {'oldnick': msg.nick, 'newnick': msg.args[0]} if self.registryValue('color'): args['oldnick'] = '\x03%s%s\x03' % (self.simpleHash(msg.nick), msg.nick) args['newnick'] = '\x03%s%s\x03' % (self.simpleHash(msg.args[0]), msg.args[0])