diff --git a/plugins/Aka/config.py b/plugins/Aka/config.py
index 59e3d0274..4c5069f83 100644
--- a/plugins/Aka/config.py
+++ b/plugins/Aka/config.py
@@ -56,5 +56,9 @@ conf.registerGlobalValue(Aka, 'maximumWordsInName',
command name. Setting this to an high value may slow down your bot
on long commands.""")))
+conf.registerGroup(Aka, 'web')
+conf.registerGlobalValue(Aka.web, 'enable',
+ registry.Boolean(False, _("""Determines whether the Akas will be
+ browsable through the HTTP server.""")))
# vim:set shiftwidth=4 tabstop=4 expandtab textwidth=79:
diff --git a/plugins/Aka/plugin.py b/plugins/Aka/plugin.py
index 63e23c4c1..e41e88a3b 100644
--- a/plugins/Aka/plugin.py
+++ b/plugins/Aka/plugin.py
@@ -41,6 +41,7 @@ import supybot.plugins as plugins
import supybot.utils.minisix as minisix
import supybot.ircutils as ircutils
import supybot.callbacks as callbacks
+import supybot.httpserver as httpserver
from supybot.i18n import PluginInternationalization
_ = PluginInternationalization('Aka')
@@ -193,6 +194,15 @@ if sqlite3:
return (bool(r[0]), r[1], r[2])
else:
raise AkaError(_('This Aka does not exist.'))
+
+ def get_all(self, channel):
+ cursor = self.get_db(channel).cursor()
+ cursor.execute("""
+ SELECT id, name, alias, locked, locked_by, locked_at
+ FROM aliases;
+ """)
+ return cursor.fetchall()
+
available_db.update({'sqlite3': SQLiteAkaDB})
elif sqlalchemy:
Base = sqlalchemy.ext.declarative.declarative_base()
@@ -324,6 +334,16 @@ elif sqlalchemy:
except sqlalchemy.orm.exc.NoResultFound:
raise AkaError(_('This Aka does not exist.'))
+ def get_all(self, channel):
+ akas = self.get_db(channel).query(
+ SQLAlchemyAlias.id, SQLAlchemyAlias.name, SQLAlchemyAlias.alias,
+ SQLAlchemyAlias.locked, SQLAlchemyAlias.locked_by, SQLAlchemyAlias.locked_at
+ ).all()
+ return map(
+ lambda aka: (aka.name, aka.alias, aka.locked, aka.locked_by, aka.locked_at),
+ akas
+ )
+
available_db.update({'sqlalchemy': SqlAlchemyAkaDB})
@@ -376,6 +396,107 @@ elif 'sqlalchemy' in conf.supybot.databases() and 'sqlalchemy' in available_db:
else:
raise plugins.NoSuitableDatabase(['sqlite3', 'sqlalchemy'])
+
+class AkaHTTPCallback(httpserver.SupyHTTPServerCallback):
+ name = 'Aka web interface'
+ base_template = '''\
+
+
+
+ Aka
+
+
+
+
+ Aka
+
+ %s
+
+'''
+ index_template = base_template % '''\
+ To view the global Akas either click here or
+ enter 'global' in the form below.
+
+ '''
+ list_template = base_template % '''\
+
+
+ Name |
+ Alias |
+ Locked |
+
+ %s
+
'''
+
+ def doGet(self, handler, path, *args, **kwargs):
+ if path == '/':
+ self.send_response(200)
+ self.send_header('Content-type', 'text/html; charset=utf-8')
+ self.end_headers()
+ self.write(self.index_template)
+
+ elif path.startswith('/list/'):
+ parts = path.split('/')
+ channel = parts[2] if len(parts) == 3 else 'global'
+ channel = utils.web.urlunquote(channel)
+
+ self.send_response(200)
+ self.send_header('Content-type', 'text/html; charset=utf-8')
+ self.end_headers()
+
+ akas = {}
+ for aka in self._plugin._db.get_all('global'):
+ akas[aka[1]] = aka
+
+ if channel != 'global':
+ for aka in self._plugin._db.get_all(channel):
+ akas[aka[1]] = aka
+
+ aka_rows = []
+ for (name, aka) in sorted(akas.items()):
+ (id, name, alias, locked, locked_by, locked_at) = aka
+ locked_column = 'False'
+ if locked:
+ locked_column = format(
+ _('By %s at %s'),
+ locked_by,
+ locked_at.split('.')[0],
+ )
+ aka_rows.append(
+ format(
+ """
+
+ %s |
+ %s
|
+ %s |
+
+ """,
+ utils.web.html_escape(name),
+ utils.web.html_escape(alias),
+ utils.web.html_escape(locked_column),
+ )
+ )
+ self.write(format(self.list_template, ''.join(aka_rows)))
+
+ def doPost(self, handler, path, form, *args, **kwargs):
+ if path == '/' and 'channel' in form:
+ self.send_response(303)
+ self.send_header(
+ 'Location',
+ format('list/%s', utils.web.urlquote(form['channel'].value))
+ )
+ self.end_headers()
+ else:
+ self.send_response(400)
+ self.send_header('Content-type', 'text/plain; charset=utf-8')
+ self.end_headers()
+ self.write('Missing field \'channel\'.')
+
+
class Aka(callbacks.Plugin):
"""Aka is the improved version of the Alias plugin. It stores akas outside
of the bot.conf, which doesn't have risk of corrupting the bot.conf file
@@ -388,6 +509,32 @@ class Aka(callbacks.Plugin):
# "sqlalchemy" is only for backward compatibility
filename = conf.supybot.directories.data.dirize('Aka.sqlalchemy.db')
self._db = AkaDB(filename)
+ self._http_running = False
+ conf.supybot.plugins.Aka.web.enable.addCallback(self._httpConfCallback)
+ if self.registryValue('web.enable'):
+ self._startHttp()
+
+ def die(self):
+ if self._http_running:
+ self._stopHttp()
+
+ def _httpConfCallback(self):
+ if self.registryValue('web.enable'):
+ if not self._http_running:
+ self._startHttp()
+ else:
+ if self._http_running:
+ self._stopHttp()
+
+ def _startHttp(self):
+ callback = AkaHTTPCallback()
+ callback._plugin = self
+ httpserver.hook('aka', callback)
+ self._http_running = True
+
+ def _stopHttp(self):
+ httpserver.unhook('aka')
+ self._http_running = False
def isCommandMethod(self, name):
args = name.split(' ')
diff --git a/plugins/Aka/test.py b/plugins/Aka/test.py
index 789949490..dc997b263 100644
--- a/plugins/Aka/test.py
+++ b/plugins/Aka/test.py
@@ -31,6 +31,7 @@
from supybot.test import *
import supybot.conf as conf
+import supybot.httpserver as httpserver
import supybot.plugin as plugin
import supybot.registry as registry
from supybot.utils.minisix import u
@@ -292,4 +293,52 @@ class AkaTestCase(PluginTestCase):
# This should be case insensitive too.
self.assertRegexp('aka search MaNY', 'many words')
+class AkaWebUITestCase(ChannelHTTPPluginTestCase):
+ plugins = ('Aka',)
+ config = {
+ 'servers.http.keepAlive': True,
+ 'plugins.Aka.web.enable': False,
+ }
+
+ def setUp(self):
+ super(ChannelHTTPPluginTestCase, self).setUp()
+ httpserver.startServer()
+
+ def tearDown(self):
+ httpserver.stopServer()
+ super(ChannelHTTPPluginTestCase, self).tearDown()
+
+ def testToggleWebEnable(self):
+ self.assertHTTPResponse('/aka/', 404)
+ self.assertNotError('config plugins.Aka.web.enable True')
+ self.assertHTTPResponse('/aka/', 200)
+ self.assertNotError('config plugins.Aka.web.enable False')
+ self.assertHTTPResponse('/aka/', 404)
+
+ def testGlobalPage(self):
+ self.assertNotError('config plugins.Aka.web.enable True')
+
+ self.assertNotError('aka add foo1 echo 1')
+ self.assertNotError('aka add --channel #foo foo2 echo 2')
+ self.assertNotError('aka add --channel #bar foo3 echo 3')
+
+ (respCode, body) = self.request('/aka/list/global')
+ self.assertEqual(respCode, 200)
+ self.assertIn(b'foo1', body)
+ self.assertNotIn(b'foo2', body)
+ self.assertNotIn(b'foo3', body)
+
+ def testChannelPage(self):
+ self.assertNotError('config plugins.Aka.web.enable True')
+
+ self.assertNotError('aka add foo1 echo 1')
+ self.assertNotError('aka add --channel #foo foo2 echo 2')
+ self.assertNotError('aka add --channel #bar foo3 echo 3')
+
+ (respCode, body) = self.request('/aka/list/%23foo')
+ self.assertEqual(respCode, 200)
+ self.assertIn(b'foo1', body)
+ self.assertIn(b'foo2', body)
+ self.assertNotIn(b'foo3', body)
+
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
diff --git a/src/httpserver.py b/src/httpserver.py
index 583c025e8..715c1c5d2 100644
--- a/src/httpserver.py
+++ b/src/httpserver.py
@@ -195,8 +195,9 @@ class RealSupyHTTPServer(HTTPServer):
self.callbacks[subdir] = callback
callback.doHook(self, subdir)
def unhook(self, subdir):
- callback = self.callbacks.pop(subdir) # May raise a KeyError. We don't care.
- callback.doUnhook(self)
+ callback = self.callbacks.pop(subdir, None)
+ if callback:
+ callback.doUnhook(self)
return callback
def __str__(self):
diff --git a/src/test.py b/src/test.py
index 1127e71ac..90ff20b06 100644
--- a/src/test.py
+++ b/src/test.py
@@ -637,8 +637,6 @@ class FakeHTTPConnection(HTTPConnection):
self.rfile = rfile
self.wfile = wfile
def send(self, data):
- if minisix.PY3 and isinstance(data, bytes):
- data = data.decode()
self.wfile.write(data)
#def putheader(self, name, value):
# self._headers[name] = value
@@ -653,14 +651,14 @@ class HTTPPluginTestCase(PluginTestCase):
def request(self, url, method='GET', read=True, data={}):
assert url.startswith('/')
- wfile = minisix.io.StringIO()
- rfile = minisix.io.StringIO()
+ wfile = minisix.io.BytesIO()
+ rfile = minisix.io.BytesIO()
connection = FakeHTTPConnection(wfile, rfile)
connection.putrequest(method, url)
connection.endheaders()
rfile.seek(0)
- wfile.seek(0)
handler = TestRequestHandler(rfile, wfile)
+ wfile.seek(0)
if read:
return (handler._response, wfile.read())
else:
diff --git a/src/utils/web.py b/src/utils/web.py
index 9589b161e..139bde9ce 100644
--- a/src/utils/web.py
+++ b/src/utils/web.py
@@ -48,6 +48,7 @@ if minisix.PY2:
from urlparse import urlsplit, urlunsplit, urlparse
from htmlentitydefs import entitydefs, name2codepoint
from HTMLParser import HTMLParser
+ from cgi import escape as html_escape
Request = urllib2.Request
urlquote = urllib.quote
urlquote_plus = urllib.quote_plus
@@ -62,6 +63,7 @@ else:
from urllib.parse import urlsplit, urlunsplit, urlparse
from html.entities import entitydefs, name2codepoint
from html.parser import HTMLParser
+ from html import escape as html_escape
import urllib.request, urllib.parse, urllib.error
Request = urllib.request.Request
urlquote = urllib.parse.quote