1### 2# Copyright (c) 2002-2004, Jeremiah Fincher 3# Copyright (c) 2010,2015 James McCoy 4# All rights reserved. 5# 6# Redistribution and use in source and binary forms, with or without 7# modification, are permitted provided that the following conditions are met: 8# 9# * Redistributions of source code must retain the above copyright notice, 10# this list of conditions, and the following disclaimer. 11# * Redistributions in binary form must reproduce the above copyright notice, 12# this list of conditions, and the following disclaimer in the 13# documentation and/or other materials provided with the distribution. 14# * Neither the name of the author of this software nor the name of 15# contributors to this software may be used to endorse or promote products 16# derived from this software without specific prior written consent. 17# 18# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 21# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 22# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 23# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 24# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 25# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 26# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 27# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 28# POSSIBILITY OF SUCH DAMAGE. 29### 30 31import time 32 33import supybot.conf as conf 34import supybot.utils as utils 35import supybot.world as world 36from supybot.commands import * 37import supybot.irclib as irclib 38import supybot.ircmsgs as ircmsgs 39import supybot.ircutils as ircutils 40import supybot.callbacks as callbacks 41from supybot.utils.structures import MultiSet, TimeoutQueue 42from supybot.i18n import PluginInternationalization, internationalizeDocstring 43_ = PluginInternationalization('Relay') 44 45class Relay(callbacks.Plugin): 46 """This plugin allows you to setup a relay between networks.""" 47 noIgnore = True 48 def __init__(self, irc): 49 self.__parent = super(Relay, self) 50 self.__parent.__init__(irc) 51 self._whois = {} 52 self.queuedTopics = MultiSet() 53 self.lastRelayMsgs = ircutils.IrcDict() 54 55 def do376(self, irc, msg): 56 networkGroup = conf.supybot.networks.get(irc.network) 57 for channel in self.registryValue('channels'): 58 if self.registryValue('channels.joinOnAllNetworks', channel): 59 if channel not in irc.state.channels: 60 irc.queueMsg(networkGroup.channels.join(channel)) 61 do377 = do422 = do376 62 63 def _getRealIrc(self, irc): 64 if isinstance(irc, irclib.Irc): 65 return irc 66 else: 67 return irc.getRealIrc() 68 69 def _getIrcName(self, irc): 70 # We should allow abbreviations at some point. 71 return irc.network 72 73 @internationalizeDocstring 74 def join(self, irc, msg, args, channel): 75 """[<channel>] 76 77 Starts relaying between the channel <channel> on all networks. If on a 78 network the bot isn't in <channel>, it'll join. This commands is 79 required even if the bot is in the channel on both networks; it won't 80 relay between those channels unless it's told to join both 81 channels. If <channel> is not given, starts relaying on the channel 82 the message was sent in. 83 """ 84 self.registryValue('channels').add(channel) 85 for otherIrc in world.ircs: 86 if channel not in otherIrc.state.channels: 87 networkGroup = conf.supybot.networks.get(otherIrc.network) 88 otherIrc.queueMsg(networkGroup.channels.join(channel)) 89 irc.replySuccess() 90 join = wrap(join, ['channel', 'admin']) 91 92 @internationalizeDocstring 93 def part(self, irc, msg, args, channel): 94 """<channel> 95 96 Ceases relaying between the channel <channel> on all networks. The bot 97 will part from the channel on all networks in which it is on the 98 channel. 99 """ 100 self.registryValue('channels').discard(channel) 101 for otherIrc in world.ircs: 102 if channel in otherIrc.state.channels: 103 otherIrc.queueMsg(ircmsgs.part(channel)) 104 irc.replySuccess() 105 part = wrap(part, ['channel', 'admin']) 106 107 @internationalizeDocstring 108 def nicks(self, irc, msg, args, channel): 109 """[<channel>] 110 111 Returns the nicks of the people in the channel on the various networks 112 the bot is connected to. <channel> is only necessary if the message 113 isn't sent on the channel itself. 114 """ 115 realIrc = self._getRealIrc(irc) 116 if channel not in self.registryValue('channels'): 117 irc.error(format('I\'m not relaying in %s.', channel)) 118 return 119 users = [] 120 for otherIrc in world.ircs: 121 network = self._getIrcName(otherIrc) 122 ops = [] 123 halfops = [] 124 voices = [] 125 usersS = [] 126 if network != self._getIrcName(realIrc): 127 try: 128 Channel = otherIrc.state.channels[channel] 129 except KeyError: 130 users.append(format('(not in %s on %s)',channel,network)) 131 continue 132 numUsers = 0 133 for s in Channel.users: 134 s = s.strip() 135 if not s: 136 continue 137 numUsers += 1 138 if s in Channel.ops: 139 ops.append('@' + s) 140 elif s in Channel.halfops: 141 halfops.append('%' + s) 142 elif s in Channel.voices: 143 voices.append('+' + s) 144 else: 145 usersS.append(s) 146 utils.sortBy(ircutils.toLower, ops) 147 utils.sortBy(ircutils.toLower, voices) 148 utils.sortBy(ircutils.toLower, halfops) 149 utils.sortBy(ircutils.toLower, usersS) 150 usersS = ', '.join(filter(None, list(map(', '.join, 151 (ops,halfops,voices,usersS))))) 152 users.append(format('%s (%i): %s', 153 ircutils.bold(network), numUsers, usersS)) 154 users.sort() 155 irc.reply('; '.join(users)) 156 nicks = wrap(nicks, ['channel']) 157 158 def do311(self, irc, msg): 159 irc = self._getRealIrc(irc) 160 nick = ircutils.toLower(msg.args[1]) 161 if (irc, nick) not in self._whois: 162 return 163 else: 164 self._whois[(irc, nick)][-1][msg.command] = msg 165 166 # These are all sent by a WHOIS response. 167 do301 = do311 168 do312 = do311 169 do317 = do311 170 do319 = do311 171 do320 = do311 172 173 def do318(self, irc, msg): 174 irc = self._getRealIrc(irc) 175 nick = msg.args[1] 176 loweredNick = ircutils.toLower(nick) 177 if (irc, loweredNick) not in self._whois: 178 return 179 (replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)] 180 d['318'] = msg 181 s = ircutils.formatWhois(irc, d, caller=replyMsg.nick, 182 channel=replyMsg.args[0]) 183 replyIrc.reply(s) 184 del self._whois[(irc, loweredNick)] 185 186 def do402(self, irc, msg): 187 irc = self._getRealIrc(irc) 188 nick = msg.args[1] 189 loweredNick = ircutils.toLower(nick) 190 if (irc, loweredNick) not in self._whois: 191 return 192 (replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)] 193 del self._whois[(irc, loweredNick)] 194 s = format(_('There is no %s on %s.'), nick, self._getIrcName(irc)) 195 replyIrc.reply(s) 196 197 do401 = do402 198 199 def _formatPrivmsg(self, nick, network, msg): 200 channel = msg.args[0] 201 if self.registryValue('includeNetwork', channel): 202 network = '@' + network 203 else: 204 network = '' 205 # colorize nicks 206 color = self.registryValue('color', channel) # Also used further down. 207 if color: 208 nick = ircutils.IrcString(nick) 209 newnick = ircutils.mircColor(nick, *ircutils.canonicalColor(nick)) 210 colors = ircutils.canonicalColor(nick, shift=4) 211 nick = newnick 212 if ircmsgs.isAction(msg): 213 if color: 214 t = ircutils.mircColor('*', *colors) 215 else: 216 t = '*' 217 s = format('%s %s%s %s', t, nick, network, ircmsgs.unAction(msg)) 218 else: 219 if color: 220 lt = ircutils.mircColor('<', *colors) 221 gt = ircutils.mircColor('>', *colors) 222 else: 223 lt = '<' 224 gt = '>' 225 s = format('%s%s%s%s %s', lt, nick, network, gt, msg.args[1]) 226 return s 227 228 def _sendToOthers(self, irc, msg): 229 assert msg.command in ('PRIVMSG', 'NOTICE', 'TOPIC') 230 for otherIrc in world.ircs: 231 if otherIrc != irc and not otherIrc.zombie: 232 if msg.args[0] in otherIrc.state.channels: 233 msg.tag('relayedMsg') 234 otherIrc.queueMsg(msg) 235 236 def _checkRelayMsg(self, msg): 237 channel = msg.args[0] 238 if channel in self.lastRelayMsgs: 239 q = self.lastRelayMsgs[channel] 240 unformatted = ircutils.stripFormatting(msg.args[1]) 241 normalized = utils.str.normalizeWhitespace(unformatted) 242 for s in q: 243 if s in normalized: 244 return True 245 return False 246 247 def _punishRelayers(self, msg): 248 assert self._checkRelayMsg(msg), 'Punishing without checking.' 249 who = msg.prefix 250 channel = msg.args[0] 251 def notPunishing(irc, s, *args): 252 self.log.info('Not punishing %s in %s on %s: %s.', 253 msg.prefix, channel, irc.network, s, *args) 254 for irc in world.ircs: 255 if channel in irc.state.channels: 256 if irc.nick in irc.state.channels[channel].ops: 257 if who in irc.state.channels[channel].bans: 258 notPunishing(irc, 'already banned') 259 else: 260 self.log.info('Punishing %s in %s on %s for relaying.', 261 who, channel, irc.network) 262 irc.sendMsg(ircmsgs.ban(channel, who)) 263 kmsg = _('You seem to be relaying, punk.') 264 irc.sendMsg(ircmsgs.kick(channel, msg.nick, kmsg)) 265 else: 266 notPunishing(irc, 'not opped') 267 268 def doPrivmsg(self, irc, msg): 269 if ircmsgs.isCtcp(msg) and not ircmsgs.isAction(msg): 270 return 271 (channel, text) = msg.args 272 if irc.isChannel(channel): 273 irc = self._getRealIrc(irc) 274 if channel not in self.registryValue('channels'): 275 return 276 ignores = self.registryValue('ignores', channel) 277 for ignore in ignores: 278 if ircutils.hostmaskPatternEqual(ignore, msg.prefix): 279 self.log.debug('Refusing to relay %s, ignored by %s.', 280 msg.prefix, ignore) 281 return 282 # Let's try to detect other relay bots. 283 if self._checkRelayMsg(msg): 284 if self.registryValue('punishOtherRelayBots', channel): 285 self._punishRelayers(msg) 286 # Either way, we don't relay the message. 287 else: 288 self.log.warning('Refusing to relay message from %s, ' 289 'it appears to be a relay message.', 290 msg.prefix) 291 else: 292 network = self._getIrcName(irc) 293 s = self._formatPrivmsg(msg.nick, network, msg) 294 m = self._msgmaker(channel, s) 295 self._sendToOthers(irc, m) 296 297 def _msgmaker(self, target, s): 298 msg = dynamic.msg 299 channel = dynamic.channel 300 if self.registryValue('noticeNonPrivmsgs', dynamic.channel) and \ 301 msg.command != 'PRIVMSG': 302 return ircmsgs.notice(target, s) 303 else: 304 return ircmsgs.privmsg(target, s) 305 306 def doJoin(self, irc, msg): 307 irc = self._getRealIrc(irc) 308 channel = msg.args[0] 309 if channel not in self.registryValue('channels'): 310 return 311 network = self._getIrcName(irc) 312 if self.registryValue('hostmasks', channel): 313 hostmask = format(' (%s)', msg.prefix.split('!')[1]) 314 else: 315 hostmask = '' 316 s = format(_('%s%s has joined on %s'), msg.nick, hostmask, network) 317 m = self._msgmaker(channel, s) 318 self._sendToOthers(irc, m) 319 320 def doPart(self, irc, msg): 321 irc = self._getRealIrc(irc) 322 channel = msg.args[0] 323 if channel not in self.registryValue('channels'): 324 return 325 network = self._getIrcName(irc) 326 if self.registryValue('hostmasks', channel): 327 hostmask = format(' (%s)', msg.prefix.split('!')[1]) 328 else: 329 hostmask = '' 330 if len(msg.args) > 1: 331 s = format(_('%s%s has left on %s (%s)'), 332 msg.nick, hostmask, network, msg.args[1]) 333 else: 334 s = format(_('%s%s has left on %s'), msg.nick, hostmask, network) 335 m = self._msgmaker(channel, s) 336 self._sendToOthers(irc, m) 337 338 def doMode(self, irc, msg): 339 irc = self._getRealIrc(irc) 340 channel = msg.args[0] 341 if channel not in self.registryValue('channels'): 342 return 343 network = self._getIrcName(irc) 344 s = format(_('mode change by %s on %s: %s'), 345 msg.nick, network, ' '.join(msg.args[1:])) 346 m = self._msgmaker(channel, s) 347 self._sendToOthers(irc, m) 348 349 def doKick(self, irc, msg): 350 irc = self._getRealIrc(irc) 351 channel = msg.args[0] 352 if channel not in self.registryValue('channels'): 353 return 354 network = self._getIrcName(irc) 355 if len(msg.args) == 3: 356 s = format(_('%s was kicked by %s on %s (%s)'), 357 msg.args[1], msg.nick, network, msg.args[2]) 358 else: 359 s = format(_('%s was kicked by %s on %s'), 360 msg.args[1], msg.nick, network) 361 m = self._msgmaker(channel, s) 362 self._sendToOthers(irc, m) 363 364 def doNick(self, irc, msg): 365 irc = self._getRealIrc(irc) 366 newNick = msg.args[0] 367 network = self._getIrcName(irc) 368 s = format(_('nick change by %s to %s on %s'), msg.nick,newNick,network) 369 for channel in self.registryValue('channels'): 370 m = self._msgmaker(channel, s) 371 self._sendToOthers(irc, m) 372 373 def doTopic(self, irc, msg): 374 irc = self._getRealIrc(irc) 375 (channel, newTopic) = msg.args 376 if channel not in self.registryValue('channels'): 377 return 378 network = self._getIrcName(irc) 379 if self.registryValue('topicSync', channel): 380 m = ircmsgs.topic(channel, newTopic) 381 for otherIrc in world.ircs: 382 if irc != otherIrc: 383 try: 384 if otherIrc.state.getTopic(channel) != newTopic: 385 if (otherIrc, newTopic) not in self.queuedTopics: 386 self.queuedTopics.add((otherIrc, newTopic)) 387 otherIrc.queueMsg(m) 388 else: 389 self.queuedTopics.remove((otherIrc, newTopic)) 390 391 except KeyError: 392 self.log.warning('Not on %s on %s, ' 393 'can\'t sync topics.', 394 channel, otherIrc.network) 395 else: 396 s = format(_('topic change by %s on %s: %s'), 397 msg.nick, network, newTopic) 398 m = self._msgmaker(channel, s) 399 self._sendToOthers(irc, m) 400 401 def doQuit(self, irc, msg): 402 irc = self._getRealIrc(irc) 403 network = self._getIrcName(irc) 404 if msg.args: 405 s = format(_('%s has quit %s (%s)'), msg.nick, network, msg.args[0]) 406 else: 407 s = format(_('%s has quit %s.'), msg.nick, network) 408 for channel in self.registryValue('channels'): 409 m = self._msgmaker(channel, s) 410 self._sendToOthers(irc, m) 411 412 def doError(self, irc, msg): 413 irc = self._getRealIrc(irc) 414 network = self._getIrcName(irc) 415 s = format(_('disconnected from %s: %s'), network, msg.args[0]) 416 for channel in self.registryValue('channels'): 417 m = self._msgmaker(channel, s) 418 self._sendToOthers(irc, m) 419 420 def outFilter(self, irc, msg): 421 irc = self._getRealIrc(irc) 422 if msg.command == 'PRIVMSG': 423 if msg.relayedMsg: 424 self._addRelayMsg(msg) 425 else: 426 channel = msg.args[0] 427 if channel in self.registryValue('channels'): 428 network = self._getIrcName(irc) 429 s = self._formatPrivmsg(irc.nick, network, msg) 430 relayMsg = self._msgmaker(channel, s) 431 self._sendToOthers(irc, relayMsg) 432 return msg 433 434 def _addRelayMsg(self, msg): 435 channel = msg.args[0] 436 if channel in self.lastRelayMsgs: 437 q = self.lastRelayMsgs[channel] 438 else: 439 q = TimeoutQueue(60) # XXX Make this configurable. 440 self.lastRelayMsgs[channel] = q 441 unformatted = ircutils.stripFormatting(msg.args[1]) 442 normalized = utils.str.normalizeWhitespace(unformatted) 443 q.enqueue(normalized) 444 445 446Class = Relay 447 448# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: 449