From 867e61ea78839e3145f7bec48c3651ed69def5dd Mon Sep 17 00:00:00 2001 From: James Lu Date: Sun, 7 Feb 2016 12:47:08 -0800 Subject: [PATCH] RelayNext: cleanup, switch to using new msg.tagged('channels') - Closes #40. - Drop _getAllRelaysForNetwork() and custom state keeping code, as they aren't needed anymore - Hopefully closes #41? (we may never know) - Also, don't lowercase command names in flood prevention announcements: "PRIVMSGs" looks more correct than "privmsgs" IMO --- RelayNext/plugin.py | 116 +++++++++++++++++++++++++------------------- 1 file changed, 66 insertions(+), 50 deletions(-) diff --git a/RelayNext/plugin.py b/RelayNext/plugin.py index d3a56c7..33390da 100644 --- a/RelayNext/plugin.py +++ b/RelayNext/plugin.py @@ -80,14 +80,6 @@ class RelayNext(callbacks.Plugin): def __init__(self, irc): self.__parent = super(RelayNext, self) self.__parent.__init__(irc) - # This part is partly taken from the Relay plugin, and is used to - # keep track of quitting users. Since quit messages aren't - # channel-specific, we need to keep a cached copy of our IRC state - # file and look at that to find whether we should send a quit - # message through the relay. The conventional check for - # 'irc.state.channels[channel]' won't work either, - # because the user in question has already quit. - self.ircstates = {} # This part facilitates flood protection self.msgcounters = {} @@ -95,11 +87,10 @@ class RelayNext(callbacks.Plugin): self.db = {} self.loadDB() + + # Add our database exporter to world.flushers, so it's automatically + # ran on every flush cycle. world.flushers.append(self.exportDB) - if irc.afterConnect: - for channel in self._getAllRelaysForNetwork(irc): - # irc.queueMsg(ircmsgs.who(channel)) - irc.queueMsg(ircmsgs.names(channel)) def die(self): self.exportDB() @@ -109,29 +100,20 @@ class RelayNext(callbacks.Plugin): ### Relayer core def simpleHash(self, s): - """ - - Returns a colorized version of based on a simple hash algorithm - (sum of all characters).""" + """ + Returns a colorized version of the given text based on a simple hash algorithm + (sum of all characters). + """ colors = ('02', '03', '04', '05', '06', '07', '08', '09', '10', '11', '12', '13') num = sum([ord(char) for char in s]) num = num % len(colors) return "\x03%s%s\x03" % (colors[num], s) - def _getAllRelaysForNetwork(self, irc): - """Returns all the relays a network is involved with.""" - network = irc.network.lower() - results = [] - for relay in self.db.values(): - for cn in relay: - cn = cn.split("@") - if cn[1] == network: - results.append(cn[0]) - self.log.debug('RelayNext: got %r for _getAllRelaysForNetwork for %r', results, network) - return results - def _format(self, irc, msg, channel, announcement=False): + """ + Formats a relay given the IRC object, msg object, and channel. + """ s = '' nick = msg.nick userhost = '' @@ -143,15 +125,20 @@ class RelayNext(callbacks.Plugin): if color: nick = self.simpleHash(nick) netname = self.simpleHash(netname) + + # Attempt to mitigate highlights (for some clients) by adding + # a hyphen in front of the nick. if noHighlight: nick = '-' + nick + # Skip hostmask checking if the sender is a server - # ('.') present in name + # (i.e. a '.' is present in their name) if useHostmask and '.' not in nick: try: userhost = ' (%s)' % msg.prefix.split('!', 1)[1] except: pass + if announcement: # Announcements use a special syntax s = '*** %s' % announcement @@ -161,6 +148,7 @@ class RelayNext(callbacks.Plugin): if color: newnick = self.simpleHash(newnick) s = '- %s is now known as %s' % (nick, newnick) + elif msg.command == 'PRIVMSG': text = msg.args[1] if re.match('^\x01ACTION .*\x01$', text): @@ -168,24 +156,31 @@ class RelayNext(callbacks.Plugin): s = '* %s %s' % (nick, text) else: s = '<%s> %s' % (nick, msg.args[1]) + elif msg.command == 'JOIN': s = '- %s%s has joined %s' % (nick, userhost, channel) + elif msg.command == 'PART': - # Part message isn't a required field and can be empty sometimes + # Part message isn't a required field and can be empty try: partmsg = ' (%s)' % msg.args[1] except: partmsg = '' s = '- %s%s has left %s%s' % (nick, userhost, channel, partmsg) + elif msg.command == 'QUIT': 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) + elif msg.command == 'TOPIC': s = '- %s set topic on %s to: %s' % (nick, channel, msg.args[1]) + elif msg.command == 'KICK': kicked = msg.args[1] + # Show the host of the kicked user, not the kicker userhost = irc.state.nickToHostmask(kicked).split('!', 1)[1] if color: kicked = self.simpleHash(kicked) @@ -199,12 +194,7 @@ class RelayNext(callbacks.Plugin): s = s.replace("- -", "-", 1) return s - def keepstate(self): - for irc in world.ircs: - self.ircstates[irc.network] = deepcopy(irc.state.channels) - def relay(self, irc, msg, channel=None): - self.keepstate() channel = (channel or msg.args[0]).lower() # Check for ignored events first @@ -229,32 +219,57 @@ class RelayNext(callbacks.Plugin): self.log.debug("RelayNext: found targets %s for relay %s", targets, relay) if self.registryValue("antiflood.enable", channel): + # Flood prevention timeout - how long commands of a certain type + # should cease being relayed after flood prevention triggers timeout = self.registryValue("antiflood.timeout", channel) - seconds = self.registryValue("antiflood.seconds", channel) + + # If messages of the same kind on one channel is + # received in seconds, flood prevention timeout is + # triggered. maximum = self.registryValue("antiflood.maximum", channel) + seconds = self.registryValue("antiflood.seconds", channel) + + # Store the message in a counter, with the keys taking the + # form of (source channel@network, command name). If the counter + # doesn't already exist, create one here. try: self.msgcounters[(source, msg.command)].enqueue(msg.prefix) except KeyError: self.msgcounters[(source, msg.command)] = TimeoutQueue(seconds) + + # Two different limits: one for messages and one for all others if msg.command == "PRIVMSG": maximum = self.registryValue("antiflood.maximum", channel) else: - maximum = self.registryValue("antiflood.maximum.nonPrivmsgs", channel) + maximum = self.registryValue("antiflood.maximum.nonPrivmsgs", + channel) + if len(self.msgcounters[(source, msg.command)]) > maximum: + # Amount of messages in the counter surpassed our limit, + # announce the flood and block relaying messages of the + # same type for X seconds self.log.debug("RelayNext (%s): message from %s blocked by " "flood protection.", irc.network, channel) + if self.floodTriggered.get((source, msg.command)): + # However, only send the announcement once. return - c = msg.command.lower() + + c = msg.command 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, channel, announcement=e) + self.floodTriggered[(source, msg.command)] = True self.log.info("RelayNext (%s): %s", irc.network, e) else: self.floodTriggered[(source, msg.command)] = False + for cn in targets: + # Iterate over all the relay targets for this message: + # each target is stored internally as a #channel@netname + # string. target, net = cn.split("@") otherIrc = world.getIrc(net) if otherIrc is None: @@ -271,6 +286,9 @@ class RelayNext(callbacks.Plugin): "channel!", target, net) else: out_msg = ircmsgs.privmsg(target, out_s) + + # Tag the message as relayed so we (and other relayers) don't + # try to relay it again. out_msg.tag('relayedMsg') otherIrc.queueMsg(out_msg) @@ -286,17 +304,13 @@ class RelayNext(callbacks.Plugin): # NICK and QUIT aren't channel specific, so they require a bit # of extra handling def doNick(self, irc, msg): - newnick = msg.args[0] - for channel in self._getAllRelaysForNetwork(irc): - if self.registryValue("events.relaynicks", channel) and \ - newnick in irc.state.channels[channel].users: + for channel in msg.tagged('channels'): + if self.registryValue("events.relaynicks", channel): self.relay(irc, msg, channel=channel) def doQuit(self, irc, msg): - for channel in self._getAllRelaysForNetwork(irc): - if self.registryValue("events.relayquits", channel) and \ - (msg.nick in self.ircstates[irc.network][channel].users \ - or msg.nick == irc.nick): + for channel in msg.tagged('channels'): + if self.registryValue("events.relayquits", channel): self.relay(irc, msg, channel=channel) def outFilter(self, irc, msg): @@ -304,10 +318,9 @@ class RelayNext(callbacks.Plugin): # useful because Supybot is often a multi-purpose bot!) try: if msg.command == 'PRIVMSG' and not msg.relayedMsg: - if msg.args[0].lower() in self._getAllRelaysForNetwork(irc): - new_msg = deepcopy(msg) - new_msg.nick = irc.nick - self.relay(irc, new_msg, channel=msg.args[0]) + new_msg = deepcopy(msg) + new_msg.nick = irc.nick + self.relay(irc, new_msg, channel=msg.args[0]) except Exception: # We want to log errors, but not block the bot's output log.exception("RelayNext: Caught error in outFilter:") @@ -323,12 +336,15 @@ class RelayNext(callbacks.Plugin): itself. If --count is specified, only the amount of users in the relay is given.""" opts = dict(optlist) + if irc.nested and 'count' not in keys: irc.error('This command cannot be nested.', Raise=True) + try: c = irc.state.channels[channel] except KeyError: irc.error("Unknown channel '%s'." % channel, Raise=True) + if msg.nick not in c.users: self.log.warning('RelayNext: %s on %s attempted to view' ' nicks in %s without being in it.', msg.nick,