def __init__(self, irc): self.__parent = super(Relay, self) self.__parent.__init__(irc) self._whois = {} self.lastmsg = {} self.ircstates = {} self.queuedTopics = MultiSet() self.lastRelayMsgs = ircutils.IrcDict()
class Relay(callbacks.Plugin): noIgnore = True def __init__(self, irc): self.__parent = super(Relay, self) self.__parent.__init__(irc) self._whois = {} self.lastmsg = {} self.ircstates = {} self.queuedTopics = MultiSet() self.lastRelayMsgs = ircutils.IrcDict() def __call__(self, irc, msg): try: irc = self._getRealIrc(irc) if irc not in self.ircstates: self._addIrc(irc) self.ircstates[irc].addMsg(irc, self.lastmsg[irc]) finally: self.lastmsg[irc] = msg self.__parent.__call__(irc, msg) def do376(self, irc, msg): networkGroup = conf.supybot.networks.get(irc.network) for channel in self.registryValue("channels"): if self.registryValue("channels.joinOnAllNetworks", channel): if channel not in irc.state.channels: irc.queueMsg(networkGroup.channels.join(channel)) do377 = do422 = do376 def _getRealIrc(self, irc): if isinstance(irc, irclib.Irc): return irc else: return irc.getRealIrc() def _getIrcName(self, irc): # We should allow abbreviations at some point. return irc.network def _addIrc(self, irc): # Let's just be extra-special-careful here. if irc not in self.ircstates: self.ircstates[irc] = irclib.IrcState() if irc not in self.lastmsg: self.lastmsg[irc] = ircmsgs.ping("this is just a fake message") if irc.afterConnect: # We've probably been reloaded. Let's send some messages to get # our IrcState objects up to current. for channel in self.registryValue("channels"): irc.queueMsg(ircmsgs.who(channel)) irc.queueMsg(ircmsgs.names(channel)) def join(self, irc, msg, args, channel): """[<channel>] Starts relaying between the channel <channel> on all networks. If on a network the bot isn't in <channel>, he'll join. This commands is required even if the bot is in the channel on both networks; he won't relay between those channels unless he's told to join both channels. If <channel> is not given, starts relaying on the channel the message was sent in. """ self.registryValue("channels").add(channel) for otherIrc in world.ircs: if channel not in otherIrc.state.channels: networkGroup = conf.supybot.networks.get(otherIrc.network) otherIrc.queueMsg(networkGroup.channels.join(channel)) irc.replySuccess() join = wrap(join, ["channel", "admin"]) def part(self, irc, msg, args, channel): """<channel> Ceases relaying between the channel <channel> on all networks. The bot will part from the channel on all networks in which it is on the channel. """ self.registryValue("channels").discard(channel) for otherIrc in world.ircs: if channel in otherIrc.state.channels: otherIrc.queueMsg(ircmsgs.part(channel)) irc.replySuccess() part = wrap(part, ["channel", "admin"]) def nicks(self, irc, msg, args, channel): """[<channel>] Returns the nicks of the people in the channel on the various networks the bot is connected to. <channel> is only necessary if the message isn't sent on the channel itself. """ realIrc = self._getRealIrc(irc) if channel not in self.registryValue("channels"): irc.error(format("I'm not relaying in %s.", channel)) return users = [] for otherIrc in world.ircs: network = self._getIrcName(otherIrc) ops = [] halfops = [] voices = [] usersS = [] if network != self._getIrcName(realIrc): try: Channel = otherIrc.state.channels[channel] except KeyError: users.append(format("(not in %s on %s)", channel, network)) continue numUsers = 0 for s in Channel.users: s = s.strip() if not s: continue numUsers += 1 if s in Channel.ops: ops.append("@" + s) elif s in Channel.halfops: halfops.append("%" + s) elif s in Channel.voices: voices.append("+" + s) else: usersS.append(s) utils.sortBy(ircutils.toLower, ops) utils.sortBy(ircutils.toLower, voices) utils.sortBy(ircutils.toLower, halfops) utils.sortBy(ircutils.toLower, usersS) usersS = ", ".join(filter(None, map(", ".join, (ops, halfops, voices, usersS)))) users.append(format("%s (%i): %s", ircutils.bold(network), numUsers, usersS)) users.sort() irc.reply("; ".join(users)) nicks = wrap(nicks, ["channel"]) def do311(self, irc, msg): irc = self._getRealIrc(irc) nick = ircutils.toLower(msg.args[1]) if (irc, nick) not in self._whois: return else: self._whois[(irc, nick)][-1][msg.command] = msg # These are all sent by a WHOIS response. do301 = do311 do312 = do311 do317 = do311 do319 = do311 do320 = do311 def do318(self, irc, msg): irc = self._getRealIrc(irc) nick = msg.args[1] loweredNick = ircutils.toLower(nick) if (irc, loweredNick) not in self._whois: return (replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)] d["318"] = msg s = ircutils.formatWhois(irc, d, caller=replyMsg.nick, channel=replyMsg.args[0]) replyIrc.reply(s) del self._whois[(irc, loweredNick)] def do402(self, irc, msg): irc = self._getRealIrc(irc) nick = msg.args[1] loweredNick = ircutils.toLower(nick) if (irc, loweredNick) not in self._whois: return (replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)] del self._whois[(irc, loweredNick)] s = format("There is no %s on %s.", nick, self._getIrcName(irc)) replyIrc.reply(s) do401 = do402 def _formatPrivmsg(self, nick, network, msg): channel = msg.args[0] if self.registryValue("includeNetwork", channel): network = "@" + network else: network = "" # colorize nicks color = self.registryValue("color", channel) # Also used further down. if color: nick = ircutils.IrcString(nick) newnick = ircutils.mircColor(nick, *ircutils.canonicalColor(nick)) colors = ircutils.canonicalColor(nick, shift=4) nick = newnick if ircmsgs.isAction(msg): if color: t = ircutils.mircColor("*", *colors) else: t = "*" s = format("%s %s%s %s", t, nick, network, ircmsgs.unAction(msg)) else: if color: lt = ircutils.mircColor("<", *colors) gt = ircutils.mircColor(">", *colors) else: lt = "<" gt = ">" s = format("%s%s%s%s %s", lt, nick, network, gt, msg.args[1]) return s def _sendToOthers(self, irc, msg): assert msg.command in ("PRIVMSG", "NOTICE", "TOPIC") for otherIrc in world.ircs: if otherIrc != irc and not otherIrc.zombie: if msg.args[0] in otherIrc.state.channels: msg.tag("relayedMsg") otherIrc.queueMsg(msg) def _checkRelayMsg(self, msg): channel = msg.args[0] if channel in self.lastRelayMsgs: q = self.lastRelayMsgs[channel] unformatted = ircutils.stripFormatting(msg.args[1]) normalized = utils.str.normalizeWhitespace(unformatted) for s in q: if s in normalized: return True return False def _punishRelayers(self, msg): assert self._checkRelayMsg(msg), "Punishing without checking." who = msg.prefix channel = msg.args[0] def notPunishing(irc, s, *args): self.log.info("Not punishing %s in %s on %s: %s.", msg.prefix, channel, irc.network, s, *args) for irc in world.ircs: if channel in irc.state.channels: if irc.nick in irc.state.channels[channel].ops: if who in irc.state.channels[channel].bans: notPunishing(irc, "already banned") else: self.log.info("Punishing %s in %s on %s for relaying.", who, channel, irc.network) irc.sendMsg(ircmsgs.ban(channel, who)) kmsg = "You seem to be relaying, punk." irc.sendMsg(ircmsgs.kick(channel, msg.nick, kmsg)) else: notPunishing(irc, "not opped") def doPrivmsg(self, irc, msg): if ircmsgs.isCtcp(msg) and not ircmsgs.isAction(msg): return (channel, text) = msg.args if irc.isChannel(channel): irc = self._getRealIrc(irc) if channel not in self.registryValue("channels"): return ignores = self.registryValue("ignores", channel) for ignore in ignores: if ircutils.hostmaskPatternEqual(ignore, msg.prefix): self.log.debug("Refusing to relay %s, ignored by %s.", msg.prefix, ignore) return # Let's try to detect other relay bots. if self._checkRelayMsg(msg): if self.registryValue("punishOtherRelayBots", channel): self._punishRelayers(msg) # Either way, we don't relay the message. else: self.log.warning( "Refusing to relay message from %s, " "it appears to be a relay message.", msg.prefix ) else: network = self._getIrcName(irc) s = self._formatPrivmsg(msg.nick, network, msg) m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def _msgmaker(self, target, s): msg = dynamic.msg channel = dynamic.channel if self.registryValue("noticeNonPrivmsgs", dynamic.channel) and msg.command != "PRIVMSG": return ircmsgs.notice(target, s) else: return ircmsgs.privmsg(target, s) def doJoin(self, irc, msg): irc = self._getRealIrc(irc) channel = msg.args[0] if channel not in self.registryValue("channels"): return network = self._getIrcName(irc) if self.registryValue("hostmasks", channel): hostmask = format(" (%s)", msg.prefix.split("!")[1]) else: hostmask = "" s = format("%s%s has joined on %s", msg.nick, hostmask, network) m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def doPart(self, irc, msg): irc = self._getRealIrc(irc) channel = msg.args[0] if channel not in self.registryValue("channels"): return network = self._getIrcName(irc) if self.registryValue("hostmasks", channel): hostmask = format(" (%s)", msg.prefix.split("!")[1]) else: hostmask = "" if len(msg.args) > 1: s = format("%s%s has left on %s (%s)", msg.nick, hostmask, network, msg.args[1]) else: s = format("%s%s has left on %s", msg.nick, hostmask, network) m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def doMode(self, irc, msg): irc = self._getRealIrc(irc) channel = msg.args[0] if channel not in self.registryValue("channels"): return network = self._getIrcName(irc) s = format("mode change by %s on %s: %s", msg.nick, network, " ".join(msg.args[1:])) m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def doKick(self, irc, msg): irc = self._getRealIrc(irc) channel = msg.args[0] if channel not in self.registryValue("channels"): return network = self._getIrcName(irc) if len(msg.args) == 3: s = format("%s was kicked by %s on %s (%s)", msg.args[1], msg.nick, network, msg.args[2]) else: s = format("%s was kicked by %s on %s", msg.args[1], msg.nick, network) m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def doNick(self, irc, msg): irc = self._getRealIrc(irc) newNick = msg.args[0] network = self._getIrcName(irc) s = format("nick change by %s to %s on %s", msg.nick, newNick, network) for channel in self.registryValue("channels"): if channel in irc.state.channels: if newNick in irc.state.channels[channel].users: m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def doTopic(self, irc, msg): irc = self._getRealIrc(irc) (channel, newTopic) = msg.args if channel not in self.registryValue("channels"): return network = self._getIrcName(irc) if self.registryValue("topicSync", channel): m = ircmsgs.topic(channel, newTopic) for otherIrc in world.ircs: if irc != otherIrc: try: if otherIrc.state.getTopic(channel) != newTopic: if (otherIrc, newTopic) not in self.queuedTopics: self.queuedTopics.add((otherIrc, newTopic)) otherIrc.queueMsg(m) else: self.queuedTopics.remove((otherIrc, newTopic)) except KeyError: self.log.warning("Not on %s on %s, " "can't sync topics.", channel, otherIrc.network) else: s = format("topic change by %s on %s: %s", msg.nick, network, newTopic) m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def doQuit(self, irc, msg): irc = self._getRealIrc(irc) network = self._getIrcName(irc) if msg.args: s = format("%s has quit %s (%s)", msg.nick, network, msg.args[0]) else: s = format("%s has quit %s.", msg.nick, network) for channel in self.registryValue("channels"): if channel in self.ircstates[irc].channels: if msg.nick in self.ircstates[irc].channels[channel].users: m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def doError(self, irc, msg): irc = self._getRealIrc(irc) network = self._getIrcName(irc) s = format("disconnected from %s: %s", network, msg.args[0]) for channel in self.registryValue("channels"): m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def outFilter(self, irc, msg): irc = self._getRealIrc(irc) if msg.command == "PRIVMSG": if msg.relayedMsg: self._addRelayMsg(msg) else: channel = msg.args[0] if channel in self.registryValue("channels"): network = self._getIrcName(irc) s = self._formatPrivmsg(irc.nick, network, msg) relayMsg = self._msgmaker(channel, s) self._sendToOthers(irc, relayMsg) return msg def _addRelayMsg(self, msg): channel = msg.args[0] if channel in self.lastRelayMsgs: q = self.lastRelayMsgs[channel] else: q = TimeoutQueue(60) # XXX Make this configurable. self.lastRelayMsgs[channel] = q unformatted = ircutils.stripFormatting(msg.args[1]) normalized = utils.str.normalizeWhitespace(unformatted) q.enqueue(normalized)
class Relay(callbacks.Plugin): noIgnore = True def __init__(self, irc): self.__parent = super(Relay, self) self.__parent.__init__(irc) self._whois = {} self.lastmsg = {} self.ircstates = {} self.queuedTopics = MultiSet() self.lastRelayMsgs = ircutils.IrcDict() def __call__(self, irc, msg): try: irc = self._getRealIrc(irc) if irc not in self.ircstates: self._addIrc(irc) self.ircstates[irc].addMsg(irc, self.lastmsg[irc]) finally: self.lastmsg[irc] = msg self.__parent.__call__(irc, msg) def do376(self, irc, msg): networkGroup = conf.supybot.networks.get(irc.network) for channel in self.registryValue('channels'): if self.registryValue('channels.joinOnAllNetworks', channel): if channel not in irc.state.channels: irc.queueMsg(networkGroup.channels.join(channel)) do377 = do422 = do376 def _getRealIrc(self, irc): if isinstance(irc, irclib.Irc): return irc else: return irc.getRealIrc() def _getIrcName(self, irc): # We should allow abbreviations at some point. return irc.network def _addIrc(self, irc): # Let's just be extra-special-careful here. if irc not in self.ircstates: self.ircstates[irc] = irclib.IrcState() if irc not in self.lastmsg: self.lastmsg[irc] = ircmsgs.ping('this is just a fake message') if irc.afterConnect: # We've probably been reloaded. Let's send some messages to get # our IrcState objects up to current. for channel in self.registryValue('channels'): irc.queueMsg(ircmsgs.who(channel)) irc.queueMsg(ircmsgs.names(channel)) @internationalizeDocstring def join(self, irc, msg, args, channel): """[<channel>] Starts relaying between the channel <channel> on all networks. If on a network the bot isn't in <channel>, he'll join. This commands is required even if the bot is in the channel on both networks; he won't relay between those channels unless he's told to join both channels. If <channel> is not given, starts relaying on the channel the message was sent in. """ self.registryValue('channels').add(channel) for otherIrc in world.ircs: if channel not in otherIrc.state.channels: networkGroup = conf.supybot.networks.get(otherIrc.network) otherIrc.queueMsg(networkGroup.channels.join(channel)) irc.replySuccess() join = wrap(join, ['channel', 'admin']) @internationalizeDocstring def part(self, irc, msg, args, channel): """<channel> Ceases relaying between the channel <channel> on all networks. The bot will part from the channel on all networks in which it is on the channel. """ self.registryValue('channels').discard(channel) for otherIrc in world.ircs: if channel in otherIrc.state.channels: otherIrc.queueMsg(ircmsgs.part(channel)) irc.replySuccess() part = wrap(part, ['channel', 'admin']) @internationalizeDocstring def nicks(self, irc, msg, args, channel): """[<channel>] Returns the nicks of the people in the channel on the various networks the bot is connected to. <channel> is only necessary if the message isn't sent on the channel itself. """ realIrc = self._getRealIrc(irc) if channel not in self.registryValue('channels'): irc.error(format('I\'m not relaying in %s.', channel)) return users = [] for otherIrc in world.ircs: network = self._getIrcName(otherIrc) ops = [] halfops = [] voices = [] usersS = [] if network != self._getIrcName(realIrc): try: Channel = otherIrc.state.channels[channel] except KeyError: users.append(format('(not in %s on %s)',channel,network)) continue numUsers = 0 for s in Channel.users: s = s.strip() if not s: continue numUsers += 1 if s in Channel.ops: ops.append('@' + s) elif s in Channel.halfops: halfops.append('%' + s) elif s in Channel.voices: voices.append('+' + s) else: usersS.append(s) utils.sortBy(ircutils.toLower, ops) utils.sortBy(ircutils.toLower, voices) utils.sortBy(ircutils.toLower, halfops) utils.sortBy(ircutils.toLower, usersS) usersS = ', '.join(filter(None, map(', '.join, (ops,halfops,voices,usersS)))) users.append(format('%s (%i): %s', ircutils.bold(network), numUsers, usersS)) users.sort() irc.reply('; '.join(users)) nicks = wrap(nicks, ['channel']) def do311(self, irc, msg): irc = self._getRealIrc(irc) nick = ircutils.toLower(msg.args[1]) if (irc, nick) not in self._whois: return else: self._whois[(irc, nick)][-1][msg.command] = msg # These are all sent by a WHOIS response. do301 = do311 do312 = do311 do317 = do311 do319 = do311 do320 = do311 def do318(self, irc, msg): irc = self._getRealIrc(irc) nick = msg.args[1] loweredNick = ircutils.toLower(nick) if (irc, loweredNick) not in self._whois: return (replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)] hostmask = '@'.join(d['311'].args[2:4]) user = d['311'].args[-1] if '319' in d: channels = d['319'].args[-1].split() ops = [] voices = [] normal = [] halfops = [] for channel in channels: if channel.startswith('@'): ops.append(channel[1:]) elif channel.startswith('%'): halfops.append(channel[1:]) elif channel.startswith('+'): voices.append(channel[1:]) else: normal.append(channel) L = [] if ops: L.append(format(_('is an op on %L'), ops)) if halfops: L.append(format(_('is a halfop on %L'), halfups)) if voices: L.append(format(_('is voiced on %L'), voices)) if normal: if L: L.append(format(_('is also on %L'), normal)) else: L.append(format(_('is on %L'), normal)) else: L = [_('isn\'t on any non-secret channels')] channels = format('%L', L) if '317' in d: idle = utils.timeElapsed(d['317'].args[2]) signon = time.strftime(conf.supybot.reply.format.time(), time.localtime(float(d['317'].args[3]))) else: idle = _('<unknown>') signon = _('<unknown>') if '312' in d: server = d['312'].args[2] else: server = _('<unknown>') if '301' in d: away = format(_(' %s is away: %s.'), nick, d['301'].args[2]) else: away = '' if '320' in d: if d['320'].args[2]: identify = _(' identified') else: identify = '' else: identify = '' s = format(_('%s (%s) has been%s on server %s since %s (idle for %s) ' 'and %s.%s'), user, hostmask, identify, server, signon, idle, channels, away) replyIrc.reply(s) del self._whois[(irc, loweredNick)] def do402(self, irc, msg): irc = self._getRealIrc(irc) nick = msg.args[1] loweredNick = ircutils.toLower(nick) if (irc, loweredNick) not in self._whois: return (replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)] del self._whois[(irc, loweredNick)] s = format(_('There is no %s on %s.'), nick, self._getIrcName(irc)) replyIrc.reply(s) do401 = do402 def _formatPrivmsg(self, nick, network, msg): channel = msg.args[0] if self.registryValue('includeNetwork', channel): network = '@' + network else: network = '' # colorize nicks color = self.registryValue('color', channel) # Also used further down. if color: nick = ircutils.IrcString(nick) newnick = ircutils.mircColor(nick, *ircutils.canonicalColor(nick)) colors = ircutils.canonicalColor(nick, shift=4) nick = newnick if ircmsgs.isAction(msg): if color: t = ircutils.mircColor('*', *colors) else: t = '*' s = format('%s %s%s %s', t, nick, network, ircmsgs.unAction(msg)) else: if color: lt = ircutils.mircColor('<', *colors) gt = ircutils.mircColor('>', *colors) else: lt = '<' gt = '>' s = format('%s%s%s%s %s', lt, nick, network, gt, msg.args[1]) return s def _sendToOthers(self, irc, msg): assert msg.command in ('PRIVMSG', 'NOTICE', 'TOPIC') for otherIrc in world.ircs: if otherIrc != irc and not otherIrc.zombie: if msg.args[0] in otherIrc.state.channels: msg.tag('relayedMsg') otherIrc.queueMsg(msg) def _checkRelayMsg(self, msg): channel = msg.args[0] if channel in self.lastRelayMsgs: q = self.lastRelayMsgs[channel] unformatted = ircutils.stripFormatting(msg.args[1]) normalized = utils.str.normalizeWhitespace(unformatted) for s in q: if s in normalized: return True return False def _punishRelayers(self, msg): assert self._checkRelayMsg(msg), 'Punishing without checking.' who = msg.prefix channel = msg.args[0] def notPunishing(irc, s, *args): self.log.info('Not punishing %s in %s on %s: %s.', msg.prefix, channel, irc.network, s, *args) for irc in world.ircs: if channel in irc.state.channels: if irc.nick in irc.state.channels[channel].ops: if who in irc.state.channels[channel].bans: notPunishing(irc, 'already banned') else: self.log.info('Punishing %s in %s on %s for relaying.', who, channel, irc.network) irc.sendMsg(ircmsgs.ban(channel, who)) kmsg = _('You seem to be relaying, punk.') irc.sendMsg(ircmsgs.kick(channel, msg.nick, kmsg)) else: notPunishing(irc, 'not opped') def doPrivmsg(self, irc, msg): if ircmsgs.isCtcp(msg) and not ircmsgs.isAction(msg): return (channel, text) = msg.args if irc.isChannel(channel): irc = self._getRealIrc(irc) if channel not in self.registryValue('channels'): return ignores = self.registryValue('ignores', channel) for ignore in ignores: if ircutils.hostmaskPatternEqual(ignore, msg.prefix): self.log.debug('Refusing to relay %s, ignored by %s.', msg.prefix, ignore) return # Let's try to detect other relay bots. if self._checkRelayMsg(msg): if self.registryValue('punishOtherRelayBots', channel): self._punishRelayers(msg) # Either way, we don't relay the message. else: self.log.warning('Refusing to relay message from %s, ' 'it appears to be a relay message.', msg.prefix) else: network = self._getIrcName(irc) s = self._formatPrivmsg(msg.nick, network, msg) m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def _msgmaker(self, target, s): msg = dynamic.msg channel = dynamic.channel if self.registryValue('noticeNonPrivmsgs', dynamic.channel) and \ msg.command != 'PRIVMSG': return ircmsgs.notice(target, s) else: return ircmsgs.privmsg(target, s) def doJoin(self, irc, msg): irc = self._getRealIrc(irc) channel = msg.args[0] if channel not in self.registryValue('channels'): return network = self._getIrcName(irc) if self.registryValue('hostmasks', channel): hostmask = format(' (%s)', msg.prefix.split('!')[1]) else: hostmask = '' s = format(_('%s%s has joined on %s'), msg.nick, hostmask, network) m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def doPart(self, irc, msg): irc = self._getRealIrc(irc) channel = msg.args[0] if channel not in self.registryValue('channels'): return network = self._getIrcName(irc) if self.registryValue('hostmasks', channel): hostmask = format(' (%s)', msg.prefix.split('!')[1]) else: hostmask = '' if len(msg.args) > 1: s = format(_('%s%s has left on %s (%s)'), msg.nick, hostmask, network, msg.args[1]) else: s = format(_('%s%s has left on %s'), msg.nick, hostmask, network) m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def doMode(self, irc, msg): irc = self._getRealIrc(irc) channel = msg.args[0] if channel not in self.registryValue('channels'): return network = self._getIrcName(irc) s = format(_('mode change by %s on %s: %s'), msg.nick, network, ' '.join(msg.args[1:])) m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def doKick(self, irc, msg): irc = self._getRealIrc(irc) channel = msg.args[0] if channel not in self.registryValue('channels'): return network = self._getIrcName(irc) if len(msg.args) == 3: s = format(_('%s was kicked by %s on %s (%s)'), msg.args[1], msg.nick, network, msg.args[2]) else: s = format(_('%s was kicked by %s on %s'), msg.args[1], msg.nick, network) m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def doNick(self, irc, msg): irc = self._getRealIrc(irc) newNick = msg.args[0] network = self._getIrcName(irc) s = format(_('nick change by %s to %s on %s'), msg.nick,newNick,network) for channel in self.registryValue('channels'): if channel in irc.state.channels: if newNick in irc.state.channels[channel].users: m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def doTopic(self, irc, msg): irc = self._getRealIrc(irc) (channel, newTopic) = msg.args if channel not in self.registryValue('channels'): return network = self._getIrcName(irc) if self.registryValue('topicSync', channel): m = ircmsgs.topic(channel, newTopic) for otherIrc in world.ircs: if irc != otherIrc: try: if otherIrc.state.getTopic(channel) != newTopic: if (otherIrc, newTopic) not in self.queuedTopics: self.queuedTopics.add((otherIrc, newTopic)) otherIrc.queueMsg(m) else: self.queuedTopics.remove((otherIrc, newTopic)) except KeyError: self.log.warning('Not on %s on %s, ' 'can\'t sync topics.', channel, otherIrc.network) else: s = format(_('topic change by %s on %s: %s'), msg.nick, network, newTopic) m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def doQuit(self, irc, msg): irc = self._getRealIrc(irc) network = self._getIrcName(irc) if msg.args: s = format(_('%s has quit %s (%s)'), msg.nick, network, msg.args[0]) else: s = format(_('%s has quit %s.'), msg.nick, network) for channel in self.registryValue('channels'): if channel in self.ircstates[irc].channels: if msg.nick in self.ircstates[irc].channels[channel].users: m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def doError(self, irc, msg): irc = self._getRealIrc(irc) network = self._getIrcName(irc) s = format(_('disconnected from %s: %s'), network, msg.args[0]) for channel in self.registryValue('channels'): m = self._msgmaker(channel, s) self._sendToOthers(irc, m) def outFilter(self, irc, msg): irc = self._getRealIrc(irc) if msg.command == 'PRIVMSG': if msg.relayedMsg: self._addRelayMsg(msg) else: channel = msg.args[0] if channel in self.registryValue('channels'): network = self._getIrcName(irc) s = self._formatPrivmsg(irc.nick, network, msg) relayMsg = self._msgmaker(channel, s) self._sendToOthers(irc, relayMsg) return msg def _addRelayMsg(self, msg): channel = msg.args[0] if channel in self.lastRelayMsgs: q = self.lastRelayMsgs[channel] else: q = TimeoutQueue(60) # XXX Make this configurable. self.lastRelayMsgs[channel] = q unformatted = ircutils.stripFormatting(msg.args[1]) normalized = utils.str.normalizeWhitespace(unformatted) q.enqueue(normalized)
class Relay(callbacks.Plugin): """ This plugin allows you to setup a relay between networks. Note that you must tell the bot to join the channel you wish to relay on all networks with the ``join`` command or ``network command <network> join <channel>`` or to join the channel on all networks ``network cmdall join <channel>``. There are several advanced alternatives to this plugin, available as third-party plugins. You can check them out at https://limnoria.net/plugins.xhtml#messaging """ noIgnore = True def __init__(self, irc): self.__parent = super(Relay, self) self.__parent.__init__(irc) self._whois = {} self.queuedTopics = MultiSet() self.lastRelayMsgs = ircutils.IrcDict() def do376(self, irc, msg): networkGroup = conf.supybot.networks.get(irc.network) for channel in self.registryValue('channels'): if self.registryValue('channels.joinOnAllNetworks', channel): if channel not in irc.state.channels: irc.queueMsg(networkGroup.channels.join(channel)) do377 = do422 = do376 def _getRealIrc(self, irc): if isinstance(irc, irclib.Irc): return irc else: return irc.getRealIrc() def _getIrcName(self, irc): # We should allow abbreviations at some point. return irc.network @internationalizeDocstring def join(self, irc, msg, args, channel): """[<channel>] Starts relaying between the channel <channel> on all networks. If on a network the bot isn't in <channel>, it'll join. This commands is required even if the bot is in the channel on both networks; it won't relay between those channels unless it's told to join both channels. If <channel> is not given, starts relaying on the channel the message was sent in. """ self.registryValue('channels').add(channel) for otherIrc in world.ircs: if channel not in otherIrc.state.channels: networkGroup = conf.supybot.networks.get(otherIrc.network) otherIrc.queueMsg(networkGroup.channels.join(channel)) irc.replySuccess() join = wrap(join, ['channel', 'admin']) @internationalizeDocstring def part(self, irc, msg, args, channel): """<channel> Ceases relaying between the channel <channel> on all networks. The bot will part from the channel on all networks in which it is on the channel. """ self.registryValue('channels').discard(channel) for otherIrc in world.ircs: if channel in otherIrc.state.channels: otherIrc.queueMsg(ircmsgs.part(channel)) irc.replySuccess() part = wrap(part, ['channel', 'admin']) @internationalizeDocstring def nicks(self, irc, msg, args, channel): """[<channel>] Returns the nicks of the people in the channel on the various networks the bot is connected to. <channel> is only necessary if the message isn't sent on the channel itself. """ realIrc = self._getRealIrc(irc) if channel not in self.registryValue('channels'): irc.error(format('I\'m not relaying in %s.', channel)) return users = [] for otherIrc in world.ircs: network = self._getIrcName(otherIrc) ops = [] halfops = [] voices = [] usersS = [] if network != self._getIrcName(realIrc): try: Channel = otherIrc.state.channels[channel] except KeyError: users.append(format('(not in %s on %s)', channel, network)) continue numUsers = 0 for s in Channel.users: s = s.strip() if not s: continue numUsers += 1 if s in Channel.ops: ops.append('@' + s) elif s in Channel.halfops: halfops.append('%' + s) elif s in Channel.voices: voices.append('+' + s) else: usersS.append(s) utils.sortBy(ircutils.toLower, ops) utils.sortBy(ircutils.toLower, voices) utils.sortBy(ircutils.toLower, halfops) utils.sortBy(ircutils.toLower, usersS) usersS = ', '.join( filter( None, list(map(', '.join, (ops, halfops, voices, usersS))))) users.append( format('%s (%i): %s', ircutils.bold(network), numUsers, usersS)) users.sort() irc.reply('; '.join(users)) nicks = wrap(nicks, ['channel']) def do311(self, irc, msg): irc = self._getRealIrc(irc) nick = ircutils.toLower(msg.args[1]) if (irc, nick) not in self._whois: return else: self._whois[(irc, nick)][-1][msg.command] = msg # These are all sent by a WHOIS response. do301 = do311 do312 = do311 do317 = do311 do319 = do311 do320 = do311 def do318(self, irc, msg): irc = self._getRealIrc(irc) nick = msg.args[1] loweredNick = ircutils.toLower(nick) if (irc, loweredNick) not in self._whois: return (replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)] d['318'] = msg s = ircutils.formatWhois(irc, d, caller=replyMsg.nick, channel=replyMsg.args[0]) replyIrc.reply(s) del self._whois[(irc, loweredNick)] def do402(self, irc, msg): irc = self._getRealIrc(irc) nick = msg.args[1] loweredNick = ircutils.toLower(nick) if (irc, loweredNick) not in self._whois: return (replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)] del self._whois[(irc, loweredNick)] s = format(_('There is no %s on %s.'), nick, self._getIrcName(irc)) replyIrc.reply(s) do401 = do402 def _formatDisplayName(self, nick, network, channel): displayName = nick if self.registryValue('includeNetwork', channel): displayName += '@' + network return displayName def _formatPrivmsg(self, nick, network, msg): channel = msg.channel # colorize nicks color = self.registryValue('color', channel) # Also used further down. if color: nick = ircutils.IrcString(nick) newnick = ircutils.mircColor(nick, *ircutils.canonicalColor(nick)) colors = ircutils.canonicalColor(nick, shift=4) nick = newnick if ircmsgs.isAction(msg): if color: t = ircutils.mircColor('*', *colors) else: t = '*' displayName = self._formatDisplayName(nick, network, msg.channel) s = format('%s %s %s', t, displayName, ircmsgs.unAction(msg)) else: if color: lt = ircutils.mircColor('<', *colors) gt = ircutils.mircColor('>', *colors) else: lt = '<' gt = '>' displayName = self._formatDisplayName(nick, network, msg.channel) s = format('%s%s%s %s', lt, displayName, gt, msg.args[1]) return s def _sendToOthers(self, irc, msg, nick): assert msg.command in ('PRIVMSG', 'NOTICE', 'TOPIC') for otherIrc in world.ircs: if otherIrc != irc and not otherIrc.zombie: if msg.channel in otherIrc.state.channels: self._sendToOther(irc, otherIrc, msg, nick) def _sendToOther(self, sourceIrc, destIrc, msg, nick): msg = copy.deepcopy(msg) msg.tag('relayedMsg') if 'message-tags' in destIrc.state.capabilities_ack \ and conf.supybot.protocols.irc.experimentalExtensions(): displayName = self._formatDisplayName(nick, sourceIrc.network, msg.channel) # https://github.com/ircv3/ircv3-specifications/pull/452 msg.server_tags['+draft/display-name'] = displayName destIrc.queueMsg(msg) def _checkRelayMsg(self, msg): channel = msg.channel if channel in self.lastRelayMsgs: q = self.lastRelayMsgs[channel] unformatted = ircutils.stripFormatting(msg.args[1]) normalized = utils.str.normalizeWhitespace(unformatted) for s in q: if s in normalized: return True return False def _punishRelayers(self, msg): assert self._checkRelayMsg(msg), 'Punishing without checking.' who = msg.prefix channel = msg.channel def notPunishing(irc, s, *args): self.log.info('Not punishing %s in %s on %s: %s.', msg.prefix, channel, irc.network, s, *args) for irc in world.ircs: if channel in irc.state.channels: if irc.nick in irc.state.channels[channel].ops: if who in irc.state.channels[channel].bans: notPunishing(irc, 'already banned') else: self.log.info('Punishing %s in %s on %s for relaying.', who, channel, irc.network) irc.sendMsg(ircmsgs.ban(channel, who)) kmsg = _('You seem to be relaying, punk.') irc.sendMsg(ircmsgs.kick(channel, msg.nick, kmsg)) else: notPunishing(irc, 'not opped') def doPrivmsg(self, irc, msg): if ircmsgs.isCtcp(msg) and not ircmsgs.isAction(msg): return text = msg.args[1] if msg.channel: irc = self._getRealIrc(irc) if msg.channel not in self.registryValue('channels'): return ignores = self.registryValue('ignores', msg.channel, irc.network) for ignore in ignores: if ircutils.hostmaskPatternEqual(ignore, msg.prefix): self.log.debug('Refusing to relay %s, ignored by %s.', msg.prefix, ignore) return # Let's try to detect other relay bots. if self._checkRelayMsg(msg): if self.registryValue('punishOtherRelayBots', msg.channel, irc.network): self._punishRelayers(msg) # Either way, we don't relay the message. else: self.log.warning( 'Refusing to relay message from %s, ' 'it appears to be a relay message.', msg.prefix) else: network = self._getIrcName(irc) s = self._formatPrivmsg(msg.nick, network, msg) m = self._msgmaker(msg.channel, network, s) self._sendToOthers(irc, m, msg.nick) def _msgmaker(self, target, network, s): msg = dynamic.msg if self.registryValue('noticeNonPrivmsgs', target, network) and \ msg.command != 'PRIVMSG': m = ircmsgs.notice(target, s) else: m = ircmsgs.privmsg(target, s) m.channel = target return m def doJoin(self, irc, msg): irc = self._getRealIrc(irc) channel = msg.args[0] if channel not in self.registryValue('channels'): return network = self._getIrcName(irc) if self.registryValue('hostmasks', channel) and '!' in msg.prefix: hostmask = format(' (%s)', msg.prefix.split('!')[1]) else: hostmask = '' s = format(_('%s%s has joined on %s'), msg.nick, hostmask, network) m = self._msgmaker(channel, network, s) self._sendToOthers(irc, m, msg.nick) def doPart(self, irc, msg): irc = self._getRealIrc(irc) channel = msg.args[0] if channel not in self.registryValue('channels'): return network = self._getIrcName(irc) if self.registryValue('hostmasks', channel) and '!' in msg.prefix: hostmask = format(' (%s)', msg.prefix.split('!')[1]) else: hostmask = '' if len(msg.args) > 1: s = format(_('%s%s has left on %s (%s)'), msg.nick, hostmask, network, msg.args[1]) else: s = format(_('%s%s has left on %s'), msg.nick, hostmask, network) m = self._msgmaker(channel, network, s) self._sendToOthers(irc, m, msg.nick) def doMode(self, irc, msg): irc = self._getRealIrc(irc) channel = msg.args[0] if channel not in self.registryValue('channels'): return network = self._getIrcName(irc) s = format(_('mode change by %s on %s: %s'), msg.nick, network, ' '.join(msg.args[1:])) m = self._msgmaker(channel, network, s) self._sendToOthers(irc, m, msg.nick) def doKick(self, irc, msg): irc = self._getRealIrc(irc) channel = msg.args[0] if channel not in self.registryValue('channels'): return network = self._getIrcName(irc) if len(msg.args) == 3: s = format(_('%s was kicked by %s on %s (%s)'), msg.args[1], msg.nick, network, msg.args[2]) else: s = format(_('%s was kicked by %s on %s'), msg.args[1], msg.nick, network) m = self._msgmaker(channel, network, s) self._sendToOthers(irc, m, msg.nick) def doNick(self, irc, msg): irc = self._getRealIrc(irc) newNick = msg.args[0] network = self._getIrcName(irc) s = format(_('nick change by %s to %s on %s'), msg.nick, newNick, network) for channel in self.registryValue('channels'): if channel not in msg.tagged('channels'): continue m = self._msgmaker(channel, network, s) self._sendToOthers(irc, m, msg.nick) def doTopic(self, irc, msg): irc = self._getRealIrc(irc) (channel, newTopic) = msg.args if channel not in self.registryValue('channels'): return network = self._getIrcName(irc) if self.registryValue('topicSync', channel): m = ircmsgs.topic(channel, newTopic) for otherIrc in world.ircs: if irc != otherIrc: try: if otherIrc.state.getTopic(channel) != newTopic: if (otherIrc, newTopic) not in self.queuedTopics: self.queuedTopics.add((otherIrc, newTopic)) otherIrc.queueMsg(m) else: self.queuedTopics.remove((otherIrc, newTopic)) except KeyError: self.log.warning( 'Not on %s on %s, ' 'can\'t sync topics.', channel, otherIrc.network) else: s = format(_('topic change by %s on %s: %s'), msg.nick, network, newTopic) m = self._msgmaker(channel, network, s) self._sendToOthers(irc, m, msg.nick) def doQuit(self, irc, msg): irc = self._getRealIrc(irc) network = self._getIrcName(irc) if msg.args: s = format(_('%s has quit %s (%s)'), msg.nick, network, msg.args[0]) else: s = format(_('%s has quit %s.'), msg.nick, network) for channel in self.registryValue('channels'): if channel not in msg.tagged('channels'): continue m = self._msgmaker(channel, network, s) self._sendToOthers(irc, m, msg.nick) def doError(self, irc, msg): irc = self._getRealIrc(irc) network = self._getIrcName(irc) s = format(_('disconnected from %s: %s'), network, msg.args[0]) for channel in self.registryValue('channels'): m = self._msgmaker(channel, network, s) self._sendToOthers(irc, m, msg.nick) def outFilter(self, irc, msg): irc = self._getRealIrc(irc) if msg.command == 'PRIVMSG': if msg.relayedMsg: self._addRelayMsg(msg) else: if msg.channel in self.registryValue('channels'): network = self._getIrcName(irc) s = self._formatPrivmsg(irc.nick, network, msg) relayMsg = self._msgmaker(msg.args[0], network, s) self._sendToOthers(irc, relayMsg, irc.nick) return msg def _addRelayMsg(self, msg): channel = msg.channel if channel in self.lastRelayMsgs: q = self.lastRelayMsgs[channel] else: q = TimeoutQueue(60) # XXX Make this configurable. self.lastRelayMsgs[channel] = q unformatted = ircutils.stripFormatting(msg.args[1]) normalized = utils.str.normalizeWhitespace(unformatted) q.enqueue(normalized)