589 lines
20 KiB
Python
589 lines
20 KiB
Python
import time
|
|
|
|
from conf import conf
|
|
import ui
|
|
import windows
|
|
import irc
|
|
|
|
COMMAND_PREFIX = conf.get('command_prefix', '/')
|
|
|
|
NICK_SUFFIX = r"`_-\|0123456789"
|
|
|
|
_hextochr = dict(('%02x' % i, chr(i)) for i in range(256))
|
|
def unquote(url, rurl=""):
|
|
|
|
while '%' in url:
|
|
url, char = url.rsplit('%', 1)
|
|
|
|
chars = char[:2].lower()
|
|
|
|
if chars in _hextochr:
|
|
rurl = '%s%s%s' % (_hextochr[chars], char[2:], rurl)
|
|
else:
|
|
rurl = "%s%s%s" % ('%', char, rurl)
|
|
|
|
return url + rurl
|
|
|
|
#for getting a list of alternative nicks to try on a network
|
|
def _nick_generator(network):
|
|
for nick in network.nicks[1:]:
|
|
yield nick
|
|
if network._nick_error:
|
|
nick = 'ircperson'
|
|
else:
|
|
nick = network.nicks[0]
|
|
import itertools
|
|
for i in itertools.count(1):
|
|
for j in xrange(len(NICK_SUFFIX)**i):
|
|
suffix = ''.join(NICK_SUFFIX[(j/(len(NICK_SUFFIX)**x))%len(NICK_SUFFIX)] for x in xrange(i))
|
|
if network._nick_max_length:
|
|
yield nick[0:network._nick_max_length-i]+suffix
|
|
else:
|
|
yield nick+suffix
|
|
|
|
def setdownRaw(e):
|
|
if not e.done:
|
|
if not e.network.got_nick:
|
|
if e.msg[1] in ('432','433','436','437'): #nickname unavailable
|
|
failednick = e.msg[3]
|
|
nicks = list(e.network.nicks)
|
|
|
|
if hasattr(e.network,'_nick_generator'):
|
|
if len(failednick) < len(e.network._next_nick):
|
|
e.network._nick_max_length = len(failednick)
|
|
e.network._next_nick = e.network._nick_generator.next()
|
|
e.network.raw('NICK %s' % e.network._next_nick)
|
|
e.network._nick_error |= (e.msg[1] == '432')
|
|
else:
|
|
e.network._nick_error = (e.msg[1] == '432')
|
|
if len(failednick) < len(e.network.nicks[0]):
|
|
e.network._nick_max_length = len(failednick)
|
|
else:
|
|
e.network._nick_max_length = 0
|
|
e.network._nick_generator = _nick_generator(e.network)
|
|
e.network._next_nick = e.network._nick_generator.next()
|
|
e.network.raw('NICK %s' % e.network._next_nick)
|
|
|
|
elif e.msg[1] == '431': #no nickname given--this shouldn't happen
|
|
pass
|
|
|
|
elif e.msg[1] == '001':
|
|
e.network.got_nick = True
|
|
if e.network.me != e.msg[2]:
|
|
core.events.trigger(
|
|
'Nick', network=e.network, window=e.window,
|
|
source=e.network.me, target=e.msg[2], address='',
|
|
text=e.msg[2]
|
|
)
|
|
e.network.me = e.msg[2]
|
|
if hasattr(e.network,'_nick_generator'):
|
|
del e.network._nick_generator, e.network._nick_max_length, e.network._next_nick
|
|
|
|
if e.msg[1] == "PING":
|
|
e.network.raw("PONG :%s" % e.msg[-1])
|
|
e.done = True
|
|
|
|
elif e.msg[1] == "JOIN":
|
|
e.channel = e.target
|
|
e.requested = e.network.norm_case(e.channel) in e.network.requested_joins
|
|
core.events.trigger("Join", e)
|
|
e.done = True
|
|
|
|
elif e.msg[1] == "PART":
|
|
e.channel = e.target
|
|
e.requested = e.network.norm_case(e.channel) in e.network.requested_parts
|
|
e.text = ' '.join(e.msg[3:])
|
|
core.events.trigger("Part", e)
|
|
e.done = True
|
|
|
|
elif e.msg[1] in "MODE":
|
|
e.channel = e.target
|
|
e.text = ' '.join(e.msg[3:])
|
|
core.events.trigger("Mode", e)
|
|
e.done = True
|
|
|
|
elif e.msg[1] == "QUIT":
|
|
core.events.trigger('Quit', e)
|
|
e.done = True
|
|
|
|
elif e.msg[1] == "KICK":
|
|
e.channel = e.msg[2]
|
|
e.target = e.msg[3]
|
|
core.events.trigger('Kick', e)
|
|
e.done = True
|
|
|
|
elif e.msg[1] == "NICK":
|
|
core.events.trigger('Nick', e)
|
|
if e.network.me == e.source:
|
|
e.network.me = e.target
|
|
|
|
e.done = True
|
|
|
|
elif e.msg[1] == "PRIVMSG":
|
|
core.events.trigger('Text', e)
|
|
e.done = True
|
|
|
|
elif e.msg[1] == "NOTICE":
|
|
core.events.trigger('Notice', e)
|
|
e.done = True
|
|
|
|
elif e.msg[1] == "TOPIC":
|
|
core.events.trigger('Topic', e)
|
|
e.done = True
|
|
|
|
elif e.msg[1] in ("376", "422"): #RPL_ENDOFMOTD
|
|
if e.network.status == irc.INITIALIZING:
|
|
e.network.status = irc.CONNECTED
|
|
core.events.trigger('Connect', e)
|
|
e.done = True
|
|
|
|
elif e.msg[1] == "470": #forwarded from channel X to channel Y
|
|
if e.network.norm_case(e.msg[3]) in e.network.requested_joins:
|
|
e.network.requested_joins.discard(e.network.norm_case(e.msg[3]))
|
|
e.network.requested_joins.add(e.network.norm_case(e.msg[4]))
|
|
|
|
elif e.msg[1] == "005": #RPL_ISUPPORT
|
|
for arg in e.msg[3:]:
|
|
if ' ' not in arg: #ignore "are supported by this server"
|
|
if '=' in arg:
|
|
name, value = arg.split('=', 1)
|
|
if value.isdigit():
|
|
value = int(value)
|
|
else:
|
|
name, value = arg, ''
|
|
|
|
#Workaround for broken servers (bahamut on EnterTheGame)
|
|
if name == 'PREFIX' and value[0] != '(':
|
|
continue
|
|
|
|
#in theory, we're supposed to replace \xHH with the
|
|
# corresponding ascii character, but I don't think anyone
|
|
# really does this
|
|
e.network.isupport[name] = value
|
|
|
|
if name == 'PREFIX':
|
|
new_prefixes = {}
|
|
modes, prefixes = value[1:].split(')')
|
|
for mode, prefix in zip(modes, prefixes):
|
|
new_prefixes[mode] = prefix
|
|
new_prefixes[prefix] = mode
|
|
e.network.prefixes = new_prefixes
|
|
|
|
def setupSocketConnect(e):
|
|
e.network.got_nick = False
|
|
e.network.isupport = {
|
|
'NETWORK': e.network.server,
|
|
'PREFIX': '(ohv)@%+',
|
|
'CHANMODES': 'b,k,l,imnpstr',
|
|
}
|
|
e.network.prefixes = {'o':'@', 'h':'%', 'v':'+', '@':'o', '%':'h', '+':'v'}
|
|
e.network.connect_timestamp = time.time()
|
|
e.network.requested_joins.clear()
|
|
e.network.requested_parts.clear()
|
|
e.network.on_channels.clear()
|
|
if hasattr(e.network,'_nick_generator'):
|
|
del e.network._nick_generator, e.network._nick_max_length, e.network._next_nick
|
|
if not e.done:
|
|
#this needs to be tested--anyone have a server that uses PASS?
|
|
if e.network.password:
|
|
e.network.raw("PASS :%s" % e.network.password)
|
|
e.network.raw("NICK %s" % e.network.nicks[0])
|
|
e.network.raw("USER %s %s %s :%s" %
|
|
(e.network.username, "8", "*", e.network.fullname))
|
|
#per rfc2812 these are username, user mode flags, unused, realname
|
|
|
|
#e.network.me = None
|
|
e.done = True
|
|
|
|
def onDisconnect(e):
|
|
if hasattr(e.network,'_reconnect_source'):
|
|
e.network._reconnect_source.unregister()
|
|
del e.network._reconnect_source
|
|
if hasattr(e.network,'connect_timestamp'):
|
|
if e.error and conf.get('autoreconnect',True):
|
|
delay = time.time() - e.network.connect_timestamp > 30 and 30 or 120
|
|
def do_reconnect():
|
|
if not e.network.status:
|
|
server(network=e.network)
|
|
def do_announce_reconnect():
|
|
if not e.network.status:
|
|
windows.get_default(e.network).write("* Will reconnect in %s seconds.." % delay)
|
|
e.network._reconnect_source = ui.register_timer(delay*1000,do_reconnect)
|
|
e.network._reconnect_source = ui.register_idle(do_announce_reconnect)
|
|
|
|
def onCloseNetwork(e):
|
|
e.network.quit()
|
|
if hasattr(e.network,'_reconnect_source'):
|
|
e.network._reconnect_source.unregister()
|
|
del e.network._reconnect_source
|
|
|
|
def setdownDisconnect(e):
|
|
if hasattr(e.network,'connect_timestamp'):
|
|
del e.network.connect_timestamp
|
|
|
|
def setupInput(e):
|
|
if not e.done:
|
|
if e.text.startswith(COMMAND_PREFIX) and not e.ctrl:
|
|
command = e.text[len(COMMAND_PREFIX):]
|
|
else:
|
|
command = 'say - %s' % e.text
|
|
|
|
core.events.run(command, e.window, e.network)
|
|
|
|
e.done = True
|
|
|
|
def onCommandSay(e):
|
|
if isinstance(e.window, windows.ChannelWindow) or isinstance(e.window, windows.QueryWindow):
|
|
e.network.msg(e.window.id, ' '.join(e.args))
|
|
else:
|
|
raise core.events.CommandError("There's no one here to speak to.")
|
|
|
|
def onCommandMsg(e):
|
|
e.network.msg(e.args[0], ' '.join(e.args[1:]))
|
|
|
|
def onCommandNotice(e):
|
|
e.network.notice(e.args[0], ' '.join(e.args[1:]))
|
|
|
|
def onCommandQuery(e):
|
|
windows.new(windows.QueryWindow, e.network, e.args[0], core).activate()
|
|
if len(e.args) > 1:
|
|
message = ' '.join(e.args[1:])
|
|
if message: #this is false if you do "/query nickname "
|
|
e.network.msg(e.args[0], ' '.join(e.args[1:]))
|
|
|
|
def setupJoin(e):
|
|
if e.source == e.network.me:
|
|
chan = e.network.norm_case(e.channel)
|
|
e.network.on_channels.add(chan)
|
|
e.network.requested_joins.discard(chan)
|
|
|
|
def setdownPart(e):
|
|
if e.source == e.network.me:
|
|
chan = e.network.norm_case(e.channel)
|
|
e.network.on_channels.discard(chan)
|
|
e.network.requested_parts.discard(chan)
|
|
|
|
def setdownKick(e):
|
|
if e.target == e.network.me:
|
|
chan = e.network.norm_case(e.channel)
|
|
e.network.on_channels.discard(chan)
|
|
|
|
def ischan(network, channel):
|
|
return network.norm_case(channel) in network.on_channels
|
|
|
|
# make /nick work offline
|
|
def change_nick(network, nick):
|
|
if not network.status:
|
|
core.events.trigger(
|
|
'Nick',
|
|
network=network, window=windows.get_default(network),
|
|
source=network.me, target=nick, address='', text=nick
|
|
)
|
|
network.nicks[0] = nick
|
|
network.me = nick
|
|
else:
|
|
network.raw('NICK :%s' % nick)
|
|
|
|
def onCommandNick(e):
|
|
default_nick = irc.default_nicks()[0]
|
|
if 't' not in e.switches and e.network.me == default_nick:
|
|
conf['nick'] = e.args[0]
|
|
import conf as _conf
|
|
_conf.save()
|
|
for network in set(w.network for w in core.manager):
|
|
if network.me == default_nick:
|
|
change_nick(network, e.args[0])
|
|
else:
|
|
change_nick(e.network, e.args[0])
|
|
|
|
def setdownNick(e):
|
|
if e.source != e.network.me:
|
|
window = windows.get(windows.QueryWindow, e.network, e.source)
|
|
if window:
|
|
window.id = e.target
|
|
|
|
# make /quit always disconnect us
|
|
def onCommandQuit(e):
|
|
if e.network.status:
|
|
e.network.quit(' '.join(e.args))
|
|
else:
|
|
raise core.events.CommandError("We're not connected to a network.")
|
|
|
|
def onCommandRaw(e):
|
|
if e.network.status >= irc.INITIALIZING:
|
|
e.network.raw(' '.join(e.args))
|
|
else:
|
|
raise core.events.CommandError("We're not connected to a network.")
|
|
|
|
onCommandQuote = onCommandRaw
|
|
|
|
def onCommandJoin(e):
|
|
if e.args:
|
|
if e.network.status >= irc.INITIALIZING:
|
|
e.network.join(' '.join(e.args), requested = 'n' not in e.switches)
|
|
else:
|
|
raise core.events.CommandError("We're not connected.")
|
|
elif isinstance(e.window, windows.ChannelWindow):
|
|
e.window.network.join(e.window.id, requested = 'n' not in e.switches)
|
|
else:
|
|
raise core.events.CommandError("You must supply a channel.")
|
|
|
|
def onCommandPart(e):
|
|
if e.args:
|
|
if e.network.status >= irc.INITIALIZING:
|
|
e.network.part(' '.join(e.args), requested = 'n' not in e.switches)
|
|
else:
|
|
raise core.events.CommandError("We're not connected.")
|
|
elif isinstance(e.window, windows.ChannelWindow):
|
|
e.window.network.part(e.window.id, requested = 'n' not in e.switches)
|
|
else:
|
|
raise core.events.CommandError("You must supply a channel.")
|
|
|
|
def onCommandHop(e):
|
|
if e.args:
|
|
if e.network.status >= irc.INITIALIZING:
|
|
e.network.part(e.args[0], requested = False)
|
|
e.network.join(' '.join(e.args), requested = False)
|
|
else:
|
|
raise core.events.CommandError("We're not connected.")
|
|
elif isinstance(e.window, windows.ChannelWindow):
|
|
e.window.network.part(e.window.id, requested = False)
|
|
e.window.network.join(e.window.id, requested = False)
|
|
else:
|
|
raise core.events.CommandError("You must supply a channel.")
|
|
|
|
#this should be used whereever a new irc.Network may need to be created
|
|
def server(server=None,port=6667,network=None,connect=True):
|
|
network_info = {}
|
|
|
|
if server:
|
|
network_info["name"] = server
|
|
network_info["server"] = server
|
|
if port:
|
|
network_info["port"] = port
|
|
get_network_info(server, network_info)
|
|
|
|
if not network:
|
|
network = irc.Network(**network_info)
|
|
windows.new(windows.StatusWindow, network, "status").activate()
|
|
else:
|
|
if "server" in network_info:
|
|
network.name = network_info['name']
|
|
network.server = network_info['server']
|
|
if not network.status:
|
|
#window = windows.get_default(network)
|
|
window = core.window
|
|
if window:
|
|
window.update()
|
|
if "port" in network_info:
|
|
network.port = network_info["port"]
|
|
|
|
if network.status:
|
|
network.quit()
|
|
if connect:
|
|
network.connect()
|
|
core.window.write("* Connecting to %s on port %s" % (network.server, network.port))
|
|
#windows.get_default(network).write(
|
|
# "* Connecting to %s on port %s" % (network.server, network.port)
|
|
# )
|
|
|
|
return network
|
|
|
|
def onCommandServer(e):
|
|
host = port = None
|
|
|
|
if e.args:
|
|
host = e.args[0]
|
|
|
|
if ':' in host:
|
|
host, port = host.rsplit(':', 1)
|
|
port = int(port)
|
|
|
|
elif len(e.args) > 1:
|
|
port = int(e.args[1])
|
|
|
|
else:
|
|
port = 6667
|
|
|
|
if 'm' in e.switches:
|
|
network = None
|
|
else:
|
|
network = e.network
|
|
|
|
server(server=host, port=port, network=network, connect='o' not in e.switches)
|
|
|
|
#see http://www.w3.org/Addressing/draft-mirashi-url-irc-01.txt
|
|
def onCommandIrcurl(e):
|
|
url = e.args[0]
|
|
|
|
if url.startswith('irc://'):
|
|
url = url[6:]
|
|
|
|
if not url.startswith('/'):
|
|
host, target = url.rsplit('/',1)
|
|
if ':' in host:
|
|
host, port = host.rsplit(':',1)
|
|
else:
|
|
port = 6667
|
|
else:
|
|
host = None
|
|
port = 6667
|
|
target = url
|
|
|
|
if host:
|
|
if e.network and e.network.server == host:
|
|
network = e.network
|
|
else:
|
|
for w in list(windows.manager):
|
|
if w.network and w.network.server == host:
|
|
network = w.network
|
|
break
|
|
else:
|
|
for w in list(windows.manager):
|
|
if w.network and w.network.server == 'irc.default.org':
|
|
network = server(host,port,w.network)
|
|
break
|
|
else:
|
|
network = server(host,port)
|
|
|
|
if ',' in target:
|
|
target, modifiers = target.split(',',1)
|
|
action = ''
|
|
else:
|
|
target = unquote(target)
|
|
if target[0] not in '#&+':
|
|
target = '#'+target
|
|
action = 'join %s' % target
|
|
|
|
if network.status == irc.CONNECTED:
|
|
core.events.run(action, windows.get_default(network), network)
|
|
else:
|
|
if not hasattr(network,'temp_perform'):
|
|
network.temp_perform = [action]
|
|
else:
|
|
network.temp_perform.append(action)
|
|
|
|
#commands that we need to add a : to but otherwise can send unchanged
|
|
#the dictionary contains the number of arguments we take without adding the :
|
|
trailing = {
|
|
'away':0,
|
|
'cnotice':2,
|
|
'cprivmsg':2,
|
|
'kick':2,
|
|
'kill':1,
|
|
'part':1,
|
|
'squery':1,
|
|
'squit':1,
|
|
'topic':1,
|
|
'wallops':0,
|
|
}
|
|
|
|
needschan = {
|
|
'topic':0,
|
|
'invite':1,
|
|
'kick':0,
|
|
# 'mode':0, #this is commonly used for channels, but can apply to users
|
|
# 'names':0, #with no parameters, this is supposed to give a list of all users; we may be able to safely ignore that.
|
|
}
|
|
|
|
def setupCommand(e):
|
|
if not e.done:
|
|
if e.name in needschan and isinstance(e.window, windows.ChannelWindow):
|
|
valid_chan_prefixes = e.network.isupport.get('CHANTYPES', '#&+')
|
|
chan_pos = needschan[e.name]
|
|
|
|
if len(e.args) > chan_pos:
|
|
if not e.args[chan_pos] or e.args[chan_pos][0] not in valid_chan_prefixes:
|
|
e.args.insert(chan_pos, e.window.id)
|
|
else:
|
|
e.args.append(e.window.id)
|
|
|
|
if e.name in trailing:
|
|
trailing_pos = trailing[e.name]
|
|
|
|
if len(e.args) > trailing_pos:
|
|
e.args[trailing_pos] = ':%s' % e.args[trailing_pos]
|
|
|
|
e.text = '%s %s' % (e.name, ' '.join(e.args))
|
|
|
|
def setdownCommand(e):
|
|
if not e.done and e.network.status >= irc.INITIALIZING:
|
|
e.network.raw(e.text)
|
|
e.done = True
|
|
|
|
def get_network_info(name, network_info):
|
|
conf_info = conf.get('networks', {}).get(name)
|
|
|
|
if conf_info:
|
|
network_info['server'] = name
|
|
network_info.update(conf_info)
|
|
|
|
def onStart(e):
|
|
for network in conf.get('start_networks', []):
|
|
server(server=network)
|
|
|
|
def onConnect(e):
|
|
network_info = conf.get('networks', {}).get(e.network.name, {})
|
|
|
|
for command in network_info.get('perform', []):
|
|
while command.startswith(COMMAND_PREFIX):
|
|
command = command[len(COMMAND_PREFIX):]
|
|
core.events.run(command, e.window, e.network)
|
|
|
|
tojoin = ','.join(network_info.get('join', []))
|
|
if tojoin:
|
|
core.events.run('join -n %s' % tojoin, e.window, e.network)
|
|
|
|
if hasattr(e.network,'temp_perform'):
|
|
for command in e.network.temp_perform:
|
|
core.events.run(command, e.window, e.network)
|
|
del e.network.temp_perform
|
|
|
|
def isautojoin(network, channel):
|
|
try:
|
|
joinlist = conf['networks'][network.name]['join']
|
|
except KeyError:
|
|
return False
|
|
normchannel = network.norm_case(channel)
|
|
for chan in joinlist:
|
|
if normchannel == network.norm_case(chan):
|
|
return True
|
|
return False
|
|
|
|
def setautojoin(network, channel):
|
|
if 'networks' not in conf:
|
|
conf['networks'] = networks = {}
|
|
else:
|
|
networks = conf['networks']
|
|
if network.name not in networks:
|
|
networks[network.name] = network_settings = {}
|
|
if 'start_networks' not in conf:
|
|
conf['start_networks'] = []
|
|
conf['start_networks'].append(network.name)
|
|
else:
|
|
network_settings = networks[network.name]
|
|
|
|
if 'join' not in network_settings:
|
|
network_settings['join'] = [channel]
|
|
else:
|
|
network_settings['join'].append(channel)
|
|
|
|
def unsetautojoin(network, channel):
|
|
try:
|
|
joinlist = conf['networks'][network.name]['join']
|
|
except KeyError:
|
|
return False
|
|
normchannel = network.norm_case(channel)
|
|
for i, chan in enumerate(joinlist[:]):
|
|
if normchannel == network.norm_case(chan):
|
|
joinlist.pop(i)
|
|
|
|
def onChannelMenu(e):
|
|
def toggle_join():
|
|
if isautojoin(e.network, e.channel):
|
|
unsetautojoin(e.network, e.channel)
|
|
else:
|
|
setautojoin(e.network, e.channel)
|
|
|
|
e.menu.append(('Autojoin', isautojoin(e.network, e.channel), toggle_join))
|