RelayNext: yet another crappy antiflood mechanism

Also, catch errors in outFilter so they don't block all of the bot's output.
This commit is contained in:
GLolol 2015-01-15 17:31:38 -08:00
parent d0ff6c9a16
commit f62c6eef73
2 changed files with 92 additions and 34 deletions

View File

@ -61,6 +61,20 @@ conf.registerChannelValue(RelayNext, 'noHighlight',
registry.Boolean(False, _("""Determines whether the bot should prefix nicks registry.Boolean(False, _("""Determines whether the bot should prefix nicks
with a hyphen (-) to prevent excess highlights (in PRIVMSGs and actions)."""))) with a hyphen (-) to prevent excess highlights (in PRIVMSGs and actions).""")))
conf.registerGroup(RelayNext, 'antiflood')
conf.registerChannelValue(RelayNext.antiflood, 'enable',
registry.Boolean(False, _("""Determines whether flood prevention should be enabled
for the given channel.""")))
conf.registerChannelValue(RelayNext.antiflood, 'seconds',
registry.PositiveInteger(20, _("""Determines how many seconds messages should be queued
for flood protection.""")))
conf.registerChannelValue(RelayNext.antiflood, 'maximum',
registry.PositiveInteger(15, _("""Determines the maximum amount of incoming messages
the bot will allow from a relay channel before flood protection is triggered.""")))
conf.registerChannelValue(RelayNext.antiflood, 'timeout',
registry.PositiveInteger(60, _("""Determines the amount of time in seconds the bot should
block messages if flood protection is triggered.""")))
conf.registerGroup(RelayNext, 'events') conf.registerGroup(RelayNext, 'events')
_events = ('quit', 'join', 'part', 'nick', 'mode', 'kick') _events = ('quit', 'join', 'part', 'nick', 'mode', 'kick')

View File

@ -31,6 +31,7 @@
from copy import deepcopy from copy import deepcopy
import pickle import pickle
import re import re
import traceback
import supybot.world as world import supybot.world as world
import supybot.irclib as irclib import supybot.irclib as irclib
@ -41,6 +42,7 @@ from supybot.commands import *
import supybot.plugins as plugins import supybot.plugins as plugins
import supybot.ircutils as ircutils import supybot.ircutils as ircutils
import supybot.callbacks as callbacks import supybot.callbacks as callbacks
from supybot.utils.structures import TimeoutQueue
try: try:
from supybot.i18n import PluginInternationalization from supybot.i18n import PluginInternationalization
_ = PluginInternationalization('RelayNext') _ = PluginInternationalization('RelayNext')
@ -91,6 +93,10 @@ class RelayNext(callbacks.Plugin):
self.ircstates = {} self.ircstates = {}
self.lastmsg = {} self.lastmsg = {}
# This part facilitates flood protection
self.msgcounters = {}
self.floodTriggered = False
self.db = {} self.db = {}
self.loadDB() self.loadDB()
world.flushers.append(self.exportDB) world.flushers.append(self.exportDB)
@ -144,7 +150,7 @@ class RelayNext(callbacks.Plugin):
results.append(cn[0]) results.append(cn[0])
return results return results
def _format(self, irc, msg): def _format(self, irc, msg, announcement=False):
s = '' s = ''
nick = msg.nick nick = msg.nick
userhost = '' userhost = ''
@ -166,39 +172,48 @@ class RelayNext(callbacks.Plugin):
userhost = ' (%s)' % msg.prefix.split('!', 1)[1] userhost = ' (%s)' % msg.prefix.split('!', 1)[1]
except: except:
pass pass
if announcement:
if msg.command == 'NICK': # Announcements use a special syntax
newnick = msg.args[0] s = '*** %s' % announcement
if color: else:
newnick = self.simpleHash(newnick) if msg.command == 'NICK':
s = '- %s is now known as %s' % (nick, newnick) newnick = msg.args[0]
elif msg.command == 'PRIVMSG': if color:
text = msg.args[1] newnick = self.simpleHash(newnick)
if re.match('^\x01ACTION .*\x01$', text): s = '- %s is now known as %s' % (nick, newnick)
text = text[8:-1] elif msg.command == 'PRIVMSG':
s = '* %s %s' % (nick, text) text = msg.args[1]
else: if re.match('^\x01ACTION .*\x01$', text):
s = '<%s> %s' % (nick, msg.args[1]) text = text[8:-1]
elif msg.command == 'JOIN': s = '* %s %s' % (nick, text)
s = '- %s%s has joined %s' % (nick, userhost, channel) else:
elif msg.command == 'PART': s = '<%s> %s' % (nick, msg.args[1])
# Part message isn't a required field and can be empty sometimes elif msg.command == 'JOIN':
try: s = '- %s%s has joined %s' % (nick, userhost, channel)
partmsg = ' (%s)' % msg.args[1] elif msg.command == 'PART':
except: # Part message isn't a required field and can be empty sometimes
partmsg = '' try:
s = '- %s%s has left %s%s' % (nick, userhost, channel, partmsg) partmsg = ' (%s)' % msg.args[1]
elif msg.command == 'QUIT': except:
s = '- %s has quit (%s)' % (nick, msg.args[0]) partmsg = ''
elif msg.command == 'MODE': s = '- %s%s has left %s%s' % (nick, userhost, channel, partmsg)
modes = ' '.join(msg.args[1:]) elif msg.command == 'QUIT':
s = '- %s%s set mode %s on %s' % (nick, userhost, modes, channel) s = '- %s has quit (%s)' % (nick, msg.args[0])
elif msg.command == 'MODE':
modes = ' '.join(msg.args[1:])
s = '- %s%s set mode %s on %s' % (nick, userhost, modes, channel)
if s: # Add the network name and some final touch-ups if s: # Add the network name and some final touch-ups
s = "\x02[%s]\x02 %s" % (netname, s) s = "\x02[%s]\x02 %s" % (netname, s)
s = s.replace("- -", "-", 1) s = s.replace("- -", "-", 1)
return s return s
def checkFlood(self, channel, source, command):
maximum = self.registryValue("antiflood.maximum", channel)
enabled = self.registryValue("antiflood.enable", channel)
if enabled and len(self.msgcounters[(source, command)]) > maximum:
return True
def relay(self, irc, msg, channel=None): def relay(self, irc, msg, channel=None):
channel = channel or msg.args[0] channel = channel or msg.args[0]
# Get the source channel # Get the source channel
@ -206,6 +221,29 @@ class RelayNext(callbacks.Plugin):
source = source.lower() source = source.lower()
out_s = self._format(irc, msg) out_s = self._format(irc, msg)
if out_s: if out_s:
### Begin Flood checking clause
timeout = self.registryValue("antiflood.timeout", channel)
seconds = self.registryValue("antiflood.seconds", channel)
maximum = self.registryValue("antiflood.maximum", channel)
try:
self.msgcounters[(source, msg.command)].enqueue(msg.prefix)
except KeyError:
self.msgcounters[(source, msg.command)] = TimeoutQueue(seconds)
if self.checkFlood(channel, source, msg.command):
self.log.debug("RelayNext (%s): message from %s blocked by "
"flood preotection.", irc.network, channel)
if self.floodTriggered:
return
c = msg.command.lower()
e = format("Flood detected on %s (%s %ss/%s seconds), "
"not relaying %ss for %s seconds!", channel,
maximum, c, seconds, c, timeout)
out_s = self._format(irc, msg, announcement=e)
self.log.info("RelayNext (%s): %s", irc.network, e)
self.floodTriggered = True
else:
self.floodTriggered = False
### End Flood checking clause
for relay in self.db.values(): for relay in self.db.values():
if source in relay: # If our channel is in a relay if source in relay: # If our channel is in a relay
# Remove ourselves so we don't get duplicated messages # Remove ourselves so we don't get duplicated messages
@ -255,12 +293,18 @@ class RelayNext(callbacks.Plugin):
def outFilter(self, irc, msg): def outFilter(self, irc, msg):
# Catch our own messages and send them into the relay (this is # Catch our own messages and send them into the relay (this is
# useful because Supybot is often a multi-purpose bot!) # useful because Supybot is often a multi-purpose bot!)
if msg.command == 'PRIVMSG' and not msg.relayedMsg: try:
if msg.args[0] in self._getAllRelaysForNetwork(irc): if msg.command == 'PRIVMSG' and not msg.relayedMsg:
new_msg = deepcopy(msg) if msg.args[0] in self._getAllRelaysForNetwork(irc):
new_msg.nick = irc.nick new_msg = deepcopy(msg)
self.relay(irc, new_msg, channel=msg.args[0]) new_msg.nick = irc.nick
return msg self.relay(irc, new_msg, channel=msg.args[0])
except Exception as e:
# We want to log errors, but not block the bot's output
traceback.print_exc()
log.error(str(e))
finally:
return msg
### User commands ### User commands