1### 2# Copyright (c) 2002-2005 Jeremiah Fincher 3# All rights reserved. 4# 5# Redistribution and use in source and binary forms, with or without 6# modification, are permitted provided that the following conditions are met: 7# 8# * Redistributions of source code must retain the above copyright notice, 9# this list of conditions, and the following disclaimer. 10# * Redistributions in binary form must reproduce the above copyright notice, 11# this list of conditions, and the following disclaimer in the 12# documentation and/or other materials provided with the distribution. 13# * Neither the name of the author of this software nor the name of 14# contributors to this software may be used to endorse or promote products 15# derived from this software without specific prior written consent. 16# 17# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 18# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 19# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 20# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 21# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 22# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 23# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 24# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 25# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 26# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 27# POSSIBILITY OF SUCH DAMAGE. 28### 29 30import re 31import copy 32import time 33import random 34import base64 35import collections 36 37try: 38 import ecdsa 39except ImportError: 40 ecdsa = None 41 42try: 43 import pyxmpp2_scram as scram 44except ImportError: 45 scram = None 46 47from . import conf, ircdb, ircmsgs, ircutils, log, utils, world 48from .utils.str import rsplit 49from .utils.iter import chain 50from .utils.structures import smallqueue, RingBuffer 51 52### 53# The base class for a callback to be registered with an Irc object. Shows 54# the required interface for callbacks -- name(), 55# inFilter(irc, msg), outFilter(irc, msg), and __call__(irc, msg) [used so as 56# to make functions used as callbacks conceivable, and so if refactoring ever 57# changes the nature of the callbacks from classes to functions, syntactical 58# changes elsewhere won't be required.] 59### 60 61class IrcCommandDispatcher(object): 62 """Base class for classes that must dispatch on a command.""" 63 def dispatchCommand(self, command): 64 """Given a string 'command', dispatches to doCommand.""" 65 return getattr(self, 'do' + command.capitalize(), None) 66 67 68class IrcCallback(IrcCommandDispatcher, log.Firewalled): 69 """Base class for standard callbacks. 70 71 Callbacks derived from this class should have methods of the form 72 "doCommand" -- doPrivmsg, doNick, do433, etc. These will be called 73 on matching messages. 74 """ 75 callAfter = () 76 callBefore = () 77 __firewalled__ = {'die': None, 78 'reset': None, 79 '__call__': None, 80 'inFilter': lambda self, irc, msg: msg, 81 'outFilter': lambda self, irc, msg: msg, 82 'name': lambda self: self.__class__.__name__, 83 'callPrecedence': lambda self, irc: ([], []), 84 } 85 86 def __init__(self, *args, **kwargs): 87 #object doesn't take any args, so the buck stops here. 88 #super(IrcCallback, self).__init__(*args, **kwargs) 89 pass 90 91 def __repr__(self): 92 return '<%s %s %s>' % \ 93 (self.__class__.__name__, self.name(), object.__repr__(self)) 94 95 def name(self): 96 """Returns the name of the callback.""" 97 return self.__class__.__name__ 98 99 def callPrecedence(self, irc): 100 """Returns a pair of (callbacks to call before me, 101 callbacks to call after me)""" 102 after = [] 103 before = [] 104 for name in self.callBefore: 105 cb = irc.getCallback(name) 106 if cb is not None: 107 after.append(cb) 108 for name in self.callAfter: 109 cb = irc.getCallback(name) 110 if cb is not None: 111 before.append(cb) 112 assert self not in after, '%s was in its own after.' % self.name() 113 assert self not in before, '%s was in its own before.' % self.name() 114 return (before, after) 115 116 def inFilter(self, irc, msg): 117 """Used for filtering/modifying messages as they're entering. 118 119 ircmsgs.IrcMsg objects are immutable, so this method is expected to 120 return another ircmsgs.IrcMsg object. Obviously the same IrcMsg 121 can be returned. 122 """ 123 return msg 124 125 def outFilter(self, irc, msg): 126 """Used for filtering/modifying messages as they're leaving. 127 128 As with inFilter, an IrcMsg is returned. 129 """ 130 return msg 131 132 def __call__(self, irc, msg): 133 """Used for handling each message.""" 134 method = self.dispatchCommand(msg.command) 135 if method is not None: 136 method(irc, msg) 137 138 def reset(self): 139 """Resets the callback. Called when reconnecting to the server.""" 140 pass 141 142 def die(self): 143 """Makes the callback die. Called when the parent Irc object dies.""" 144 pass 145 146### 147# Basic queue for IRC messages. It doesn't presently (but should at some 148# later point) reorder messages based on priority or penalty calculations. 149### 150_high = frozenset(['MODE', 'KICK', 'PONG', 'NICK', 'PASS', 'CAPAB', 'REMOVE']) 151_low = frozenset(['PRIVMSG', 'PING', 'WHO', 'NOTICE', 'JOIN']) 152class IrcMsgQueue(object): 153 """Class for a queue of IrcMsgs. Eventually, it should be smart. 154 155 Probably smarter than it is now, though it's gotten quite a bit smarter 156 than it originally was. A method to "score" methods, and a heapq to 157 maintain a priority queue of the messages would be the ideal way to do 158 intelligent queuing. 159 160 As it stands, however, we simply keep track of 'high priority' messages, 161 'low priority' messages, and normal messages, and just make sure to return 162 the 'high priority' ones before the normal ones before the 'low priority' 163 ones. 164 """ 165 __slots__ = ('msgs', 'highpriority', 'normal', 'lowpriority', 'lastJoin') 166 def __init__(self, iterable=()): 167 self.reset() 168 for msg in iterable: 169 self.enqueue(msg) 170 171 def reset(self): 172 """Clears the queue.""" 173 self.lastJoin = 0 174 self.highpriority = smallqueue() 175 self.normal = smallqueue() 176 self.lowpriority = smallqueue() 177 178 def enqueue(self, msg): 179 """Enqueues a given message.""" 180 if msg in self and \ 181 conf.supybot.protocols.irc.queuing.duplicates(): 182 s = str(msg).strip() 183 log.info('Not adding message %q to queue, already added.', s) 184 return False 185 else: 186 if msg.command in _high: 187 self.highpriority.enqueue(msg) 188 elif msg.command in _low: 189 self.lowpriority.enqueue(msg) 190 else: 191 self.normal.enqueue(msg) 192 return True 193 194 def dequeue(self): 195 """Dequeues a given message.""" 196 msg = None 197 if self.highpriority: 198 msg = self.highpriority.dequeue() 199 elif self.normal: 200 msg = self.normal.dequeue() 201 elif self.lowpriority: 202 msg = self.lowpriority.dequeue() 203 if msg.command == 'JOIN': 204 limit = conf.supybot.protocols.irc.queuing.rateLimit.join() 205 now = time.time() 206 if self.lastJoin + limit <= now: 207 self.lastJoin = now 208 else: 209 self.lowpriority.enqueue(msg) 210 msg = None 211 return msg 212 213 def __contains__(self, msg): 214 return msg in self.normal or \ 215 msg in self.lowpriority or \ 216 msg in self.highpriority 217 218 def __bool__(self): 219 return bool(self.highpriority or self.normal or self.lowpriority) 220 __nonzero__ = __bool__ 221 222 def __len__(self): 223 return len(self.highpriority)+len(self.lowpriority)+len(self.normal) 224 225 def __repr__(self): 226 name = self.__class__.__name__ 227 return '%s(%r)' % (name, list(chain(self.highpriority, 228 self.normal, 229 self.lowpriority))) 230 __str__ = __repr__ 231 232 233### 234# Maintains the state of IRC connection -- the most recent messages, the 235# status of various modes (especially ops/halfops/voices) in channels, etc. 236### 237class ChannelState(utils.python.Object): 238 __slots__ = ('users', 'ops', 'halfops', 'bans', 239 'voices', 'topic', 'modes', 'created') 240 def __init__(self): 241 self.topic = '' 242 self.created = 0 243 self.ops = ircutils.IrcSet() 244 self.bans = ircutils.IrcSet() 245 self.users = ircutils.IrcSet() 246 self.voices = ircutils.IrcSet() 247 self.halfops = ircutils.IrcSet() 248 self.modes = {} 249 250 def isOp(self, nick): 251 return nick in self.ops 252 def isOpPlus(self, nick): 253 return nick in self.ops 254 def isVoice(self, nick): 255 return nick in self.voices 256 def isVoicePlus(self, nick): 257 return nick in self.voices or nick in self.halfops or nick in self.ops 258 def isHalfop(self, nick): 259 return nick in self.halfops 260 def isHalfopPlus(self, nick): 261 return nick in self.halfops or nick in self.ops 262 263 def addUser(self, user): 264 "Adds a given user to the ChannelState. Power prefixes are handled." 265 nick = user.lstrip('@%+&~!') 266 if not nick: 267 return 268 # & is used to denote protected users in UnrealIRCd 269 # ~ is used to denote channel owner in UnrealIRCd 270 # ! is used to denote protected users in UltimateIRCd 271 while user and user[0] in '@%+&~!': 272 (marker, user) = (user[0], user[1:]) 273 assert user, 'Looks like my caller is passing chars, not nicks.' 274 if marker in '@&~!': 275 self.ops.add(nick) 276 elif marker == '%': 277 self.halfops.add(nick) 278 elif marker == '+': 279 self.voices.add(nick) 280 self.users.add(nick) 281 282 def replaceUser(self, oldNick, newNick): 283 """Changes the user oldNick to newNick; used for NICK changes.""" 284 # Note that this doesn't have to have the sigil (@%+) that users 285 # have to have for addUser; it just changes the name of the user 286 # without changing any of their categories. 287 for s in (self.users, self.ops, self.halfops, self.voices): 288 if oldNick in s: 289 s.remove(oldNick) 290 s.add(newNick) 291 292 def removeUser(self, user): 293 """Removes a given user from the channel.""" 294 self.users.discard(user) 295 self.ops.discard(user) 296 self.halfops.discard(user) 297 self.voices.discard(user) 298 299 def setMode(self, mode, value=None): 300 assert mode not in 'ovhbeq' 301 self.modes[mode] = value 302 303 def unsetMode(self, mode): 304 assert mode not in 'ovhbeq' 305 if mode in self.modes: 306 del self.modes[mode] 307 308 def doMode(self, msg): 309 def getSet(c): 310 if c == 'o': 311 Set = self.ops 312 elif c == 'v': 313 Set = self.voices 314 elif c == 'h': 315 Set = self.halfops 316 elif c == 'b': 317 Set = self.bans 318 else: # We don't care yet, so we'll just return an empty set. 319 Set = set() 320 return Set 321 for (mode, value) in ircutils.separateModes(msg.args[1:]): 322 (action, modeChar) = mode 323 if modeChar in 'ovhbeq': # We don't handle e or q yet. 324 Set = getSet(modeChar) 325 if action == '-': 326 Set.discard(value) 327 elif action == '+': 328 Set.add(value) 329 else: 330 if action == '+': 331 self.setMode(modeChar, value) 332 else: 333 assert action == '-' 334 self.unsetMode(modeChar) 335 336 def __getstate__(self): 337 return [getattr(self, name) for name in self.__slots__] 338 339 def __setstate__(self, t): 340 for (name, value) in zip(self.__slots__, t): 341 setattr(self, name, value) 342 343 def __eq__(self, other): 344 ret = True 345 for name in self.__slots__: 346 ret = ret and getattr(self, name) == getattr(other, name) 347 return ret 348 349Batch = collections.namedtuple('Batch', 'type arguments messages') 350 351class IrcState(IrcCommandDispatcher, log.Firewalled): 352 """Maintains state of the Irc connection. Should also become smarter. 353 """ 354 __firewalled__ = {'addMsg': None} 355 def __init__(self, history=None, supported=None, 356 nicksToHostmasks=None, channels=None, 357 capabilities_ack=None, capabilities_nak=None, 358 capabilities_ls=None): 359 if history is None: 360 history = RingBuffer(conf.supybot.protocols.irc.maxHistoryLength()) 361 if supported is None: 362 supported = utils.InsensitivePreservingDict() 363 if nicksToHostmasks is None: 364 nicksToHostmasks = ircutils.IrcDict() 365 if channels is None: 366 channels = ircutils.IrcDict() 367 self.capabilities_ack = capabilities_ack or set() 368 self.capabilities_nak = capabilities_nak or set() 369 self.capabilities_ls = capabilities_ls or {} 370 self.ircd = None 371 self.supported = supported 372 self.history = history 373 self.channels = channels 374 self.nicksToHostmasks = nicksToHostmasks 375 self.batches = {} 376 377 def reset(self): 378 """Resets the state to normal, unconnected state.""" 379 self.history.reset() 380 self.channels.clear() 381 self.supported.clear() 382 self.nicksToHostmasks.clear() 383 self.history.resize(conf.supybot.protocols.irc.maxHistoryLength()) 384 self.batches = {} 385 386 def __reduce__(self): 387 return (self.__class__, (self.history, self.supported, 388 self.nicksToHostmasks, self.channels)) 389 390 def __eq__(self, other): 391 return self.history == other.history and \ 392 self.channels == other.channels and \ 393 self.supported == other.supported and \ 394 self.nicksToHostmasks == other.nicksToHostmasks and \ 395 self.batches == other.batches 396 397 def __ne__(self, other): 398 return not self == other 399 400 def copy(self): 401 ret = self.__class__() 402 ret.history = copy.deepcopy(self.history) 403 ret.nicksToHostmasks = copy.deepcopy(self.nicksToHostmasks) 404 ret.channels = copy.deepcopy(self.channels) 405 ret.batches = copy.deepcopy(self.batches) 406 return ret 407 408 def addMsg(self, irc, msg): 409 """Updates the state based on the irc object and the message.""" 410 self.history.append(msg) 411 if ircutils.isUserHostmask(msg.prefix) and not msg.command == 'NICK': 412 self.nicksToHostmasks[msg.nick] = msg.prefix 413 if 'batch' in msg.server_tags: 414 batch = msg.server_tags['batch'] 415 assert batch in self.batches, \ 416 'Server references undeclared batch %s' % batch 417 self.batches[batch].messages.append(msg) 418 method = self.dispatchCommand(msg.command) 419 if method is not None: 420 method(irc, msg) 421 422 def getTopic(self, channel): 423 """Returns the topic for a given channel.""" 424 return self.channels[channel].topic 425 426 def nickToHostmask(self, nick): 427 """Returns the hostmask for a given nick.""" 428 return self.nicksToHostmasks[nick] 429 430 def do004(self, irc, msg): 431 """Handles parsing the 004 reply 432 433 Supported user and channel modes are cached""" 434 # msg.args = [nick, server, ircd-version, umodes, modes, 435 # modes that require arguments? (non-standard)] 436 self.ircd = msg.args[2] if len(msg.args) >= 3 else msg.args[1] 437 self.supported['umodes'] = frozenset(msg.args[3]) 438 self.supported['chanmodes'] = frozenset(msg.args[4]) 439 440 _005converters = utils.InsensitivePreservingDict({ 441 'modes': int, 442 'keylen': int, 443 'nicklen': int, 444 'userlen': int, 445 'hostlen': int, 446 'kicklen': int, 447 'awaylen': int, 448 'silence': int, 449 'topiclen': int, 450 'channellen': int, 451 'maxtargets': int, 452 'maxnicklen': int, 453 'maxchannels': int, 454 'watch': int, # DynastyNet, EnterTheGame 455 }) 456 def _prefixParser(s): 457 if ')' in s: 458 (left, right) = s.split(')') 459 assert left[0] == '(', 'Odd PREFIX in 005: %s' % s 460 left = left[1:] 461 assert len(left) == len(right), 'Odd PREFIX in 005: %s' % s 462 return dict(list(zip(left, right))) 463 else: 464 return dict(list(zip('ovh', s))) 465 _005converters['prefix'] = _prefixParser 466 del _prefixParser 467 def _maxlistParser(s): 468 modes = '' 469 limits = [] 470 pairs = s.split(',') 471 for pair in pairs: 472 (mode, limit) = pair.split(':', 1) 473 modes += mode 474 limits += (int(limit),) * len(mode) 475 return dict(list(zip(modes, limits))) 476 _005converters['maxlist'] = _maxlistParser 477 del _maxlistParser 478 def _maxbansParser(s): 479 # IRCd using a MAXLIST style string (IRCNet) 480 if ':' in s: 481 modes = '' 482 limits = [] 483 pairs = s.split(',') 484 for pair in pairs: 485 (mode, limit) = pair.split(':', 1) 486 modes += mode 487 limits += (int(limit),) * len(mode) 488 d = dict(list(zip(modes, limits))) 489 assert 'b' in d 490 return d['b'] 491 else: 492 return int(s) 493 _005converters['maxbans'] = _maxbansParser 494 del _maxbansParser 495 def do005(self, irc, msg): 496 for arg in msg.args[1:-1]: # 0 is nick, -1 is "are supported" 497 if '=' in arg: 498 (name, value) = arg.split('=', 1) 499 converter = self._005converters.get(name, lambda x: x) 500 try: 501 self.supported[name] = converter(value) 502 except Exception: 503 log.exception('Uncaught exception in 005 converter:') 504 log.error('Name: %s, Converter: %s', name, converter) 505 else: 506 self.supported[arg] = None 507 508 def do352(self, irc, msg): 509 # WHO reply. 510 511 (nick, user, host) = (msg.args[5], msg.args[2], msg.args[3]) 512 hostmask = '%s!%s@%s' % (nick, user, host) 513 self.nicksToHostmasks[nick] = hostmask 514 515 def do354(self, irc, msg): 516 # WHOX reply. 517 518 if len(msg.args) != 9 or msg.args[1] != '1': 519 return 520 # irc.nick 1 user ip host nick status account gecos 521 (n, t, user, ip, host, nick, status, account, gecos) = msg.args 522 hostmask = '%s!%s@%s' % (nick, user, host) 523 self.nicksToHostmasks[nick] = hostmask 524 525 def do353(self, irc, msg): 526 # NAMES reply. 527 (__, type, channel, items) = msg.args 528 if channel not in self.channels: 529 self.channels[channel] = ChannelState() 530 c = self.channels[channel] 531 for item in items.split(): 532 if ircutils.isUserHostmask(item): 533 name = ircutils.nickFromHostmask(item) 534 self.nicksToHostmasks[name] = name 535 else: 536 name = item 537 c.addUser(name) 538 if type == '@': 539 c.modes['s'] = None 540 541 def doChghost(self, irc, msg): 542 (user, host) = msg.args 543 nick = msg.nick 544 hostmask = '%s!%s@%s' % (nick, user, host) 545 self.nicksToHostmasks[nick] = hostmask 546 547 def doJoin(self, irc, msg): 548 for channel in msg.args[0].split(','): 549 if channel in self.channels: 550 self.channels[channel].addUser(msg.nick) 551 elif msg.nick: # It must be us. 552 chan = ChannelState() 553 chan.addUser(msg.nick) 554 self.channels[channel] = chan 555 # I don't know why this assert was here. 556 #assert msg.nick == irc.nick, msg 557 558 def do367(self, irc, msg): 559 # Example: 560 # :server 367 user #chan some!random@user evil!channel@op 1356276459 561 try: 562 state = self.channels[msg.args[1]] 563 except KeyError: 564 # We have been kicked of the channel before the server replied to 565 # the MODE +b command. 566 pass 567 else: 568 state.bans.add(msg.args[2]) 569 570 def doMode(self, irc, msg): 571 channel = msg.args[0] 572 if irc.isChannel(channel): # There can be user modes, as well. 573 try: 574 chan = self.channels[channel] 575 except KeyError: 576 chan = ChannelState() 577 self.channels[channel] = chan 578 chan.doMode(msg) 579 580 def do324(self, irc, msg): 581 channel = msg.args[1] 582 try: 583 chan = self.channels[channel] 584 except KeyError: 585 chan = ChannelState() 586 self.channels[channel] = chan 587 for (mode, value) in ircutils.separateModes(msg.args[2:]): 588 modeChar = mode[1] 589 if mode[0] == '+' and mode[1] not in 'ovh': 590 chan.setMode(modeChar, value) 591 elif mode[0] == '-' and mode[1] not in 'ovh': 592 chan.unsetMode(modeChar) 593 594 def do329(self, irc, msg): 595 # This is the last part of an empty mode. 596 channel = msg.args[1] 597 try: 598 chan = self.channels[channel] 599 except KeyError: 600 chan = ChannelState() 601 self.channels[channel] = chan 602 chan.created = int(msg.args[2]) 603 604 def doPart(self, irc, msg): 605 for channel in msg.args[0].split(','): 606 try: 607 chan = self.channels[channel] 608 except KeyError: 609 continue 610 if ircutils.strEqual(msg.nick, irc.nick): 611 del self.channels[channel] 612 else: 613 chan.removeUser(msg.nick) 614 615 def doKick(self, irc, msg): 616 (channel, users) = msg.args[:2] 617 chan = self.channels[channel] 618 for user in users.split(','): 619 if ircutils.strEqual(user, irc.nick): 620 del self.channels[channel] 621 return 622 else: 623 chan.removeUser(user) 624 625 def doQuit(self, irc, msg): 626 channel_names = ircutils.IrcSet() 627 for (name, channel) in self.channels.items(): 628 if msg.nick in channel.users: 629 channel_names.add(name) 630 channel.removeUser(msg.nick) 631 # Remember which channels the user was on 632 msg.tag('channels', channel_names) 633 if msg.nick in self.nicksToHostmasks: 634 # If we're quitting, it may not be. 635 del self.nicksToHostmasks[msg.nick] 636 637 def doTopic(self, irc, msg): 638 if len(msg.args) == 1: 639 return # Empty TOPIC for information. Does not affect state. 640 try: 641 chan = self.channels[msg.args[0]] 642 chan.topic = msg.args[1] 643 except KeyError: 644 pass # We don't have to be in a channel to send a TOPIC. 645 646 def do332(self, irc, msg): 647 chan = self.channels[msg.args[1]] 648 chan.topic = msg.args[2] 649 650 def doNick(self, irc, msg): 651 newNick = msg.args[0] 652 oldNick = msg.nick 653 try: 654 if msg.user and msg.host: 655 # Nick messages being handed out from the bot itself won't 656 # have the necessary prefix to make a hostmask. 657 newHostmask = ircutils.joinHostmask(newNick,msg.user,msg.host) 658 self.nicksToHostmasks[newNick] = newHostmask 659 del self.nicksToHostmasks[oldNick] 660 except KeyError: 661 pass 662 channel_names = ircutils.IrcSet() 663 for (name, channel) in self.channels.items(): 664 if msg.nick in channel.users: 665 channel_names.add(name) 666 channel.replaceUser(oldNick, newNick) 667 msg.tag('channels', channel_names) 668 669 def doBatch(self, irc, msg): 670 batch_name = msg.args[0][1:] 671 if msg.args[0].startswith('+'): 672 batch_type = msg.args[1] 673 batch_arguments = tuple(msg.args[2:]) 674 self.batches[batch_name] = Batch(type=batch_type, 675 arguments=batch_arguments, messages=[]) 676 elif msg.args[0].startswith('-'): 677 batch = self.batches.pop(batch_name) 678 msg.tag('batch', batch) 679 else: 680 assert False, msg.args[0] 681 682 def doAway(self, irc, msg): 683 channel_names = ircutils.IrcSet() 684 for (name, channel) in self.channels.items(): 685 if msg.nick in channel.users: 686 channel_names.add(name) 687 msg.tag('channels', channel_names) 688 689 690### 691# The basic class for handling a connection to an IRC server. Accepts 692# callbacks of the IrcCallback interface. Public attributes include 'driver', 693# 'queue', and 'state', in addition to the standard nick/user/ident attributes. 694### 695_callbacks = [] 696class Irc(IrcCommandDispatcher, log.Firewalled): 697 """The base class for an IRC connection. 698 699 Handles PING commands already. 700 """ 701 __firewalled__ = {'die': None, 702 'feedMsg': None, 703 'takeMsg': None,} 704 _nickSetters = set(['001', '002', '003', '004', '250', '251', '252', 705 '254', '255', '265', '266', '372', '375', '376', 706 '333', '353', '332', '366', '005']) 707 # We specifically want these callbacks to be common between all Ircs, 708 # that's why we don't do the normal None default with a check. 709 def __init__(self, network, callbacks=_callbacks): 710 self.zombie = False 711 world.ircs.append(self) 712 self.network = network 713 self.startedAt = time.time() 714 self.callbacks = callbacks 715 self.state = IrcState() 716 self.queue = IrcMsgQueue() 717 self.fastqueue = smallqueue() 718 self.driver = None # The driver should set this later. 719 self._setNonResettingVariables() 720 self._queueConnectMessages() 721 self.startedSync = ircutils.IrcDict() 722 self.monitoring = ircutils.IrcDict() 723 724 def isChannel(self, s): 725 """Helper function to check whether a given string is a channel on 726 the network this Irc object is connected to.""" 727 kw = {} 728 if 'chantypes' in self.state.supported: 729 kw['chantypes'] = self.state.supported['chantypes'] 730 if 'channellen' in self.state.supported: 731 kw['channellen'] = self.state.supported['channellen'] 732 return ircutils.isChannel(s, **kw) 733 734 def isNick(self, s): 735 kw = {} 736 if 'nicklen' in self.state.supported: 737 kw['nicklen'] = self.state.supported['nicklen'] 738 return ircutils.isNick(s, **kw) 739 740 # This *isn't* threadsafe! 741 def addCallback(self, callback): 742 """Adds a callback to the callbacks list. 743 744 :param callback: A callback object 745 :type callback: supybot.irclib.IrcCallback 746 """ 747 assert not self.getCallback(callback.name()) 748 self.callbacks.append(callback) 749 # This is the new list we're building, which will be tsorted. 750 cbs = [] 751 # The vertices are self.callbacks itself. Now we make the edges. 752 edges = set() 753 for cb in self.callbacks: 754 (before, after) = cb.callPrecedence(self) 755 assert cb not in after, 'cb was in its own after.' 756 assert cb not in before, 'cb was in its own before.' 757 for otherCb in before: 758 edges.add((otherCb, cb)) 759 for otherCb in after: 760 edges.add((cb, otherCb)) 761 def getFirsts(): 762 firsts = set(self.callbacks) - set(cbs) 763 for (before, after) in edges: 764 firsts.discard(after) 765 return firsts 766 firsts = getFirsts() 767 while firsts: 768 # Then we add these to our list of cbs, and remove all edges that 769 # originate with these cbs. 770 for cb in firsts: 771 cbs.append(cb) 772 edgesToRemove = [] 773 for edge in edges: 774 if edge[0] is cb: 775 edgesToRemove.append(edge) 776 for edge in edgesToRemove: 777 edges.remove(edge) 778 firsts = getFirsts() 779 assert len(cbs) == len(self.callbacks), \ 780 'cbs: %s, self.callbacks: %s' % (cbs, self.callbacks) 781 self.callbacks[:] = cbs 782 783 def getCallback(self, name): 784 """Gets a given callback by name.""" 785 name = name.lower() 786 for callback in self.callbacks: 787 if callback.name().lower() == name: 788 return callback 789 else: 790 return None 791 792 def removeCallback(self, name): 793 """Removes a callback from the callback list.""" 794 name = name.lower() 795 def nameMatches(cb): 796 return cb.name().lower() == name 797 (bad, good) = utils.iter.partition(nameMatches, self.callbacks) 798 self.callbacks[:] = good 799 return bad 800 801 def queueMsg(self, msg): 802 """Queues a message to be sent to the server.""" 803 if not self.zombie: 804 return self.queue.enqueue(msg) 805 else: 806 log.warning('Refusing to queue %r; %s is a zombie.', msg, self) 807 return False 808 809 def sendMsg(self, msg): 810 """Queues a message to be sent to the server *immediately*""" 811 if not self.zombie: 812 self.fastqueue.enqueue(msg) 813 else: 814 log.warning('Refusing to send %r; %s is a zombie.', msg, self) 815 816 def takeMsg(self): 817 """Called by the IrcDriver; takes a message to be sent.""" 818 if not self.callbacks: 819 log.critical('No callbacks in %s.', self) 820 now = time.time() 821 msg = None 822 if self.fastqueue: 823 msg = self.fastqueue.dequeue() 824 elif self.queue: 825 if now-self.lastTake <= conf.supybot.protocols.irc.throttleTime(): 826 log.debug('Irc.takeMsg throttling.') 827 else: 828 self.lastTake = now 829 msg = self.queue.dequeue() 830 elif self.afterConnect and \ 831 conf.supybot.protocols.irc.ping() and \ 832 now > self.lastping + conf.supybot.protocols.irc.ping.interval(): 833 if self.outstandingPing: 834 s = 'Ping sent at %s not replied to.' % \ 835 log.timestamp(self.lastping) 836 log.warning(s) 837 self.feedMsg(ircmsgs.error(s)) 838 self.driver.reconnect() 839 elif not self.zombie: 840 self.lastping = now 841 now = str(int(now)) 842 self.outstandingPing = True 843 self.queueMsg(ircmsgs.ping(now)) 844 if msg: 845 for callback in reversed(self.callbacks): 846 msg = callback.outFilter(self, msg) 847 if msg is None: 848 log.debug('%s.outFilter returned None.', callback.name()) 849 return self.takeMsg() 850 world.debugFlush() 851 if len(str(msg)) > 512: 852 # Yes, this violates the contract, but at this point it doesn't 853 # matter. That's why we gotta go munging in private attributes 854 # 855 # I'm changing this to a log.debug to fix a possible loop in 856 # the LogToIrc plugin. Since users can't do anything about 857 # this issue, there's no fundamental reason to make it a 858 # warning. 859 log.debug('Truncating %r, message is too long.', msg) 860 msg._str = msg._str[:500] + '\r\n' 861 msg._len = len(str(msg)) 862 # I don't think we should do this. Why should it matter? If it's 863 # something important, then the server will send it back to us, 864 # and if it's just a privmsg/notice/etc., we don't care. 865 # On second thought, we need this for testing. 866 if world.testing: 867 self.state.addMsg(self, msg) 868 log.debug('Outgoing message (%s): %s', self.network, str(msg).rstrip('\r\n')) 869 return msg 870 elif self.zombie: 871 # We kill the driver here so it doesn't continue to try to 872 # take messages from us. 873 self.driver.die() 874 self._reallyDie() 875 else: 876 return None 877 878 _numericErrorCommandRe = re.compile(r'^[45][0-9][0-9]$') 879 def feedMsg(self, msg): 880 """Called by the IrcDriver; feeds a message received.""" 881 msg.tag('receivedBy', self) 882 msg.tag('receivedOn', self.network) 883 msg.tag('receivedAt', time.time()) 884 if msg.args and self.isChannel(msg.args[0]): 885 channel = msg.args[0] 886 else: 887 channel = None 888 preInFilter = str(msg).rstrip('\r\n') 889 log.debug('Incoming message (%s): %s', self.network, preInFilter) 890 891 # Yeah, so this is odd. Some networks (oftc) seem to give us certain 892 # messages with our nick instead of our prefix. We'll fix that here. 893 if msg.prefix == self.nick: 894 log.debug('Got one of those odd nick-instead-of-prefix msgs.') 895 msg = ircmsgs.IrcMsg(prefix=self.prefix, msg=msg) 896 897 # This catches cases where we know our own nick (from sending it to the 898 # server) but we don't yet know our prefix. 899 if msg.nick == self.nick and self.prefix != msg.prefix: 900 self.prefix = msg.prefix 901 902 # This keeps our nick and server attributes updated. 903 if msg.command in self._nickSetters: 904 if msg.args[0] != self.nick: 905 self.nick = msg.args[0] 906 log.debug('Updating nick attribute to %s.', self.nick) 907 if msg.prefix != self.server: 908 self.server = msg.prefix 909 log.debug('Updating server attribute to %s.', self.server) 910 911 # Dispatch to specific handlers for commands. 912 method = self.dispatchCommand(msg.command) 913 if method is not None: 914 method(msg) 915 elif self._numericErrorCommandRe.search(msg.command): 916 log.error('Unhandled error message from server: %r' % msg) 917 918 # Now update the IrcState object. 919 try: 920 self.state.addMsg(self, msg) 921 except: 922 log.exception('Exception in update of IrcState object:') 923 924 # Now call the callbacks. 925 world.debugFlush() 926 for callback in self.callbacks: 927 try: 928 m = callback.inFilter(self, msg) 929 if not m: 930 log.debug('%s.inFilter returned None', callback.name()) 931 return 932 msg = m 933 except: 934 log.exception('Uncaught exception in inFilter:') 935 world.debugFlush() 936 postInFilter = str(msg).rstrip('\r\n') 937 if postInFilter != preInFilter: 938 log.debug('Incoming message (post-inFilter): %s', postInFilter) 939 for callback in self.callbacks: 940 try: 941 if callback is not None: 942 callback(self, msg) 943 except: 944 log.exception('Uncaught exception in callback:') 945 world.debugFlush() 946 947 def die(self): 948 """Makes the Irc object *promise* to die -- but it won't die (of its 949 own volition) until all its queues are clear. Isn't that cool?""" 950 self.zombie = True 951 if not self.afterConnect: 952 self._reallyDie() 953 954 # This is useless because it's in world.ircs, so it won't be deleted until 955 # the program exits. Just figured you might want to know. 956 #def __del__(self): 957 # self._reallyDie() 958 959 def reset(self): 960 """Resets the Irc object. Called when the driver reconnects.""" 961 self._setNonResettingVariables() 962 self.state.reset() 963 self.queue.reset() 964 self.fastqueue.reset() 965 self.startedSync.clear() 966 for callback in self.callbacks: 967 callback.reset() 968 self._queueConnectMessages() 969 970 def _setNonResettingVariables(self): 971 # Configuration stuff. 972 network_config = conf.supybot.networks.get(self.network) 973 def get_value(name): 974 return getattr(network_config, name)() or \ 975 getattr(conf.supybot, name)() 976 self.nick = get_value('nick') 977 # Expand variables like $version in realname. 978 self.user = ircutils.standardSubstitute(self, None, get_value('user')) 979 self.ident = get_value('ident') 980 self.alternateNicks = conf.supybot.nick.alternates()[:] 981 self.triedNicks = ircutils.IrcSet() 982 self.password = network_config.password() 983 self.prefix = '%s!%s@%s' % (self.nick, self.ident, 'unset.domain') 984 # The rest. 985 self.lastTake = 0 986 self.server = 'unset' 987 self.afterConnect = False 988 self.startedAt = time.time() 989 self.lastping = time.time() 990 self.outstandingPing = False 991 self.capNegociationEnded = False 992 self.requireStarttls = not network_config.ssl() and \ 993 network_config.requireStarttls() 994 if self.requireStarttls: 995 log.error(('STARTTLS is no longer supported. Set ' 996 'supybot.networks.%s.requireStarttls to False ' 997 'to disable it, and use supybot.networks.%s.ssl ' 998 'instead.') % (self.network, self.network)) 999 self.driver.die() 1000 self._reallyDie() 1001 return 1002 self.resetSasl() 1003 1004 def resetSasl(self): 1005 network_config = conf.supybot.networks.get(self.network) 1006 self.sasl_authenticated = False 1007 self.sasl_username = network_config.sasl.username() 1008 self.sasl_password = network_config.sasl.password() 1009 self.sasl_ecdsa_key = network_config.sasl.ecdsa_key() 1010 self.sasl_scram_state = {'step': 'uninitialized'} 1011 self.authenticate_decoder = None 1012 self.sasl_next_mechanisms = [] 1013 self.sasl_current_mechanism = None 1014 1015 for mechanism in network_config.sasl.mechanisms(): 1016 if mechanism == 'ecdsa-nist256p-challenge' and \ 1017 ecdsa and self.sasl_username and self.sasl_ecdsa_key: 1018 self.sasl_next_mechanisms.append(mechanism) 1019 elif mechanism == 'external' and ( 1020 network_config.certfile() or 1021 conf.supybot.protocols.irc.certfile()): 1022 self.sasl_next_mechanisms.append(mechanism) 1023 elif mechanism.startswith('scram-') and scram and \ 1024 self.sasl_username and self.sasl_password: 1025 self.sasl_next_mechanisms.append(mechanism) 1026 elif mechanism == 'plain' and \ 1027 self.sasl_username and self.sasl_password: 1028 self.sasl_next_mechanisms.append(mechanism) 1029 1030 if self.sasl_next_mechanisms: 1031 self.REQUEST_CAPABILITIES.add('sasl') 1032 1033 1034 REQUEST_CAPABILITIES = set(['account-notify', 'extended-join', 1035 'multi-prefix', 'metadata-notify', 'account-tag', 1036 'userhost-in-names', 'invite-notify', 'server-time', 1037 'chghost', 'batch', 'away-notify', 'message-tags']) 1038 1039 def _queueConnectMessages(self): 1040 if self.zombie: 1041 self.driver.die() 1042 self._reallyDie() 1043 1044 return 1045 1046 self.sendMsg(ircmsgs.IrcMsg(command='CAP', args=('LS', '302'))) 1047 1048 self.sendAuthenticationMessages() 1049 1050 def sendAuthenticationMessages(self): 1051 # Notes: 1052 # * using sendMsg instead of queueMsg because these messages cannot 1053 # be throttled. 1054 1055 if self.password: 1056 log.info('%s: Queuing PASS command, not logging the password.', 1057 self.network) 1058 self.sendMsg(ircmsgs.password(self.password)) 1059 1060 log.debug('%s: Sending NICK command, nick is %s.', 1061 self.network, self.nick) 1062 1063 self.sendMsg(ircmsgs.nick(self.nick)) 1064 1065 log.debug('%s: Sending USER command, ident is %s, user is %s.', 1066 self.network, self.ident, self.user) 1067 1068 self.sendMsg(ircmsgs.user(self.ident, self.user)) 1069 1070 def endCapabilityNegociation(self): 1071 if not self.capNegociationEnded: 1072 self.capNegociationEnded = True 1073 self.sendMsg(ircmsgs.IrcMsg(command='CAP', args=('END',))) 1074 1075 def sendSaslString(self, string): 1076 for chunk in ircutils.authenticate_generator(string): 1077 self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', 1078 args=(chunk,))) 1079 1080 def tryNextSaslMechanism(self): 1081 if self.sasl_next_mechanisms: 1082 self.sasl_current_mechanism = self.sasl_next_mechanisms.pop(0) 1083 self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', 1084 args=(self.sasl_current_mechanism.upper(),))) 1085 elif conf.supybot.networks.get(self.network).sasl.required(): 1086 log.error('None of the configured SASL mechanisms succeeded, ' 1087 'aborting connection.') 1088 else: 1089 self.sasl_current_mechanism = None 1090 self.endCapabilityNegociation() 1091 1092 def filterSaslMechanisms(self, available): 1093 available = set(map(str.lower, available)) 1094 self.sasl_next_mechanisms = [ 1095 x for x in self.sasl_next_mechanisms 1096 if x.lower() in available] 1097 1098 def doAuthenticate(self, msg): 1099 if not self.authenticate_decoder: 1100 self.authenticate_decoder = ircutils.AuthenticateDecoder() 1101 self.authenticate_decoder.feed(msg) 1102 if not self.authenticate_decoder.ready: 1103 return # Waiting for other messages 1104 string = self.authenticate_decoder.get() 1105 self.authenticate_decoder = None 1106 1107 mechanism = self.sasl_current_mechanism 1108 if mechanism == 'ecdsa-nist256p-challenge': 1109 self.doAuthenticateEcdsa(string) 1110 elif mechanism == 'external': 1111 self.sendSaslString(b'') 1112 elif mechanism.startswith('scram-'): 1113 step = self.sasl_scram_state['step'] 1114 try: 1115 if step == 'uninitialized': 1116 log.debug('%s: starting SCRAM.', 1117 self.network) 1118 self.doAuthenticateScramFirst(mechanism) 1119 elif step == 'first-sent': 1120 log.debug('%s: received SCRAM challenge.', 1121 self.network) 1122 self.doAuthenticateScramChallenge(string) 1123 elif step == 'final-sent': 1124 log.debug('%s: finishing SCRAM.', 1125 self.network) 1126 self.doAuthenticateScramFinish(string) 1127 else: 1128 assert False 1129 except scram.ScramException: 1130 self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', 1131 args=('*',))) 1132 self.tryNextSaslMechanism() 1133 elif mechanism == 'plain': 1134 authstring = b'\0'.join([ 1135 self.sasl_username.encode('utf-8'), 1136 self.sasl_username.encode('utf-8'), 1137 self.sasl_password.encode('utf-8'), 1138 ]) 1139 self.sendSaslString(authstring) 1140 1141 def doAuthenticateEcdsa(self, string): 1142 if string == b'': 1143 self.sendSaslString(self.sasl_username.encode('utf-8')) 1144 return 1145 try: 1146 with open(self.sasl_ecdsa_key) as fd: 1147 private_key = ecdsa.SigningKey.from_pem(fd.read()) 1148 authstring = private_key.sign(string) 1149 self.sendSaslString(authstring) 1150 except (ecdsa.BadDigestError, OSError, ValueError): 1151 self.sendMsg(ircmsgs.IrcMsg(command='AUTHENTICATE', 1152 args=('*',))) 1153 self.tryNextSaslMechanism() 1154 1155 def doAuthenticateScramFirst(self, mechanism): 1156 """Handle sending the client-first message of SCRAM auth.""" 1157 hash_name = mechanism[len('scram-'):] 1158 if hash_name.endswith('-plus'): 1159 hash_name = hash_name[:-len('-plus')] 1160 hash_name = hash_name.upper() 1161 if hash_name not in scram.HASH_FACTORIES: 1162 log.debug('%s: SCRAM hash %r not supported, aborting.', 1163 self.network, hash_name) 1164 self.tryNextSaslMechanism() 1165 return 1166 authenticator = scram.SCRAMClientAuthenticator(hash_name, 1167 channel_binding=False) 1168 self.sasl_scram_state['authenticator'] = authenticator 1169 client_first = authenticator.start({ 1170 'username': self.sasl_username, 1171 'password': self.sasl_password, 1172 }) 1173 self.sendSaslString(client_first) 1174 self.sasl_scram_state['step'] = 'first-sent' 1175 1176 def doAuthenticateScramChallenge(self, challenge): 1177 client_final = self.sasl_scram_state['authenticator'] \ 1178 .challenge(challenge) 1179 self.sendSaslString(client_final) 1180 self.sasl_scram_state['step'] = 'final-sent' 1181 1182 def doAuthenticateScramFinish(self, data): 1183 try: 1184 res = self.sasl_scram_state['authenticator'] \ 1185 .finish(data) 1186 except scram.BadSuccessException as e: 1187 log.warning('%s: SASL authentication failed with SCRAM error: %e', 1188 self.network, e) 1189 self.tryNextSaslMechanism() 1190 else: 1191 self.sendSaslString(b'') 1192 self.sasl_scram_state['step'] = 'authenticated' 1193 1194 def do903(self, msg): 1195 log.info('%s: SASL authentication successful', self.network) 1196 self.sasl_authenticated = True 1197 self.endCapabilityNegociation() 1198 1199 def do904(self, msg): 1200 log.warning('%s: SASL authentication failed', self.network) 1201 self.tryNextSaslMechanism() 1202 1203 def do905(self, msg): 1204 log.warning('%s: SASL authentication failed because the username or ' 1205 'password is too long.', self.network) 1206 self.tryNextSaslMechanism() 1207 1208 def do906(self, msg): 1209 log.warning('%s: SASL authentication aborted', self.network) 1210 self.tryNextSaslMechanism() 1211 1212 def do907(self, msg): 1213 log.warning('%s: Attempted SASL authentication when we were already ' 1214 'authenticated.', self.network) 1215 self.tryNextSaslMechanism() 1216 1217 def do908(self, msg): 1218 log.info('%s: Supported SASL mechanisms: %s', 1219 self.network, msg.args[1]) 1220 self.filterSaslMechanisms(set(msg.args[1].split(','))) 1221 1222 def doCap(self, msg): 1223 subcommand = msg.args[1] 1224 if subcommand == 'ACK': 1225 self.doCapAck(msg) 1226 elif subcommand == 'NAK': 1227 self.doCapNak(msg) 1228 elif subcommand == 'LS': 1229 self.doCapLs(msg) 1230 elif subcommand == 'DEL': 1231 self.doCapDel(msg) 1232 elif subcommand == 'NEW': 1233 self.doCapNew(msg) 1234 def doCapAck(self, msg): 1235 if len(msg.args) != 3: 1236 log.warning('Bad CAP ACK from server: %r', msg) 1237 return 1238 caps = msg.args[2].split() 1239 assert caps, 'Empty list of capabilities' 1240 log.debug('%s: Server acknowledged capabilities: %L', 1241 self.network, caps) 1242 self.state.capabilities_ack.update(caps) 1243 1244 if 'sasl' in caps: 1245 self.tryNextSaslMechanism() 1246 else: 1247 self.endCapabilityNegociation() 1248 def doCapNak(self, msg): 1249 if len(msg.args) != 3: 1250 log.warning('Bad CAP NAK from server: %r', msg) 1251 return 1252 caps = msg.args[2].split() 1253 assert caps, 'Empty list of capabilities' 1254 self.state.capabilities_nak.update(caps) 1255 log.warning('%s: Server refused capabilities: %L', 1256 self.network, caps) 1257 self.endCapabilityNegociation() 1258 def _addCapabilities(self, capstring): 1259 for item in capstring.split(): 1260 while item.startswith(('=', '~')): 1261 item = item[1:] 1262 if '=' in item: 1263 (cap, value) = item.split('=', 1) 1264 self.state.capabilities_ls[cap] = value 1265 else: 1266 self.state.capabilities_ls[item] = None 1267 def doCapLs(self, msg): 1268 if len(msg.args) == 4: 1269 # Multi-line LS 1270 if msg.args[2] != '*': 1271 log.warning('Bad CAP LS from server: %r', msg) 1272 return 1273 self._addCapabilities(msg.args[3]) 1274 elif len(msg.args) == 3: # End of LS 1275 self._addCapabilities(msg.args[2]) 1276 common_supported_capabilities = set(self.state.capabilities_ls) & \ 1277 self.REQUEST_CAPABILITIES 1278 if 'sasl' in self.state.capabilities_ls: 1279 s = self.state.capabilities_ls['sasl'] 1280 if s is not None: 1281 self.filterSaslMechanisms(set(s.split(','))) 1282 # NOTE: Capabilities are requested in alphabetic order, because 1283 # sets are unordered, and their "order" is nondeterministic. 1284 # This is needed for the tests. 1285 if common_supported_capabilities: 1286 caps = ' '.join(sorted(common_supported_capabilities)) 1287 self.sendMsg(ircmsgs.IrcMsg(command='CAP', 1288 args=('REQ', caps))) 1289 else: 1290 self.endCapabilityNegociation() 1291 else: 1292 log.warning('Bad CAP LS from server: %r', msg) 1293 return 1294 def doCapDel(self, msg): 1295 if len(msg.args) != 3: 1296 log.warning('Bad CAP DEL from server: %r', msg) 1297 return 1298 caps = msg.args[2].split() 1299 assert caps, 'Empty list of capabilities' 1300 for cap in caps: 1301 # The spec says "If capability negotiation 3.2 was used, extensions 1302 # listed MAY contain values." for CAP NEW and CAP DEL 1303 cap = cap.split('=')[0] 1304 try: 1305 del self.state.capabilities_ls[cap] 1306 except KeyError: 1307 pass 1308 try: 1309 self.state.capabilities_ack.remove(cap) 1310 except KeyError: 1311 pass 1312 def doCapNew(self, msg): 1313 if len(msg.args) != 3: 1314 log.warning('Bad CAP NEW from server: %r', msg) 1315 return 1316 caps = msg.args[2].split() 1317 assert caps, 'Empty list of capabilities' 1318 self._addCapabilities(msg.args[2]) 1319 if not self.sasl_authenticated and 'sasl' in self.state.capabilities_ls: 1320 self.resetSasl() 1321 s = self.state.capabilities_ls['sasl'] 1322 if s is not None: 1323 self.filterSaslMechanisms(set(s.split(','))) 1324 common_supported_unrequested_capabilities = ( 1325 set(self.state.capabilities_ls) & 1326 self.REQUEST_CAPABILITIES - 1327 self.state.capabilities_ack) 1328 if common_supported_unrequested_capabilities: 1329 caps = ' '.join(sorted(common_supported_unrequested_capabilities)) 1330 self.sendMsg(ircmsgs.IrcMsg(command='CAP', 1331 args=('REQ', caps))) 1332 1333 def monitor(self, targets): 1334 """Increment a counter of how many callbacks monitor each target; 1335 and send a MONITOR + to the server if the target is not yet 1336 monitored.""" 1337 if isinstance(targets, str): 1338 targets = [targets] 1339 not_yet_monitored = set() 1340 for target in targets: 1341 if target in self.monitoring: 1342 self.monitoring[target] += 1 1343 else: 1344 not_yet_monitored.add(target) 1345 self.monitoring[target] = 1 1346 if not_yet_monitored: 1347 self.queueMsg(ircmsgs.monitor('+', not_yet_monitored)) 1348 return not_yet_monitored 1349 1350 def unmonitor(self, targets): 1351 """Decrements a counter of how many callbacks monitor each target; 1352 and send a MONITOR - to the server if the counter drops to 0.""" 1353 if isinstance(targets, str): 1354 targets = [targets] 1355 should_be_unmonitored = set() 1356 for target in targets: 1357 self.monitoring[target] -= 1 1358 if self.monitoring[target] == 0: 1359 del self.monitoring[target] 1360 should_be_unmonitored.add(target) 1361 if should_be_unmonitored: 1362 self.queueMsg(ircmsgs.monitor('-', should_be_unmonitored)) 1363 return should_be_unmonitored 1364 1365 def _getNextNick(self): 1366 if self.alternateNicks: 1367 nick = self.alternateNicks.pop(0) 1368 if '%s' in nick: 1369 network_nick = conf.supybot.networks.get(self.network).nick() 1370 if network_nick == '': 1371 nick %= conf.supybot.nick() 1372 else: 1373 nick %= network_nick 1374 if nick not in self.triedNicks: 1375 self.triedNicks.add(nick) 1376 return nick 1377 1378 nick = conf.supybot.nick() 1379 network_nick = conf.supybot.networks.get(self.network).nick() 1380 if network_nick != '': 1381 nick = network_nick 1382 ret = nick 1383 L = list(nick) 1384 while len(L) <= 3: 1385 L.append('`') 1386 while ret in self.triedNicks: 1387 L[random.randrange(len(L))] = utils.iter.choice('0123456789') 1388 ret = ''.join(L) 1389 self.triedNicks.add(ret) 1390 return ret 1391 1392 def do002(self, msg): 1393 """Logs the ircd version.""" 1394 (beginning, version) = rsplit(msg.args[-1], maxsplit=1) 1395 log.info('Server %s has version %s', self.server, version) 1396 1397 def doPing(self, msg): 1398 """Handles PING messages.""" 1399 self.sendMsg(ircmsgs.pong(msg.args[0])) 1400 1401 def doPong(self, msg): 1402 """Handles PONG messages.""" 1403 self.outstandingPing = False 1404 1405 def do376(self, msg): 1406 log.info('Got end of MOTD from %s', self.server) 1407 self.afterConnect = True 1408 # Let's reset nicks in case we had to use a weird one. 1409 self.alternateNicks = conf.supybot.nick.alternates()[:] 1410 umodes = conf.supybot.networks.get(self.network).umodes() 1411 if umodes == '': 1412 umodes = conf.supybot.protocols.irc.umodes() 1413 supported = self.state.supported.get('umodes') 1414 if supported: 1415 acceptedchars = supported.union('+-') 1416 umodes = ''.join([m for m in umodes if m in acceptedchars]) 1417 if umodes: 1418 log.info('Sending user modes to %s: %s', self.network, umodes) 1419 self.sendMsg(ircmsgs.mode(self.nick, umodes)) 1420 do377 = do422 = do376 1421 1422 def do43x(self, msg, problem): 1423 if not self.afterConnect: 1424 newNick = self._getNextNick() 1425 assert newNick != self.nick 1426 log.info('Got %s: %s %s. Trying %s.', 1427 msg.command, self.nick, problem, newNick) 1428 self.sendMsg(ircmsgs.nick(newNick)) 1429 def do437(self, msg): 1430 self.do43x(msg, 'is temporarily unavailable') 1431 def do433(self, msg): 1432 self.do43x(msg, 'is in use') 1433 def do432(self, msg): 1434 self.do43x(msg, 'is not a valid nickname') 1435 1436 def doJoin(self, msg): 1437 if msg.nick == self.nick: 1438 channel = msg.args[0] 1439 self.queueMsg(ircmsgs.who(channel, args=('%tuhnairf,1',))) # Ends with 315. 1440 self.queueMsg(ircmsgs.mode(channel)) # Ends with 329. 1441 for channel in msg.args[0].split(','): 1442 self.queueMsg(ircmsgs.mode(channel, '+b')) 1443 self.startedSync[channel] = time.time() 1444 1445 def do315(self, msg): 1446 channel = msg.args[1] 1447 if channel in self.startedSync: 1448 now = time.time() 1449 started = self.startedSync.pop(channel) 1450 elapsed = now - started 1451 log.info('Join to %s on %s synced in %.2f seconds.', 1452 channel, self.network, elapsed) 1453 1454 def doError(self, msg): 1455 """Handles ERROR messages.""" 1456 log.warning('Error message from %s: %s', self.network, msg.args[0]) 1457 if not self.zombie: 1458 if msg.args[0].lower().startswith('closing link'): 1459 self.driver.reconnect() 1460 elif 'too fast' in msg.args[0]: # Connecting too fast. 1461 self.driver.reconnect(wait=True) 1462 1463 def doNick(self, msg): 1464 """Handles NICK messages.""" 1465 if msg.nick == self.nick: 1466 newNick = msg.args[0] 1467 self.nick = newNick 1468 (nick, user, domain) = ircutils.splitHostmask(msg.prefix) 1469 self.prefix = ircutils.joinHostmask(self.nick, user, domain) 1470 elif conf.supybot.followIdentificationThroughNickChanges(): 1471 # We use elif here because this means it's someone else's nick 1472 # change, not our own. 1473 try: 1474 id = ircdb.users.getUserId(msg.prefix) 1475 u = ircdb.users.getUser(id) 1476 except KeyError: 1477 return 1478 if u.auth: 1479 (_, user, host) = ircutils.splitHostmask(msg.prefix) 1480 newhostmask = ircutils.joinHostmask(msg.args[0], user, host) 1481 for (i, (when, authmask)) in enumerate(u.auth[:]): 1482 if ircutils.strEqual(msg.prefix, authmask): 1483 log.info('Following identification for %s: %s -> %s', 1484 u.name, authmask, newhostmask) 1485 u.auth[i] = (u.auth[i][0], newhostmask) 1486 ircdb.users.setUser(u) 1487 1488 def _reallyDie(self): 1489 """Makes the Irc object die. Dead.""" 1490 log.info('Irc object for %s dying.', self.network) 1491 # XXX This hasattr should be removed, I'm just putting it here because 1492 # we're so close to a release. After 0.80.0 we should remove this 1493 # and fix whatever AttributeErrors arise in the drivers themselves. 1494 if self.driver is not None and hasattr(self.driver, 'die'): 1495 self.driver.die() 1496 if self in world.ircs: 1497 world.ircs.remove(self) 1498 # Only kill the callbacks if we're the last Irc. 1499 if not world.ircs: 1500 for cb in self.callbacks: 1501 cb.die() 1502 # If we shared our list of callbacks, this ensures that 1503 # cb.die() is only called once for each callback. It's 1504 # not really necessary since we already check to make sure 1505 # we're the only Irc object, but a little robustitude never 1506 # hurt anybody. 1507 log.debug('Last Irc, clearing callbacks.') 1508 self.callbacks[:] = [] 1509 else: 1510 log.warning('Irc object killed twice: %s', utils.stackTrace()) 1511 1512 def __hash__(self): 1513 return id(self) 1514 1515 def __eq__(self, other): 1516 # We check isinstance here, so that if some proxy object (like those 1517 # defined in callbacks.py) has overridden __eq__, it takes precedence. 1518 if isinstance(other, self.__class__): 1519 return id(self) == id(other) 1520 else: 1521 return other.__eq__(self) 1522 1523 def __ne__(self, other): 1524 return not (self == other) 1525 1526 def __str__(self): 1527 return 'Irc object for %s' % self.network 1528 1529 def __repr__(self): 1530 return '<irclib.Irc object for %s>' % self.network 1531 1532 1533# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79: 1534