First :D
This commit is contained in:
commit
37588421f3
100 changed files with 22673 additions and 0 deletions
53
core/config.py
Normal file
53
core/config.py
Normal file
|
@ -0,0 +1,53 @@
|
|||
import inspect
|
||||
import json
|
||||
import os
|
||||
|
||||
|
||||
def save(conf):
|
||||
json.dump(conf, open('config', 'w'), sort_keys=True, indent=2)
|
||||
|
||||
if not os.path.exists('config'):
|
||||
open('config', 'w').write(inspect.cleandoc(
|
||||
r'''
|
||||
{
|
||||
"connections":
|
||||
{
|
||||
"local irc":
|
||||
{
|
||||
"server": "localhost",
|
||||
"nick": "skybot",
|
||||
"channels": ["#test"]
|
||||
}
|
||||
},
|
||||
"disabled_plugins": [],
|
||||
"disabled_commands": [],
|
||||
"acls": {},
|
||||
"api_keys": {},
|
||||
"censored_strings":
|
||||
[
|
||||
"DCC SEND",
|
||||
"1nj3ct",
|
||||
"thewrestlinggame",
|
||||
"startkeylogger",
|
||||
"hybux",
|
||||
"\\0",
|
||||
"\\x01",
|
||||
"!coz",
|
||||
"!tell /x"
|
||||
],
|
||||
"admins": []
|
||||
}''') + '\n')
|
||||
|
||||
|
||||
def config():
|
||||
# reload config from file if file has changed
|
||||
config_mtime = os.stat('config').st_mtime
|
||||
if bot._config_mtime != config_mtime:
|
||||
try:
|
||||
bot.config = json.load(open('config'))
|
||||
bot._config_mtime = config_mtime
|
||||
except ValueError, e:
|
||||
print 'ERROR: malformed config!', e
|
||||
|
||||
|
||||
bot._config_mtime = 0
|
25
core/db.py
Normal file
25
core/db.py
Normal file
|
@ -0,0 +1,25 @@
|
|||
import os
|
||||
import sqlite3
|
||||
import thread
|
||||
|
||||
threaddbs={}
|
||||
|
||||
def get_db_connection(conn, name=''):
|
||||
"returns an sqlite3 connection to a persistent database"
|
||||
|
||||
if not name:
|
||||
name = '%s.%s.db' % (conn.nick, conn.server)
|
||||
|
||||
threadid = thread.get_ident()
|
||||
if name in threaddbs and threadid in threaddbs[name]:
|
||||
return threaddbs[name][threadid]
|
||||
filename = os.path.join(bot.persist_dir, name)
|
||||
|
||||
db = sqlite3.connect(filename, timeout=10)
|
||||
if name in threaddbs:
|
||||
threaddbs[name][threadid] = db
|
||||
else:
|
||||
threaddbs[name] = {threadid: db}
|
||||
return db
|
||||
|
||||
bot.get_db_connection = get_db_connection
|
250
core/irc.py
Normal file
250
core/irc.py
Normal file
|
@ -0,0 +1,250 @@
|
|||
import re
|
||||
import socket
|
||||
import time
|
||||
import thread
|
||||
import Queue
|
||||
|
||||
from ssl import wrap_socket, CERT_NONE, CERT_REQUIRED, SSLError
|
||||
|
||||
|
||||
def decode(txt):
|
||||
for codec in ('utf-8', 'iso-8859-1', 'shift_jis', 'cp1252'):
|
||||
try:
|
||||
return txt.decode(codec)
|
||||
except UnicodeDecodeError:
|
||||
continue
|
||||
return txt.decode('utf-8', 'ignore')
|
||||
|
||||
|
||||
def censor(text):
|
||||
replacement = '[censored]'
|
||||
if 'censored_strings' in bot.config:
|
||||
words = map(re.escape, bot.config['censored_strings'])
|
||||
regex = re.compile('(%s)' % "|".join(words))
|
||||
text = regex.sub(replacement, text)
|
||||
return text
|
||||
|
||||
|
||||
class crlf_tcp(object):
|
||||
"Handles tcp connections that consist of utf-8 lines ending with crlf"
|
||||
|
||||
def __init__(self, host, port, timeout=300):
|
||||
self.ibuffer = ""
|
||||
self.obuffer = ""
|
||||
self.oqueue = Queue.Queue() # lines to be sent out
|
||||
self.iqueue = Queue.Queue() # lines that were received
|
||||
self.socket = self.create_socket()
|
||||
self.host = host
|
||||
self.port = port
|
||||
self.timeout = timeout
|
||||
|
||||
def create_socket(self):
|
||||
return socket.socket(socket.AF_INET, socket.TCP_NODELAY)
|
||||
|
||||
def run(self):
|
||||
self.socket.connect((self.host, self.port))
|
||||
thread.start_new_thread(self.recv_loop, ())
|
||||
thread.start_new_thread(self.send_loop, ())
|
||||
|
||||
def recv_from_socket(self, nbytes):
|
||||
return self.socket.recv(nbytes)
|
||||
|
||||
def get_timeout_exception_type(self):
|
||||
return socket.timeout
|
||||
|
||||
def handle_receive_exception(self, error, last_timestamp):
|
||||
if time.time() - last_timestamp > self.timeout:
|
||||
self.iqueue.put(StopIteration)
|
||||
self.socket.close()
|
||||
return True
|
||||
return False
|
||||
|
||||
def recv_loop(self):
|
||||
last_timestamp = time.time()
|
||||
while True:
|
||||
try:
|
||||
data = self.recv_from_socket(4096)
|
||||
self.ibuffer += data
|
||||
if data:
|
||||
last_timestamp = time.time()
|
||||
else:
|
||||
if time.time() - last_timestamp > self.timeout:
|
||||
self.iqueue.put(StopIteration)
|
||||
self.socket.close()
|
||||
return
|
||||
time.sleep(1)
|
||||
except (self.get_timeout_exception_type(), socket.error) as e:
|
||||
if self.handle_receive_exception(e, last_timestamp):
|
||||
return
|
||||
continue
|
||||
|
||||
while '\r\n' in self.ibuffer:
|
||||
line, self.ibuffer = self.ibuffer.split('\r\n', 1)
|
||||
self.iqueue.put(decode(line))
|
||||
|
||||
def send_loop(self):
|
||||
while True:
|
||||
line = self.oqueue.get().splitlines()[0][:500]
|
||||
print ">>> %r" % line
|
||||
self.obuffer += line.encode('utf-8', 'replace') + '\r\n'
|
||||
while self.obuffer:
|
||||
sent = self.socket.send(self.obuffer)
|
||||
self.obuffer = self.obuffer[sent:]
|
||||
|
||||
|
||||
class crlf_ssl_tcp(crlf_tcp):
|
||||
"Handles ssl tcp connetions that consist of utf-8 lines ending with crlf"
|
||||
def __init__(self, host, port, ignore_cert_errors, timeout=300):
|
||||
self.ignore_cert_errors = ignore_cert_errors
|
||||
crlf_tcp.__init__(self, host, port, timeout)
|
||||
|
||||
def create_socket(self):
|
||||
return wrap_socket(crlf_tcp.create_socket(self), server_side=False,
|
||||
cert_reqs=CERT_NONE if self.ignore_cert_errors else
|
||||
CERT_REQUIRED)
|
||||
|
||||
def recv_from_socket(self, nbytes):
|
||||
return self.socket.read(nbytes)
|
||||
|
||||
def get_timeout_exception_type(self):
|
||||
return SSLError
|
||||
|
||||
def handle_receive_exception(self, error, last_timestamp):
|
||||
# this is terrible
|
||||
if not "timed out" in error.args[0]:
|
||||
raise
|
||||
return crlf_tcp.handle_receive_exception(self, error, last_timestamp)
|
||||
|
||||
irc_prefix_rem = re.compile(r'(.*?) (.*?) (.*)').match
|
||||
irc_noprefix_rem = re.compile(r'()(.*?) (.*)').match
|
||||
irc_netmask_rem = re.compile(r':?([^!@]*)!?([^@]*)@?(.*)').match
|
||||
irc_param_ref = re.compile(r'(?:^|(?<= ))(:.*|[^ ]+)').findall
|
||||
|
||||
|
||||
class IRC(object):
|
||||
"handles the IRC protocol"
|
||||
#see the docs/ folder for more information on the protocol
|
||||
def __init__(self, server, nick, port=6667, channels=[], conf={}):
|
||||
self.channels = channels
|
||||
self.conf = conf
|
||||
self.server = server
|
||||
self.port = port
|
||||
self.nick = nick
|
||||
|
||||
self.out = Queue.Queue() # responses from the server are placed here
|
||||
# format: [rawline, prefix, command, params,
|
||||
# nick, user, host, paramlist, msg]
|
||||
self.connect()
|
||||
|
||||
thread.start_new_thread(self.parse_loop, ())
|
||||
|
||||
def create_connection(self):
|
||||
return crlf_tcp(self.server, self.port)
|
||||
|
||||
def connect(self):
|
||||
self.conn = self.create_connection()
|
||||
thread.start_new_thread(self.conn.run, ())
|
||||
self.set_pass(self.conf.get('server_password'))
|
||||
self.set_nick(self.nick)
|
||||
self.cmd("USER",
|
||||
[conf.get('user', 'skybot'), "3", "*", conf.get('realname',
|
||||
'Python bot - http://github.com/rmmh/skybot')])
|
||||
|
||||
def parse_loop(self):
|
||||
while True:
|
||||
msg = self.conn.iqueue.get()
|
||||
|
||||
if msg == StopIteration:
|
||||
self.connect()
|
||||
continue
|
||||
|
||||
if msg.startswith(":"): # has a prefix
|
||||
prefix, command, params = irc_prefix_rem(msg).groups()
|
||||
else:
|
||||
prefix, command, params = irc_noprefix_rem(msg).groups()
|
||||
nick, user, host = irc_netmask_rem(prefix).groups()
|
||||
paramlist = irc_param_ref(params)
|
||||
lastparam = ""
|
||||
if paramlist:
|
||||
if paramlist[-1].startswith(':'):
|
||||
paramlist[-1] = paramlist[-1][1:]
|
||||
lastparam = paramlist[-1]
|
||||
self.out.put([msg, prefix, command, params, nick, user, host,
|
||||
paramlist, lastparam])
|
||||
if command == "PING":
|
||||
self.cmd("PONG", paramlist)
|
||||
|
||||
def set_pass(self, password):
|
||||
if password:
|
||||
self.cmd("PASS", [password])
|
||||
|
||||
def set_nick(self, nick):
|
||||
self.cmd("NICK", [nick])
|
||||
|
||||
def join(self, channel):
|
||||
self.cmd("JOIN", [channel])
|
||||
|
||||
def msg(self, target, text):
|
||||
self.cmd("PRIVMSG", [target, text])
|
||||
|
||||
def cmd(self, command, params=None):
|
||||
if params:
|
||||
params[-1] = ':' + params[-1]
|
||||
self.send(command + ' ' + ' '.join(map(censor, params)))
|
||||
else:
|
||||
self.send(command)
|
||||
|
||||
def send(self, str):
|
||||
self.conn.oqueue.put(str)
|
||||
|
||||
|
||||
class FakeIRC(IRC):
|
||||
def __init__(self, server, nick, port=6667, channels=[], conf={}, fn=""):
|
||||
self.channels = channels
|
||||
self.conf = conf
|
||||
self.server = server
|
||||
self.port = port
|
||||
self.nick = nick
|
||||
|
||||
self.out = Queue.Queue() # responses from the server are placed here
|
||||
|
||||
self.f = open(fn, 'rb')
|
||||
|
||||
thread.start_new_thread(self.parse_loop, ())
|
||||
|
||||
def parse_loop(self):
|
||||
while True:
|
||||
msg = decode(self.f.readline()[9:])
|
||||
|
||||
if msg == '':
|
||||
print "!!!!DONE READING FILE!!!!"
|
||||
return
|
||||
|
||||
if msg.startswith(":"): # has a prefix
|
||||
prefix, command, params = irc_prefix_rem(msg).groups()
|
||||
else:
|
||||
prefix, command, params = irc_noprefix_rem(msg).groups()
|
||||
nick, user, host = irc_netmask_rem(prefix).groups()
|
||||
paramlist = irc_param_ref(params)
|
||||
lastparam = ""
|
||||
if paramlist:
|
||||
if paramlist[-1].startswith(':'):
|
||||
paramlist[-1] = paramlist[-1][1:]
|
||||
lastparam = paramlist[-1]
|
||||
self.out.put([msg, prefix, command, params, nick, user, host,
|
||||
paramlist, lastparam])
|
||||
if command == "PING":
|
||||
self.cmd("PONG", [params])
|
||||
|
||||
def cmd(self, command, params=None):
|
||||
pass
|
||||
|
||||
|
||||
class SSLIRC(IRC):
|
||||
def __init__(self, server, nick, port=6667, channels=[], conf={},
|
||||
ignore_certificate_errors=True):
|
||||
self.ignore_cert_errors = ignore_certificate_errors
|
||||
IRC.__init__(self, server, nick, port, channels, conf)
|
||||
|
||||
def create_connection(self):
|
||||
return crlf_ssl_tcp(self.server, self.port, self.ignore_cert_errors)
|
187
core/main.py
Normal file
187
core/main.py
Normal file
|
@ -0,0 +1,187 @@
|
|||
import thread
|
||||
import traceback
|
||||
|
||||
|
||||
thread.stack_size(1024 * 512) # reduce vm size
|
||||
|
||||
|
||||
class Input(dict):
|
||||
def __init__(self, conn, raw, prefix, command, params,
|
||||
nick, user, host, paraml, msg):
|
||||
|
||||
chan = paraml[0].lower()
|
||||
if chan == conn.nick.lower(): # is a PM
|
||||
chan = nick
|
||||
|
||||
def say(msg):
|
||||
conn.msg(chan, msg)
|
||||
|
||||
def reply(msg):
|
||||
if chan == nick: # PMs don't need prefixes
|
||||
conn.msg(chan, msg)
|
||||
else:
|
||||
conn.msg(chan, '(' + nick + ') ' + msg)
|
||||
|
||||
def pm(msg):
|
||||
conn.msg(nick, msg)
|
||||
|
||||
def set_nick(nick):
|
||||
conn.set_nick(nick)
|
||||
|
||||
def me(msg):
|
||||
conn.msg(chan, "\x01%s %s\x01" % ("ACTION", msg))
|
||||
|
||||
def notice(msg):
|
||||
conn.cmd('NOTICE', [nick, msg])
|
||||
|
||||
dict.__init__(self, conn=conn, raw=raw, prefix=prefix, command=command,
|
||||
params=params, nick=nick, user=user, host=host,
|
||||
paraml=paraml, msg=msg, server=conn.server, chan=chan,
|
||||
notice=notice, say=say, reply=reply, pm=pm, bot=bot,
|
||||
me=me, set_nick=set_nick, lastparam=paraml[-1])
|
||||
|
||||
# make dict keys accessible as attributes
|
||||
def __getattr__(self, key):
|
||||
return self[key]
|
||||
|
||||
def __setattr__(self, key, value):
|
||||
self[key] = value
|
||||
|
||||
|
||||
def run(func, input):
|
||||
args = func._args
|
||||
|
||||
if 'inp' not in input:
|
||||
input.inp = input.paraml
|
||||
|
||||
if args:
|
||||
if 'db' in args and 'db' not in input:
|
||||
input.db = get_db_connection(input.conn)
|
||||
if 'input' in args:
|
||||
input.input = input
|
||||
if 0 in args:
|
||||
out = func(input.inp, **input)
|
||||
else:
|
||||
kw = dict((key, input[key]) for key in args if key in input)
|
||||
out = func(input.inp, **kw)
|
||||
else:
|
||||
out = func(input.inp)
|
||||
if out is not None:
|
||||
input.reply(unicode(out))
|
||||
|
||||
|
||||
def do_sieve(sieve, bot, input, func, type, args):
|
||||
try:
|
||||
return sieve(bot, input, func, type, args)
|
||||
except Exception:
|
||||
print 'sieve error',
|
||||
traceback.print_exc()
|
||||
return None
|
||||
|
||||
|
||||
class Handler(object):
|
||||
'''Runs plugins in their own threads (ensures order)'''
|
||||
def __init__(self, func):
|
||||
self.func = func
|
||||
self.input_queue = Queue.Queue()
|
||||
thread.start_new_thread(self.start, ())
|
||||
|
||||
def start(self):
|
||||
uses_db = 'db' in self.func._args
|
||||
db_conns = {}
|
||||
while True:
|
||||
input = self.input_queue.get()
|
||||
|
||||
if input == StopIteration:
|
||||
break
|
||||
|
||||
if uses_db:
|
||||
db = db_conns.get(input.conn)
|
||||
if db is None:
|
||||
db = bot.get_db_connection(input.conn)
|
||||
db_conns[input.conn] = db
|
||||
input.db = db
|
||||
|
||||
run(self.func, input)
|
||||
|
||||
def stop(self):
|
||||
self.input_queue.put(StopIteration)
|
||||
|
||||
def put(self, value):
|
||||
self.input_queue.put(value)
|
||||
|
||||
|
||||
def dispatch(input, kind, func, args, autohelp=False):
|
||||
for sieve, in bot.plugs['sieve']:
|
||||
input = do_sieve(sieve, bot, input, func, kind, args)
|
||||
if input == None:
|
||||
return
|
||||
|
||||
if autohelp and args.get('autohelp', True) and not input.inp \
|
||||
and func.__doc__ is not None:
|
||||
input.notice(func.__doc__)
|
||||
return
|
||||
|
||||
if func._thread:
|
||||
bot.threads[func].put(input)
|
||||
else:
|
||||
thread.start_new_thread(run, (func, input))
|
||||
|
||||
|
||||
def match_command(command):
|
||||
commands = list(bot.commands)
|
||||
|
||||
# do some fuzzy matching
|
||||
prefix = filter(lambda x: x.startswith(command), commands)
|
||||
if len(prefix) == 1:
|
||||
return prefix[0]
|
||||
elif prefix and command not in prefix:
|
||||
return prefix
|
||||
|
||||
return command
|
||||
|
||||
|
||||
def main(conn, out):
|
||||
inp = Input(conn, *out)
|
||||
|
||||
# EVENTS
|
||||
for func, args in bot.events[inp.command] + bot.events['*']:
|
||||
dispatch(Input(conn, *out), "event", func, args)
|
||||
|
||||
if inp.command == 'PRIVMSG':
|
||||
# COMMANDS
|
||||
if inp.chan == inp.nick: # private message, no command prefix
|
||||
prefix = r'^(?:[.]?|'
|
||||
else:
|
||||
prefix = r'^(?:[.]|'
|
||||
|
||||
command_re = prefix + inp.conn.nick
|
||||
command_re += r'[:]+\s+)(\w+)(?:$|\s+)(.*)'
|
||||
|
||||
m = re.match(command_re, inp.lastparam)
|
||||
|
||||
if m:
|
||||
trigger = m.group(1).lower()
|
||||
command = match_command(trigger)
|
||||
|
||||
if isinstance(command, list): # multiple potential matches
|
||||
input = Input(conn, *out)
|
||||
input.notice("Did you mean %s or %s?" %
|
||||
(', '.join(command[:-1]), command[-1]))
|
||||
elif command in bot.commands:
|
||||
input = Input(conn, *out)
|
||||
input.trigger = trigger
|
||||
input.inp_unstripped = m.group(2)
|
||||
input.inp = input.inp_unstripped.strip()
|
||||
|
||||
func, args = bot.commands[command]
|
||||
dispatch(input, "command", func, args, autohelp=True)
|
||||
|
||||
# REGEXES
|
||||
for func, args in bot.plugs['regex']:
|
||||
m = args['re'].search(inp.lastparam)
|
||||
if m:
|
||||
input = Input(conn, *out)
|
||||
input.inp = m
|
||||
|
||||
dispatch(input, "regex", func, args)
|
161
core/reload.py
Normal file
161
core/reload.py
Normal file
|
@ -0,0 +1,161 @@
|
|||
import collections
|
||||
import glob
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
|
||||
if 'mtimes' not in globals():
|
||||
mtimes = {}
|
||||
|
||||
if 'lastfiles' not in globals():
|
||||
lastfiles = set()
|
||||
|
||||
|
||||
def make_signature(f):
|
||||
return f.func_code.co_filename, f.func_name, f.func_code.co_firstlineno
|
||||
|
||||
|
||||
def format_plug(plug, kind='', lpad=0, width=40):
|
||||
out = ' ' * lpad + '%s:%s:%s' % make_signature(plug[0])
|
||||
if kind == 'command':
|
||||
out += ' ' * (50 - len(out)) + plug[1]['name']
|
||||
|
||||
if kind == 'event':
|
||||
out += ' ' * (50 - len(out)) + ', '.join(plug[1]['events'])
|
||||
|
||||
if kind == 'regex':
|
||||
out += ' ' * (50 - len(out)) + plug[1]['regex']
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def reload(init=False):
|
||||
changed = False
|
||||
|
||||
if init:
|
||||
bot.plugs = collections.defaultdict(list)
|
||||
bot.threads = {}
|
||||
|
||||
core_fileset = set(glob.glob(os.path.join("core", "*.py")))
|
||||
|
||||
for filename in core_fileset:
|
||||
mtime = os.stat(filename).st_mtime
|
||||
if mtime != mtimes.get(filename):
|
||||
mtimes[filename] = mtime
|
||||
|
||||
changed = True
|
||||
|
||||
try:
|
||||
eval(compile(open(filename, 'U').read(), filename, 'exec'),
|
||||
globals())
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
if init: # stop if there's an error (syntax?) in a core
|
||||
sys.exit() # script on startup
|
||||
continue
|
||||
|
||||
if filename == os.path.join('core', 'reload.py'):
|
||||
reload(init=init)
|
||||
return
|
||||
|
||||
fileset = set(glob.glob(os.path.join('plugins', '*.py')))
|
||||
|
||||
# remove deleted/moved plugins
|
||||
for name, data in bot.plugs.iteritems():
|
||||
bot.plugs[name] = [x for x in data if x[0]._filename in fileset]
|
||||
|
||||
for filename in list(mtimes):
|
||||
if filename not in fileset and filename not in core_fileset:
|
||||
mtimes.pop(filename)
|
||||
|
||||
for func, handler in list(bot.threads.iteritems()):
|
||||
if func._filename not in fileset:
|
||||
handler.stop()
|
||||
del bot.threads[func]
|
||||
|
||||
# compile new plugins
|
||||
for filename in fileset:
|
||||
mtime = os.stat(filename).st_mtime
|
||||
if mtime != mtimes.get(filename):
|
||||
mtimes[filename] = mtime
|
||||
|
||||
changed = True
|
||||
|
||||
try:
|
||||
code = compile(open(filename, 'U').read(), filename, 'exec')
|
||||
namespace = {}
|
||||
eval(code, namespace)
|
||||
except Exception:
|
||||
traceback.print_exc()
|
||||
continue
|
||||
|
||||
# remove plugins already loaded from this filename
|
||||
for name, data in bot.plugs.iteritems():
|
||||
bot.plugs[name] = [x for x in data
|
||||
if x[0]._filename != filename]
|
||||
|
||||
for func, handler in list(bot.threads.iteritems()):
|
||||
if func._filename == filename:
|
||||
handler.stop()
|
||||
del bot.threads[func]
|
||||
|
||||
for obj in namespace.itervalues():
|
||||
if hasattr(obj, '_hook'): # check for magic
|
||||
if obj._thread:
|
||||
bot.threads[obj] = Handler(obj)
|
||||
|
||||
for type, data in obj._hook:
|
||||
bot.plugs[type] += [data]
|
||||
|
||||
if not init:
|
||||
print '### new plugin (type: %s) loaded:' % \
|
||||
type, format_plug(data)
|
||||
|
||||
if changed:
|
||||
bot.commands = {}
|
||||
for plug in bot.plugs['command']:
|
||||
name = plug[1]['name'].lower()
|
||||
if not re.match(r'^\w+$', name):
|
||||
print '### ERROR: invalid command name "%s" (%s)' % (name,
|
||||
format_plug(plug))
|
||||
continue
|
||||
if name in bot.commands:
|
||||
print "### ERROR: command '%s' already registered (%s, %s)" % \
|
||||
(name, format_plug(bot.commands[name]),
|
||||
format_plug(plug))
|
||||
continue
|
||||
bot.commands[name] = plug
|
||||
|
||||
bot.events = collections.defaultdict(list)
|
||||
for func, args in bot.plugs['event']:
|
||||
for event in args['events']:
|
||||
bot.events[event].append((func, args))
|
||||
|
||||
if init:
|
||||
print ' plugin listing:'
|
||||
|
||||
if bot.commands:
|
||||
# hack to make commands with multiple aliases
|
||||
# print nicely
|
||||
|
||||
print ' command:'
|
||||
commands = collections.defaultdict(list)
|
||||
|
||||
for name, (func, args) in bot.commands.iteritems():
|
||||
commands[make_signature(func)].append(name)
|
||||
|
||||
for sig, names in sorted(commands.iteritems()):
|
||||
names.sort(key=lambda x: (-len(x), x)) # long names first
|
||||
out = ' ' * 6 + '%s:%s:%s' % sig
|
||||
out += ' ' * (50 - len(out)) + ', '.join(names)
|
||||
print out
|
||||
|
||||
for kind, plugs in sorted(bot.plugs.iteritems()):
|
||||
if kind == 'command':
|
||||
continue
|
||||
print ' %s:' % kind
|
||||
for plug in plugs:
|
||||
print format_plug(plug, kind=kind, lpad=6)
|
||||
print
|
Reference in a new issue