mirror of
https://github.com/Mikaela/Limnoria.git
synced 2024-12-02 07:59:32 +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
|
command name. Setting this to an high value may slow down your bot
|
||||||
on long commands.""")))
|
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:
|
# 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.utils.minisix as minisix
|
||||||
import supybot.ircutils as ircutils
|
import supybot.ircutils as ircutils
|
||||||
import supybot.callbacks as callbacks
|
import supybot.callbacks as callbacks
|
||||||
|
import supybot.httpserver as httpserver
|
||||||
from supybot.i18n import PluginInternationalization
|
from supybot.i18n import PluginInternationalization
|
||||||
_ = PluginInternationalization('Aka')
|
_ = PluginInternationalization('Aka')
|
||||||
|
|
||||||
@ -193,6 +194,15 @@ if sqlite3:
|
|||||||
return (bool(r[0]), r[1], r[2])
|
return (bool(r[0]), r[1], r[2])
|
||||||
else:
|
else:
|
||||||
raise AkaError(_('This Aka does not exist.'))
|
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})
|
available_db.update({'sqlite3': SQLiteAkaDB})
|
||||||
elif sqlalchemy:
|
elif sqlalchemy:
|
||||||
Base = sqlalchemy.ext.declarative.declarative_base()
|
Base = sqlalchemy.ext.declarative.declarative_base()
|
||||||
@ -324,6 +334,16 @@ elif sqlalchemy:
|
|||||||
except sqlalchemy.orm.exc.NoResultFound:
|
except sqlalchemy.orm.exc.NoResultFound:
|
||||||
raise AkaError(_('This Aka does not exist.'))
|
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})
|
available_db.update({'sqlalchemy': SqlAlchemyAkaDB})
|
||||||
|
|
||||||
|
|
||||||
@ -376,6 +396,107 @@ elif 'sqlalchemy' in conf.supybot.databases() and 'sqlalchemy' in available_db:
|
|||||||
else:
|
else:
|
||||||
raise plugins.NoSuitableDatabase(['sqlite3', 'sqlalchemy'])
|
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):
|
class Aka(callbacks.Plugin):
|
||||||
"""Aka is the improved version of the Alias plugin. It stores akas outside
|
"""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
|
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
|
# "sqlalchemy" is only for backward compatibility
|
||||||
filename = conf.supybot.directories.data.dirize('Aka.sqlalchemy.db')
|
filename = conf.supybot.directories.data.dirize('Aka.sqlalchemy.db')
|
||||||
self._db = AkaDB(filename)
|
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):
|
def isCommandMethod(self, name):
|
||||||
args = name.split(' ')
|
args = name.split(' ')
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
from supybot.test import *
|
from supybot.test import *
|
||||||
|
|
||||||
import supybot.conf as conf
|
import supybot.conf as conf
|
||||||
|
import supybot.httpserver as httpserver
|
||||||
import supybot.plugin as plugin
|
import supybot.plugin as plugin
|
||||||
import supybot.registry as registry
|
import supybot.registry as registry
|
||||||
from supybot.utils.minisix import u
|
from supybot.utils.minisix import u
|
||||||
@ -292,4 +293,52 @@ class AkaTestCase(PluginTestCase):
|
|||||||
# This should be case insensitive too.
|
# This should be case insensitive too.
|
||||||
self.assertRegexp('aka search MaNY', 'many words')
|
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:
|
# vim:set shiftwidth=4 softtabstop=4 expandtab textwidth=79:
|
||||||
|
@ -195,7 +195,8 @@ class RealSupyHTTPServer(HTTPServer):
|
|||||||
self.callbacks[subdir] = callback
|
self.callbacks[subdir] = callback
|
||||||
callback.doHook(self, subdir)
|
callback.doHook(self, subdir)
|
||||||
def unhook(self, subdir):
|
def unhook(self, subdir):
|
||||||
callback = self.callbacks.pop(subdir) # May raise a KeyError. We don't care.
|
callback = self.callbacks.pop(subdir, None)
|
||||||
|
if callback:
|
||||||
callback.doUnhook(self)
|
callback.doUnhook(self)
|
||||||
return callback
|
return callback
|
||||||
|
|
||||||
|
@ -637,8 +637,6 @@ class FakeHTTPConnection(HTTPConnection):
|
|||||||
self.rfile = rfile
|
self.rfile = rfile
|
||||||
self.wfile = wfile
|
self.wfile = wfile
|
||||||
def send(self, data):
|
def send(self, data):
|
||||||
if minisix.PY3 and isinstance(data, bytes):
|
|
||||||
data = data.decode()
|
|
||||||
self.wfile.write(data)
|
self.wfile.write(data)
|
||||||
#def putheader(self, name, value):
|
#def putheader(self, name, value):
|
||||||
# self._headers[name] = value
|
# self._headers[name] = value
|
||||||
@ -653,14 +651,14 @@ class HTTPPluginTestCase(PluginTestCase):
|
|||||||
|
|
||||||
def request(self, url, method='GET', read=True, data={}):
|
def request(self, url, method='GET', read=True, data={}):
|
||||||
assert url.startswith('/')
|
assert url.startswith('/')
|
||||||
wfile = minisix.io.StringIO()
|
wfile = minisix.io.BytesIO()
|
||||||
rfile = minisix.io.StringIO()
|
rfile = minisix.io.BytesIO()
|
||||||
connection = FakeHTTPConnection(wfile, rfile)
|
connection = FakeHTTPConnection(wfile, rfile)
|
||||||
connection.putrequest(method, url)
|
connection.putrequest(method, url)
|
||||||
connection.endheaders()
|
connection.endheaders()
|
||||||
rfile.seek(0)
|
rfile.seek(0)
|
||||||
wfile.seek(0)
|
|
||||||
handler = TestRequestHandler(rfile, wfile)
|
handler = TestRequestHandler(rfile, wfile)
|
||||||
|
wfile.seek(0)
|
||||||
if read:
|
if read:
|
||||||
return (handler._response, wfile.read())
|
return (handler._response, wfile.read())
|
||||||
else:
|
else:
|
||||||
|
@ -48,6 +48,7 @@ if minisix.PY2:
|
|||||||
from urlparse import urlsplit, urlunsplit, urlparse
|
from urlparse import urlsplit, urlunsplit, urlparse
|
||||||
from htmlentitydefs import entitydefs, name2codepoint
|
from htmlentitydefs import entitydefs, name2codepoint
|
||||||
from HTMLParser import HTMLParser
|
from HTMLParser import HTMLParser
|
||||||
|
from cgi import escape as html_escape
|
||||||
Request = urllib2.Request
|
Request = urllib2.Request
|
||||||
urlquote = urllib.quote
|
urlquote = urllib.quote
|
||||||
urlquote_plus = urllib.quote_plus
|
urlquote_plus = urllib.quote_plus
|
||||||
@ -62,6 +63,7 @@ else:
|
|||||||
from urllib.parse import urlsplit, urlunsplit, urlparse
|
from urllib.parse import urlsplit, urlunsplit, urlparse
|
||||||
from html.entities import entitydefs, name2codepoint
|
from html.entities import entitydefs, name2codepoint
|
||||||
from html.parser import HTMLParser
|
from html.parser import HTMLParser
|
||||||
|
from html import escape as html_escape
|
||||||
import urllib.request, urllib.parse, urllib.error
|
import urllib.request, urllib.parse, urllib.error
|
||||||
Request = urllib.request.Request
|
Request = urllib.request.Request
|
||||||
urlquote = urllib.parse.quote
|
urlquote = urllib.parse.quote
|
||||||
|
Loading…
Reference in New Issue
Block a user