diff --git a/plugins/Network.py b/plugins/Network.py index 4dbbd41f7..b8b083742 100644 --- a/plugins/Network.py +++ b/plugins/Network.py @@ -30,124 +30,253 @@ ### """ -Various network-related commands. +Includes commands for connecting, disconnecting, and reconnecting to multiple +networks, as well as several other utility functions related to IRC networks. """ +import supybot + __revision__ = "$Id$" +__author__ = supybot.authors.jemfinch import supybot.plugins as plugins -import sets -import socket -import telnetlib +import time +import supybot.conf as conf import supybot.utils as utils +import supybot.world as world +import supybot.ircmsgs as ircmsgs import supybot.ircutils as ircutils import supybot.privmsgs as privmsgs +import supybot.registry as registry import supybot.callbacks as callbacks class Network(callbacks.Privmsg): - threaded = True - def dns(self, irc, msg, args): - """ + _whois = {} + def _getIrc(self, network): + network = network.lower() + for irc in world.ircs: + if irc.network.lower() == network: + return irc + raise callbacks.Error, 'I\'m not currently connected to %s.' % network - Returns the ip of or the reverse DNS hostname of . + def _getNetwork(self, irc, args): + try: + self._getIrc(args[0]) + return args.pop(0) + except (callbacks.Error, IndexError): + return irc.network + + def connect(self, irc, msg, args): + """ [] + + Connects to another network at . If port is not provided, it + defaults to 6667, the default port for IRC. """ - host = privmsgs.getArgs(args) - if utils.isIP(host): - hostname = socket.getfqdn(host) - if hostname == host: - irc.reply('Host not found.') + (network, server) = privmsgs.getArgs(args, optional=1) + try: + otherIrc = self._getIrc(network) + irc.error('I\'m already connected to %s.' % network, Raise=True) + except callbacks.Error: + pass + if server: + if ':' in server: + (server, port) = server.split(':') + port = int(port) else: - irc.reply(hostname) + port = 6667 + serverPort = (server, port) else: try: - ip = socket.gethostbyname(host) - if ip == '64.94.110.11': # Verisign sucks! - irc.reply('Host not found.') - else: - irc.reply(ip) - except socket.error: - irc.reply('Host not found.') + serverPort = conf.supybot.networks.get(network).servers()[0] + except (registry.NonExistentRegistryEntry, IndexError): + irc.error('A server must be provided if the network is not ' + 'already registered.') + return + Owner = irc.getCallback('Owner') + newIrc = Owner._connect(network, serverPort=serverPort) + conf.supybot.networks().add(network) + assert newIrc.callbacks is irc.callbacks, 'callbacks list is different' + irc.replySuccess('Connection to %s initiated.' % network) + connect = privmsgs.checkCapability(connect, 'owner') - _tlds = sets.Set(['com', 'net', 'edu']) - _registrar = ['Sponsoring Registrar', 'Registrar', 'source'] - _updated = ['Last Updated On', 'Domain Last Updated Date', 'Updated Date', - 'Last Modified', 'changed'] - _created = ['Created On', 'Domain Registration Date', 'Creation Date'] - _expires = ['Expiration Date', 'Domain Expiration Date'] - _status = ['Status', 'Domain Status', 'status'] - def whois(self, irc, msg, args): - """ + def disconnect(self, irc, msg, args): + """[] [] - Returns WHOIS information on the registration of . + Disconnects from the network represented by the network . + If is given, quits the network with the given quit + message. is only necessary if the network is different + from the network the command is sent on. """ - domain = privmsgs.getArgs(args) - usertld = domain.split('.')[-1] - if '.' not in domain: - irc.error(' must be in .com, .net, .edu, or .org.') + network = self._getNetwork(irc, args) + quitMsg = privmsgs.getArgs(args, required=0, optional=1) + if not quitMsg: + quitMsg = msg.nick + otherIrc = self._getIrc(network) + # replySuccess here, rather than lower, in case we're being + # told to disconnect from the network we received the command on. + irc.replySuccess() + otherIrc.queueMsg(ircmsgs.quit(quitMsg)) + otherIrc.die() + conf.supybot.networks().discard(network) + disconnect = privmsgs.checkCapability(disconnect, 'owner') + + def reconnect(self, irc, msg, args): + """[] + + Disconnects and then reconnects to . If no network is given, + disconnects and then reconnects to the network the command was given + on. + """ + network = self._getNetwork(irc, args) + badIrc = self._getIrc(network) + try: + badIrc.driver.reconnect() + if badIrc != irc: + # No need to reply if we're reconnecting ourselves. + irc.replySuccess() + except AttributeError: # There's a cleaner way to do this, but I'm lazy. + irc.error('I couldn\'t reconnect. You should restart me instead.') + reconnect = privmsgs.checkCapability(reconnect, 'owner') + + def command(self, irc, msg, args): + """ [ ...] + + Gives the bot (with its associated s) on . + """ + if len(args) < 2: + raise callbacks.ArgumentError + network = args.pop(0) + otherIrc = self._getIrc(network) + Owner = irc.getCallback('Owner') + Owner.disambiguate(irc, args) + self.Proxy(otherIrc, msg, args) + command = privmsgs.checkCapability(command, 'admin') + + ### + # whois command-related stuff. + ### + def do311(self, irc, msg): + nick = ircutils.toLower(msg.args[1]) + if (irc, nick) not in self._whois: return - elif len(domain.split('.')) != 2: - irc.error(' must be a domain, not a hostname.') - return - if usertld in self._tlds: - server = 'rs.internic.net' - search = '=%s' % domain else: - server = '%s.whois-servers.net' % usertld - search = domain - try: - t = telnetlib.Telnet(server, 43) - except socket.error, e: - irc.error(str(e)) + 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): + nick = msg.args[1] + loweredNick = ircutils.toLower(nick) + if (irc, loweredNick) not in self._whois: return - t.write(search) - t.write('\n') - s = t.read_all() - (registrar, updated, created, expires, status) = ('', '', '', '', '') - for line in s.splitlines(): - line = line.strip() - if not line or ':' not in line: - continue - if not registrar and any(line.startswith, self._registrar): - registrar = ':'.join(line.split(':')[1:]).strip() - elif not updated and any(line.startswith, self._updated): - s = ':'.join(line.split(':')[1:]).strip() - updated = 'updated %s' % s - elif not created and any(line.startswith, self._created): - s = ':'.join(line.split(':')[1:]).strip() - created = 'registered %s' % s - elif not expires and any(line.startswith, self._expires): - s = ':'.join(line.split(':')[1:]).strip() - expires = 'expires %s' % s - elif not status and any(line.startswith, self._status): - status = ':'.join(line.split(':')[1:]).strip().lower() - if not status: - status = 'unknown' - try: - t = telnetlib.Telnet('whois.pir.org', 43) - except socket.error, e: - irc.error(str(e)) + (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('is an op on %s' % utils.commaAndify(ops)) + if halfops: + L.append('is a halfop on %s' % utils.commaAndify(halfops)) + if voices: + L.append('is voiced on %s' % utils.commaAndify(voices)) + if normal: + if L: + L.append('is also on %s' % utils.commaAndify(normal)) + else: + L.append('is on %s' % utils.commaAndify(normal)) + else: + L = ['isn\'t on any non-secret channels'] + channels = utils.commaAndify(L) + if '317' in d: + idle = utils.timeElapsed(d['317'].args[2]) + signon = time.strftime(conf.supybot.humanTimestampFormat(), + time.localtime(float(d['317'].args[3]))) + else: + idle = '' + signon = '' + if '312' in d: + server = d['312'].args[2] + else: + server = '' + if '301' in d: + away = ' %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 = '%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): + nick = msg.args[1] + loweredNick = ircutils.toLower(nick) + if (irc, loweredNick) not in self._whois: return - t.write('registrar id ') - t.write(registrar) - t.write('\n') - s = t.read_all() - for line in s.splitlines(): - line = line.strip() - if not line: - continue - if line.startswith('Email'): - url = ' ' % line.split('@')[-1] - if line == 'Not a valid ID pattern': - url = '' - try: - s = '%s%s is %s; %s.' % (domain, url, status, - ', '.join(filter(None, [created, updated, expires]))) - irc.reply(s) - except NameError, e: - irc.error('I couldn\'t find such a domain.') + (replyIrc, replyMsg, d) = self._whois[(irc, loweredNick)] + del self._whois[(irc, loweredNick)] + s = 'There is no %s on %s.' % (nick, self._getIrcName(irc)) + replyIrc.reply(s) + do401 = do402 + + def whois(self, irc, msg, args): + """[] + + Returns the WHOIS response gives for . is + only necessary if the network is different than the network the command + is sent on. + """ + network = self._getNetwork(irc, args) + nick = privmsgs.getArgs(args) + if not ircutils.isNick(nick): + irc.errorInvalid('nick', nick, Raise=True) + nick = ircutils.toLower(nick) + otherIrc = self._getIrc(network) + # The double nick here is necessary because single-nick WHOIS only works + # if the nick is on the same server (*not* the same network) as the user + # giving the command. Yeah, it made me say wtf too. + otherIrc.queueMsg(ircmsgs.whois(nick, nick)) + self._whois[(otherIrc, nick)] = (irc, msg, {}) + + def networks(self, irc, msg, args): + """takes no arguments + + Returns the networks to which the bot is currently connected. + """ + L = ['%s: %s' % (ircd.network, ircd.server) for ircd in world.ircs] + utils.sortBy(str.lower, L) + irc.reply(utils.commaAndify(L)) + Class = Network diff --git a/plugins/Relay.py b/plugins/Relay.py index f7da7d95d..d40b7490c 100644 --- a/plugins/Relay.py +++ b/plugins/Relay.py @@ -132,12 +132,6 @@ class Relay(callbacks.Privmsg): else: return irc.getRealIrc() - def _getIrc(self, name): - for irc in world.ircs: - if self._getIrcName(irc) == name: - return irc - raise KeyError, name - def _getIrcName(self, irc): # We should allow abbreviations at some point. return irc.network @@ -193,24 +187,6 @@ class Relay(callbacks.Privmsg): irc.replySuccess() part = privmsgs.checkCapability(part, 'owner') - def command(self, irc, msg, args): - """ [ ...] - - Gives the bot (with its associated s) on . - """ - if len(args) < 2: - raise callbacks.ArgumentError - network = args.pop(0) - try: - otherIrc = self._getIrc(network) - except KeyError: - irc.error('I\'m not currently on the network %r.' % network) - return - Owner = irc.getCallback('Owner') - Owner.disambiguate(irc, args) - self.Proxy(otherIrc, msg, args) - command = privmsgs.checkCapability(command, 'admin') - def nicks(self, irc, msg, args): """[] @@ -262,37 +238,6 @@ class Relay(callbacks.Privmsg): users.sort() irc.reply('; '.join(users)) - def whois(self, irc, msg, args): - """@ - - Returns the WHOIS response gives for . - """ - nickAtNetwork = privmsgs.getArgs(args) - realIrc = self._getRealIrc(irc) - try: - (nick, network) = nickAtNetwork.split('@', 1) - if not ircutils.isNick(nick): - irc.error('%s is not an IRC nick.' % nick) - return - nick = ircutils.toLower(nick) - except ValueError: # If split doesn't work, we get an unpack error. - if len(world.ircs) == 2: - # If there are only two networks being relayed, we can safely - # pick the *other* one. - nick = ircutils.toLower(nickAtNetwork) - for otherIrc in world.ircs: - if otherIrc != realIrc: - network = self._getIrcName(otherIrc) - else: - raise callbacks.ArgumentError - try: - otherIrc = self._getIrc(network) - except KeyError: - irc.error('I\'m not on that network.') - return - otherIrc.queueMsg(ircmsgs.whois(nick, nick)) - self._whois[(otherIrc, nick)] = (irc, msg, {}) - def ignore(self, irc, msg, args): """[] diff --git a/src/Misc.py b/src/Misc.py index c704ee329..ca8d6111c 100755 --- a/src/Misc.py +++ b/src/Misc.py @@ -630,51 +630,43 @@ class Misc(callbacks.Privmsg): text = privmsgs.getArgs(args) irc.reply(text, notice=True) - def networks(self, irc, msg, args): - """takes no arguments - - Returns the networks to which the bot is currently connected. - """ - L = ['%s: %s' % (ircd.network, ircd.server) for ircd in world.ircs] - utils.sortBy(str.lower, L) - irc.reply(utils.commaAndify(L)) - def contributors(self, irc, msg, args): - """ [] + """ [] Replies with a list of people who made contributions to a given plugin. - If is specified, that person's specific contributions will - be listed. Note: The is the part inside of the parentheses - in the people listing + If is specified, that person's specific contributions will + be listed. Note: The is the part inside of the parentheses + in the people listing. """ - (plugin, nickname) = privmsgs.getArgs(args, required=1, optional=1) - nickname = nickname.lower() + (plugin, nick) = privmsgs.getArgs(args, required=1, optional=1) + nick = nick.lower() def getShortName(authorInfo): """ Take an Authors object, and return only the name and nick values - in the format 'First Last (nickname)' + in the format 'First Last (nick)'. """ return '%(name)s (%(nick)s)' % authorInfo.__dict__ def buildContributorsString(longList): """ Take a list of long names and turn it into : - shortname[, shortname and shortname] + shortname[, shortname and shortname]. """ - outList = [getShortName(n) for n in longList] - return utils.commaAndify(outList) + L = [getShortName(n) for n in longList] + return utils.commaAndify(L) def sortAuthors(): """ Sort the list of 'long names' based on the number of contributions - associated with each + associated with each. """ L = module.__contributors__.items() - utils.sortBy(lambda elt: -len(elt[1]), L) - nameList = [pair[0] for pair in L] - return nameList + def negativeSecondElement(x): + return -x[1] + utils.sortBy(negativeSecondElement, L) + return [t[0] for t in L] def buildPeopleString(module): """ Build the list of author + contributors (if any) for the requested - plugin + plugin. """ head = 'The %s plugin' % plugin author = 'has not been claimed by an author' @@ -682,11 +674,11 @@ class Misc(callbacks.Privmsg): contrib = 'has no contributors listed' hasAuthor = False hasContribs = False - if getattr(module, '__author__', False): + if getattr(module, '__author__', None): author = 'was written by %s' % \ utils.mungeEmailForWeb(str(module.__author__)) hasAuthor = True - if getattr(module, '__contributors__', False): + if getattr(module, '__contributors__', None): contribs = sortAuthors() if hasAuthor: try: @@ -702,28 +694,31 @@ class Misc(callbacks.Privmsg): contrib = 'has no additional contributors listed' if hasContribs and not hasAuthor: conjunction = 'but' - return '%s %s %s %s' % (head, author, conjunction, contrib) + return ' '.join([head, author, conjunction, contrib]) def buildPersonString(module): """ Build the list of contributions (if any) for the requested person for the requested plugin """ isAuthor = False - authorInfo = getattr(supybot.authors, nickname, False) + authorInfo = getattr(supybot.authors, nick, None) if not authorInfo: - return 'The nickname specified (%s) is not a registered ' \ - 'contributor' % nickname + return 'The nick specified (%s) is not a registered ' \ + 'contributor' % nick fullName = utils.mungeEmailForWeb(str(authorInfo)) contributions = [] if hasattr(module, '__contributors__'): if authorInfo not in module.__contributors__: return 'The %s plugin does not have \'%s\' listed as a ' \ - 'contributor' % (plugin, nickname) + 'contributor' % (plugin, nick) contributions = module.__contributors__[authorInfo] if getattr(module, '__author__', False) == authorInfo: isAuthor = True + # XXX Partition needs moved to utils. splitContribs = fix.partition(lambda s: ' ' in s, contributions) results = [] + # XXX Assign splitContribs to specific names based on what it means + # semantically -- (foo, bar) = partition(...) if splitContribs[1]: results.append( 'the %s %s' %(utils.commaAndify(splitContribs[1]), @@ -747,7 +742,7 @@ class Misc(callbacks.Privmsg): irc.error('No such plugin %r exists.' % plugin) return module = sys.modules[cb.__class__.__module__] - if not nickname: + if not nick: irc.reply(buildPeopleString(module)) else: irc.reply(buildPersonString(module)) diff --git a/src/Owner.py b/src/Owner.py index a99eec9a4..e06b952aa 100644 --- a/src/Owner.py +++ b/src/Owner.py @@ -254,13 +254,6 @@ class Owner(privmsgs.CapabilityCheckingPrivmsg): def __lt__(self, other): return True # We should always be the first plugin. - def _getIrc(self, network): - network = network.lower() - for irc in world.ircs: - if irc.network.lower() == network: - return irc - return None - def outFilter(self, irc, msg): if msg.command == 'PRIVMSG' and not world.testing: if ircutils.strEqual(msg.args[0], irc.nick): @@ -277,6 +270,26 @@ class Owner(privmsgs.CapabilityCheckingPrivmsg): callbacks.Privmsg._mores.clear() self.__parent.reset() + def _connect(self, network, serverPort=None): + try: + group = conf.supybot.networks.get(network) + (server, port) = group.servers()[0] + except (registry.NonExistentRegistryEntry, IndexError): + if serverPort is None: + raise ValueError, 'connect requires a (server, port) ' \ + 'if the network is not registered.' + conf.registerNetwork(network) + serverS = '%s:%s' % serverPort + conf.supybot.networks.get(network).servers.append(serverS) + assert conf.supybot.networks.get(network).servers() + self.log.info('Creating new Irc for %s.', network) + newIrc = irclib.Irc(network) + for irc in world.ircs: + if irc != newIrc: + newIrc.state.history = irc.state.history + driver = drivers.newDriver(newIrc) + return newIrc + def do001(self, irc, msg): self.log.info('Loading plugins.') alwaysLoadSrcPlugins = conf.supybot.plugins.alwaysLoadDefault() @@ -696,28 +709,6 @@ class Owner(privmsgs.CapabilityCheckingPrivmsg): else: irc.error('There was no plugin %s.' % name) - def reconnect(self, irc, msg, args): - """[] - - Disconnects and then reconnects to . If no network is given, - disconnects and then reconnects to the network the command was given - on. - """ - network = privmsgs.getArgs(args, required=0, optional=1) - if network: - badIrc = self._getIrc(network) - if badIrc is None: - irc.error('I\'m not currently connected on %s.' % network) - return - else: - badIrc = irc - try: - badIrc.driver.reconnect() - if badIrc != irc: - irc.replySuccess() - except AttributeError: # There's a cleaner way to do this, but I'm lazy. - irc.error('I couldn\'t reconnect. You should restart me instead.') - def defaultcapability(self, irc, msg, args): """{add|remove} @@ -824,77 +815,6 @@ class Owner(privmsgs.CapabilityCheckingPrivmsg): irc.errorInvalid('plugin', plugin, Raise=True) self.reload(irc, msg, args) # This makes the replySuccess. - def _connect(self, network, serverPort=None): - try: - group = conf.supybot.networks.get(network) - (server, port) = group.servers()[0] - except (registry.NonExistentRegistryEntry, IndexError): - if serverPort is None: - raise ValueError, 'connect requires a (server, port) ' \ - 'if the network is not registered.' - conf.registerNetwork(network) - serverS = '%s:%s' % serverPort - conf.supybot.networks.get(network).servers.append(serverS) - assert conf.supybot.networks.get(network).servers() - self.log.info('Creating new Irc for %s.', network) - newIrc = irclib.Irc(network) - for irc in world.ircs: - if irc != newIrc: - newIrc.state.history = irc.state.history - driver = drivers.newDriver(newIrc) - return newIrc - - def connect(self, irc, msg, args): - """ [] - - Connects to another network at . If port is not provided, it - defaults to 6667, the default port for IRC. - """ - (network, server) = privmsgs.getArgs(args, optional=1) - otherIrc = self._getIrc(network) - if otherIrc is not None: - irc.error('I\'m already connected to %s.' % network) - return - if server: - if ':' in server: - (server, port) = server.split(':') - port = int(port) - else: - port = 6667 - serverPort = (server, port) - else: - try: - serverPort = conf.supybot.networks.get(network).servers()[0] - except (registry.NonExistentRegistryEntry, IndexError): - irc.error('A server must be provided if the network is not ' - 'already registered.') - return - newIrc = self._connect(network, serverPort=serverPort) - conf.supybot.networks().add(network) - assert newIrc.callbacks is irc.callbacks, 'callbacks list is different' - irc.replySuccess('Connection to %s initiated.' % network) - - def disconnect(self, irc, msg, args): - """ [] - - Disconnects and ceases to relay to and from the network represented by - the network . If is given, quits the network - with the given quit message. - """ - (network, quitMsg) = privmsgs.getArgs(args, optional=1) - if not quitMsg: - quitMsg = msg.nick - otherIrc = self._getIrc(network) - if otherIrc is not None: - # replySuccess here, rather than lower, in case we're being - # told to disconnect from the network we received the command on. - irc.replySuccess() - otherIrc.queueMsg(ircmsgs.quit(quitMsg)) - otherIrc.die() - else: - irc.error('I\'m not connected to %s.' % network, Raise=True) - conf.supybot.networks().discard(network) - Class = Owner diff --git a/test/test_Network.py b/test/test_Network.py index 7b6d461b6..12b780643 100644 --- a/test/test_Network.py +++ b/test/test_Network.py @@ -31,21 +31,14 @@ from testsupport import * -if network: - class NetworkTestCase(PluginTestCase): - plugins = ['Network'] - def testDns(self): - self.assertNotError('dns slashdot.org') - self.assertResponse('dns alsdkjfaslkdfjaslkdfj.com', - 'Host not found.') +class NetworkTestCase(PluginTestCase): + plugins = ['Network', 'Utilities'] + def testNetworks(self): + self.assertNotError('networks') - def testWhois(self): - self.assertNotError('network whois ohio-state.edu') - self.assertError('network whois www.ohio-state.edu') - self.assertNotError('network whois kuro5hin.org') - self.assertError('network whois www.kuro5hin.org') - self.assertNotError('network whois microsoft.com') - self.assertNotError('network whois inria.fr') + def testCommand(self): + self.assertResponse('network command %s echo 1' % self.irc.network, + '1') # vim:set shiftwidth=4 tabstop=8 expandtab textwidth=78: