mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-11-26 12:49:24 +01:00
Aka: Add web UI (#1373)
* utils.web: Import html escaping functions Signed-off-by: Johannes Löthberg <johannes@kyriasis.com> * Aka: Add web interface for browsing Akas Fixes #1226. Signed-off-by: Johannes Löthberg <johannes@kyriasis.com> * httpserver: Actually handle KeyError in unhook Signed-off-by: Johannes Löthberg <johannes@kyriasis.com> * test: FakeHTTPConnection: Don't decode data in send BaseHTTPRequestHandler expects to get bytes, so we can't decode the sent data. Signed-off-by: Johannes Löthberg <johannes@kyriasis.com> * test: HTTPPluginTestCase: Use BytesIO instead of StringIO BaseHTTPRequestHandler expects bytes, not strings. Signed-off-by: Johannes Löthberg <johannes@kyriasis.com> * test: HTTPPluginTestCase: Rewind wfile to 0 before reading the response Otherwise the read pointer is at the end of the file. Signed-off-by: Johannes Löthberg <johannes@kyriasis.com> * Aka: Add basic web UI tests Signed-off-by: Johannes Löthberg <johannes@kyriasis.com>
This commit is contained in:
parent
68539da7ab
commit
27e7d6a9ac
@ -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:
|
||||
|
@ -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 = '''\
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Aka</title>
|
||||
<link rel="stylesheet" href="/default.css">
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<h1>Aka</h1>
|
||||
|
||||
%s
|
||||
</body>
|
||||
</html>'''
|
||||
index_template = base_template % '''\
|
||||
<p>To view the global Akas either click <a href="list/global">here</a> or
|
||||
enter 'global' in the form below.</p>
|
||||
|
||||
<form action="" method="post">
|
||||
<label for="channel">Channel name:</label>
|
||||
<input type="text" placeholder="#channel" name="channel" id="channel">
|
||||
<input type="submit" name="submit" value="view">
|
||||
</form>'''
|
||||
list_template = base_template % '''\
|
||||
<table>
|
||||
<thead>
|
||||
<th>Name</th>
|
||||
<th>Alias</th>
|
||||
<th>Locked</th>
|
||||
</thead>
|
||||
%s
|
||||
</table>'''
|
||||
|
||||
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(
|
||||
"""
|
||||
<tr>
|
||||
<td style="white-space: nowrap;"><code>%s</code></td>
|
||||
<td><code>%s<code></td>
|
||||
<td style="white-space: nowrap;">%s</td>
|
||||
</tr>
|
||||
""",
|
||||
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(' ')
|
||||
|
@ -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:
|
||||
|
@ -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):
|
||||
|
@ -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:
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user