Network plugin for network-related commands.

This commit is contained in:
Jeremy Fincher 2004-09-12 05:46:18 +00:00
parent dfef079480
commit a9694c1012
5 changed files with 275 additions and 293 deletions

View File

@ -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):
"""<host|ip>
_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 <host> or the reverse DNS hostname of <ip>.
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):
"""<network> [<host[:port]>]
Connects to another network at <host:port>. 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):
"""<domain>
def disconnect(self, irc, msg, args):
"""[<network>] [<quit message>]
Returns WHOIS information on the registration of <domain>.
Disconnects from the network represented by the network <network>.
If <quit message> is given, quits the network with the given quit
message. <network> 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('<domain> 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):
"""[<network>]
Disconnects and then reconnects to <network>. 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):
"""<network> <command> [<arg> ...]
Gives the bot <command> (with its associated <arg>s) on <network>.
"""
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('<domain> 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 = '<unknown>'
signon = '<unknown>'
if '312' in d:
server = d['312'].args[2]
else:
server = '<unknown>'
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 = ' <registered at %s>' % 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):
"""[<network>] <nick>
Returns the WHOIS response <network> gives for <nick>. <network> 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

View File

@ -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):
"""<network> <command> [<arg> ...]
Gives the bot <command> (with its associated <arg>s) on <network>.
"""
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):
"""[<channel>]
@ -262,37 +238,6 @@ class Relay(callbacks.Privmsg):
users.sort()
irc.reply('; '.join(users))
def whois(self, irc, msg, args):
"""<nick>@<network>
Returns the WHOIS response <network> gives for <nick>.
"""
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):
"""[<channel>] <nick|hostmask>

View File

@ -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):
"""<plugin> [<nickname>]
"""<plugin> [<nick>]
Replies with a list of people who made contributions to a given plugin.
If <nickname> is specified, that person's specific contributions will
be listed. Note: The <nickname> is the part inside of the parentheses
in the people listing
If <nick> is specified, that person's specific contributions will
be listed. Note: The <nick> 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))

View File

@ -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):
"""[<network>]
Disconnects and then reconnects to <network>. 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} <capability>
@ -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):
"""<network> [<host[:port]>]
Connects to another network at <host:port>. 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):
"""<network> [<quit message>]
Disconnects and ceases to relay to and from the network represented by
the network <network>. If <quit message> 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

View File

@ -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: