Browse Source

Initial commit

master
Graham Northup 5 years ago
commit
32dd2d60fe
  1. 4
      .gitignore
  2. 391
      model.py
  3. 222
      pluto.py
  4. BIN
      static/add.jpg
  5. 60
      static/edit.svg
  6. 59
      static/home.svg
  7. 60
      static/minus.svg
  8. 60
      static/plus.svg
  9. 59
      static/search.svg
  10. 69
      static/site.js
  11. 195
      static/style.css
  12. 8
      templates/404.html
  13. 13
      templates/500.html
  14. 8
      templates/act.html
  15. 9
      templates/act_delete.html
  16. 9
      templates/act_edit.html
  17. 22
      templates/api_hit.txt
  18. 21
      templates/base.html
  19. 10
      templates/cond.html
  20. 8
      templates/cond_delete.html
  21. 10
      templates/cond_edit.html
  22. 4
      templates/cond_new.html
  23. 44
      templates/debuglogs.html
  24. 25
      templates/hook.html
  25. 8
      templates/hook_delete.html
  26. 10
      templates/hook_edit.html
  27. 10
      templates/hook_new_act.html
  28. 9
      templates/hook_new_cond.html
  29. 16
      templates/hooks.html
  30. 9
      templates/hooks_new.html
  31. 30
      templates/index.html
  32. 40
      templates/logs.html
  33. 123
      templates/macros.html
  34. 34
      util.py

4
.gitignore

@ -0,0 +1,4 @@
*.pyc
secrets.*
*.db
*.swp

391
model.py

@ -0,0 +1,391 @@
import os, sqlite3, json, urllib2, ssl, urllib, time, subprocess, socket
from flask import render_template_string
from util import *
import secrets
#db = sqlite3.connect(os.path.join(os.path.dirname(__file__), 'pluto.db'), check_same_thread = False)
db = sqlite3.connect('/var/www/pluto/pluto.db', check_same_thread = False)
cur = db.cursor()
so = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
so.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
so6 = socket.socket(socket.AF_INET6, socket.SOCK_DGRAM)
so6.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
class DBError(Exception):
pass
class NoSuchEntity(DBError):
pass
class TooManyEntities(DBError):
pass
class DBObject(object):
__FIELDS__ = ()
__DEFAULTS__ = {}
__TABLE__ = ''
__TYPES__ = {}
AUTO_COMMIT = True
def __init__(self, rowid, *data):
self.rowid = rowid
for idx, field in enumerate(self.__FIELDS__):
default = self.__DEFAULTS__.get(field)
if idx < len(data):
setattr(self, field, data[idx])
else:
setattr(self, field, default)
@classmethod
def create_table(cls):
cur.execute('CREATE TABLE IF NOT EXISTS %(table)s (%(columns)s)'%\
{'table': cls.__TABLE__,
'columns': ', '.join('%s%s'%(field, ' '+cls.__TYPES__[field] if field in cls.__TYPES__ else '') for field in cls.__FIELDS__)}
)
@classmethod
def create(cls, *data):
row = list(data)
for field in cls.__FIELDS__[len(data):]:
row.append(cls.__DEFAULTS__[field])
cur.execute('INSERT INTO %(table)s VALUES (%(fields)s)'%{
'table': cls.__TABLE__,
'fields': ', '.join(['?'] * len(cls.__FIELDS__))
}, row)
if cls.AUTO_COMMIT:
db.commit()
return cls(cur.lastrowid, *row)
def delete(self):
cur.execute('DELETE FROM %(table)s WHERE ROWID=?'%{'table': self.__TABLE__}, (self.rowid,))
if self.AUTO_COMMIT:
db.commit()
def update(self):
cur.execute('UPDATE %(table)s SET %(fields)s WHERE ROWID=?'%{
'table': self.__TABLE__,
'fields': ', '.join('%s=?'%(field,) for field in self.__FIELDS__)
}, tuple(getattr(self, field) for field in self.__FIELDS__) + (self.rowid,))
if self.AUTO_COMMIT:
db.commit()
@classmethod
def get(cls, **criteria):
pairs = criteria.items()
keys = [pair[0] for pair in pairs]
values = [pair[1] for pair in pairs]
cur.execute('SELECT ROWID, %(fields)s FROM %(table)s WHERE %(criteria)s'%{
'table': cls.__TABLE__,
'fields': ', '.join(cls.__FIELDS__),
'criteria': ' and '.join('%s=?'%(k,) for k in keys),
}, values)
return [cls(*row) for row in cur]
@classmethod
def all(cls):
cur.execute('SELECT ROWID, %(fields)s FROM %(table)s'%{
'table': cls.__TABLE__,
'fields': ', '.join(cls.__FIELDS__),
})
return [cls(*row) for row in cur]
@classmethod
def sorted(cls, by, limit=None):
cur.execute('SELECT ROWID, %(fields)s FROM %(table)s ORDER BY %(by)s %(limit)s'%{
'table': cls.__TABLE__,
'fields': ', '.join(cls.__FIELDS__),
'by': by,
'limit': ('' if limit is None else 'LIMIT %d'%(limit,)),
})
return [cls(*row) for row in cur]
@classmethod
def get_one(cls, **criteria):
res = cls.get(**criteria)
if len(res) < 1:
raise NoSuchEntity(cls, criteria)
elif len(res) > 1:
raise TooManyEntities(cls, criteria)
return res[0]
def __repr__(self):
return '<%(cls)s(%(table)s %(row)d %(items)s'%{
'table': self.__TABLE__,
'cls': type(self).__name__,
'row': self.rowid,
'items': ' '.join('%s=%r'%(field, getattr(self, field)) for field in self.__FIELDS__),
}
class Log(DBObject):
__TABLE__ = 'log'
__FIELDS__ = ('time', 'path', 'headers', 'data', 'hooks')
@classmethod
def most_recent(cls, n=None):
return cls.sorted('time DESC', n)
class DebugLog(DBObject):
__TABLE__ = 'debuglog'
__FIELDS__ = ('time', 'path', 'headers', 'data', 'hook', 'cond', 'act', 'success', 'message')
@classmethod
def most_recent(cls, n=None):
return cls.sorted('time DESC', n)
class Hook(DBObject):
__TABLE__ = 'hooks'
__FIELDS__ = ('name', 'author', 'disabled', 'debugged')
__DEFAULTS__ = {
'disabled': 0,
'debugged': 0,
}
def trigger(self, path, headers, data, response):
if self.disabled:
return False
conditions = Condition.for_hook(self)
actions = Action.for_hook(self)
for condition in conditions:
result = condition.test_select(path, headers, data, response)
if self.debugged:
DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), self.rowid, condition.rowid, None, result, None)
if not result:
break
else:
for act in actions:
result = act.actuate(path, headers, data, response)
if self.debugged:
DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), self.rowid, None, act.rowid, None, result)
if self.debugged:
DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), self.rowid, None, None, True, None)
return True
if self.debugged:
DebugLog.create(time.time(), path, header_dumps(headers), jdumps(data), self.rowid, None, None, False, None)
return False
class Condition(DBObject):
__TABLE__ = 'conditions'
__FIELDS__ = ('hook', 'selector', 's1', 's2', 's3', 'test', 't1', 't2', 't3', 'invert')
@classmethod
def for_hook(cls, hook):
return cls.get(hook=hook.rowid)
def get_hook(self):
return Hook.get_one(rowid=self.hook)
def select(self, path, headers, data, response):
return getattr(self, 'select_' + self.selector, self.no_select)(path, headers, data, response)
def no_select(self, path, headers, data, response):
return None
def select_header(self, path, headers, data, response):
return headers.get(self.s1, '')
def select_JSON(self, path, headers, data, response):
if not isinstance(data, dict):
return False
cur = data
for part in self.s1.split('.'):
cur = cur.get(part)
if cur is None:
return False
return str(cur)
def select_path(self, path, headers, data, response):
return path
def test_value(self, val):
try:
result = getattr(self, 'test_' + self.test, self.no_test)(val)
except (ValueError, TypeError):
result = False
if self.invert:
result = not result
return result
def no_test(self, val):
return False
def test_equal(self, val):
return str(val) == self.t1
def test_inrange(self, val):
return float(self.t1) <= float(val) <= float(self.t2)
def test_truthy(self, val):
return bool(val)
def test_contains(self, val):
return self.t1 in val
def test_select(self, path, headers, data, response):
return self.test_value(self.select(path, headers, data, response))
class Action(DBObject):
__TABLE__ = 'actions'
__FIELDS__ = ('hook', 'action', 'a1', 'a2', 'a3')
GITLAB_API = 'https://gitlab.cosi.clarkson.edu/api/v3/'
GITLAB_TOKEN = secrets.GITLAB_TOKEN
PROTO = ssl.PROTOCOL_TLSv1_2
@classmethod
def for_hook(cls, hook):
return cls.get(hook=hook.rowid)
def get_hook(self):
return Hook.get_one(rowid=self.hook)
def actuate(self, path, headers, data, response):
try:
return getattr(self, 'act_' + self.action, self.no_act)(path, headers, data, response)
except (ValueError, TypeError):
pass
def no_act(self, path, headers, data, response):
return 'INTERNAL ERROR: ACTION NOT FOUND'
def act_post(self, path, headers, data, response):
args = {'path': path, 'headers': headers, 'data': data}
url = render_template_string(self.a1, **args)
postdata = render_template_string(self.a2, **args)
headers = json.loads(render_template_string(self.a3, **args))
print 'Note: posting to', url, 'with data', postdata, 'and headers', headers, '...'
req = urllib2.Request(url, postdata, headers)
ctxt = ssl.SSLContext(self.PROTO)
result = urllib2.urlopen(req, context=ctxt)
out = result.read()
#out = None
print 'Complete, got', repr(out)
return out
def act_gitlab(self, path, headers, data, response):
args = {'path': path, 'headers': headers, 'data': data}
url = self.GITLAB_API + render_template_string(self.a1, **args)
params = json.loads(render_template_string(self.a2, **args))
headers = json.loads(render_template_string(self.a3, **args))
headers.update({'PRIVATE-TOKEN': self.GITLAB_TOKEN})
postdata = urllib.urlencode(params)
print 'Note: posting to', url, 'with data', postdata, 'and headers', headers, '...'
req = urllib2.Request(url, postdata, headers)
ctxt = ssl.SSLContext(self.PROTO)
result = urllib2.urlopen(req, context=ctxt)
out = result.read()
#out = None
print 'Complete, got', repr(out)
return out
def act_system(self, path, headers, data, response):
args = {'path': path, 'headers': headers, 'data': data}
cmd = render_template_string(self.a1, **args)
if not self.a2:
proc = subprocess.Popen(cmd, shell=True)
return 'forked'
else:
try:
return subprocess.check_output(cmd, stderr=subprocess.STDOUT, shell=True)
except subprocess.CalledProcessError as e:
return e.output
def act_udp(self, path, headers, data, response):
args = {'path': path, 'headers': headers, 'data': data}
dest = render_template_string(self.a1, **args)
packet = render_template_string(self.a2, **args)
encoding = render_template_string(self.a3, **args)
try:
if encoding in (u'hex', u'base64'):
packet = packet.decode(encoding)
elif encoding == 'input':
packet = str(data)
elif encoding == 'json':
packet = jdumps(data) # XXX HACKS
else:
packet = packet.encode(encoding)
except Exception as e:
return 'failed to encode packet: ' + str(e)
host, _, port = dest.partition(':')
if not _:
return 'illegal specification: no port in destination'
try:
port = int(port)
except ValueError:
return 'illegal port value: ' + port
if port < 0 or port > 65535:
return 'illegal port value: ' + str(port)
try:
res = socket.getaddrinfo(host, port)
except socket.gaierror:
return 'bad hostname:' + host
for fam, tp, proto, canon, addr in res:
if tp == socket.SOCK_DGRAM:
try:
if fam == socket.AF_INET:
so.sendto(packet, addr)
return 'sent to {}: {}'.format(addr, packet.encode('hex'))
elif fam == socket.AF_INET6:
so6.sendto(packet, addr)
return 'sent to {}: {}'.format(addr, packet.encode('hex'))
except Exception:
pass
return 'no good address family found'
def act_tcp(self, path, headers, data, response):
args = {'path': path, 'headers': headers, 'data': data}
dest = render_template_string(self.a1, **args)
packet = render_template_string(self.a2, **args)
encoding = render_template_string(self.a3, **args)
try:
if encoding in (u'hex', u'base64'):
packet = packet.decode(encoding)
elif encoding == 'input':
packet = str(data)
elif encoding == 'json':
packet = jdumps(data) # XXX HACKS
else:
packet = packet.encode(encoding)
except Exception as e:
return 'failed to encode packet: ' + str(e)
host, _, port = dest.partition(':')
if not _:
return 'illegal specification: no port in destination'
try:
port = int(port)
except ValueError:
return 'illegal port value: ' + port
if port < 0 or port > 65535:
return 'illegal port value: ' + str(port)
try:
res = socket.getaddrinfo(host, port)
except socket.gaierror:
return 'bad hostname:' + host
so = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
so.settimeout(0.1)
so6 = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
so6.settimeout(0.1)
for fam, tp, proto, canon, addr in res:
if tp == socket.SOCK_STREAM:
try:
if fam == socket.AF_INET:
so.connect(addr)
so.send(packet)
return 'sent to {}: {}'.format(addr, packet.encode('hex'))
elif fam == socket.AF_INET6:
so6.connect(addr)
so6.send(packet)
return 'sent to {}: {}'.format(addr, packet.encode('hex'))
except Exception:
pass
return 'no good address family found'
def act_set_response(self, path, headers, data, response):
args = {'path': path, 'headers': headers, 'data': data}
content = render_template_string(self.a1, **args)
content_type = render_template_string(self.a2, **args)
response.set_data(content)
response.headers['Content-type'] = content_type
return 'response set to "' + content_type + '":\n' + content

222
pluto.py

@ -0,0 +1,222 @@
import time,string
from flask import Flask, render_template, redirect, url_for, request, g, make_response
from model import *
from util import *
app = Flask('pluto')
app.debug = True
@app.before_request
def init_globals():
for cls in type.__subclasses__(DBObject):
setattr(g, cls.__name__, cls)
app.jinja_env.filters['ctime'] = time.ctime
app.jinja_env.filters['jloads'] = jloads
app.jinja_env.filters['hloads'] = header_loads
app.jinja_env.filters['split'] = string.split
app.jinja_env.globals['safe_load'] = safe_load
@app.errorhandler(404)
def error_404(error):
return render_template('404.html', entity='page'), 404
@app.route('/')
def root():
return render_template('index.html')
@app.route('/hook', methods=['GET', 'POST'])
def hook():
response = make_response(render_template('api_hit.txt'))
response.headers['Content-type'] = 'text/json'
triggered = filter(lambda hook: hook.trigger(request.path, request.headers, jloads(request.data), response), Hook.all())
Log.create(time.time(), request.path, header_dumps(request.headers), request.data, ','.join(str(hook.rowid) for hook in triggered))
return response
@app.route('/logs')
def logs():
n = request.values.get('n', 10)
return render_template('logs.html', logs=Log.most_recent(n), n=n)
@app.route('/debuglogs', methods=['GET', 'POST'])
def debuglogs():
n = request.values.get('n', 10)
return render_template('debuglogs.html', logs=DebugLog.most_recent(n), n=n)
@app.route('/hooks')
def hooks():
return render_template('hooks.html', hooks=Hook.all())
@app.route('/hooks/new', methods=['GET', 'POST'])
def hooks_new():
if request.method == 'POST':
hook = Hook.create(
request.values['name'], None, # FIXME no author yet
checkbox(request, 'disabled'),
checkbox(request, 'debugged'),
)
return redirect('/hook/%d'%(hook.rowid,))
return render_template('hooks_new.html')
@app.route('/hook/<int:hookid>')
def hook_obj(hookid):
try:
hook = Hook.get_one(rowid=hookid)
except NoSuchEntity:
return render_template('404.html', entity='hook'), 404
except TooManyEntities:
return render_template('500.html', cause='too many hooks'), 500
return render_template('hook.html', hook=hook)
@app.route('/hook/<int:hookid>/edit', methods=['GET', 'POST'])
def hook_edit(hookid):
try:
hook = Hook.get_one(rowid=hookid)
except NoSuchEntity:
return render_template('404.html', entity='hook'), 404
except TooManyEntities:
return render_template('500.html', cause='too many hooks'), 500
if request.method == 'POST':
hook.name = request.values['name']
hook.disabled = checkbox(request, 'disabled')
hook.debugged = checkbox(request, 'debugged')
hook.update()
return redirect('/hook/%d'%(hook.rowid,))
return render_template('hook_edit.html', hook=hook)
@app.route('/hook/<int:hookid>/newcond', methods=['GET', 'POST'])
def hook_new_cond(hookid):
try:
hook = Hook.get_one(rowid=hookid)
except NoSuchEntity:
return render_template('404.html', entity='hook'), 404
except TooManyEntities:
return render_template('500.html', cause='too many hooks'), 500
if request.method == 'POST':
cond = Condition.create(
hook.rowid,
request.values['selector'], request.values['s1'], request.values['s2'], request.values['s3'],
request.values['test'], request.values['t1'], request.values['t2'], request.values['t3'],
checkbox(request, 'invert'),
)
return redirect('/cond/%d'%(cond.rowid,))
return render_template('hook_new_cond.html', hook=hook)
@app.route('/hook/<int:hookid>/newact', methods=['GET', 'POST'])
def hook_new_act(hookid):
try:
hook = Hook.get_one(rowid=hookid)
except NoSuchEntity:
return render_template('404.html', entity='hook'), 404
except TooManyEntities:
return render_template('500.html', cause='too many hooks'), 500
if request.method == 'POST':
act = Action.create(
hook.rowid,
request.values['action'], request.values['a1'], request.values['a2'], request.values['a3'],
)
return redirect('/act/%d'%(act.rowid,))
return render_template('hook_new_act.html', hook=hook)
@app.route('/hook/<int:hookid>/delete', methods=['GET', 'POST'])
def hook_delete(hookid):
try:
hook = Hook.get_one(rowid=hookid)
except NoSuchEntity:
return render_template('404.html', entity='hook'), 404
except TooManyEntities:
return render_template('500.html', cause='too many hooks'), 500
if request.method == 'POST':
hook.delete()
return redirect(url_for('hooks'))
return render_template('hook_delete.html', hook=hook)
@app.route('/cond/<int:condid>')
def cond_obj(condid):
try:
cond = Condition.get_one(rowid=condid)
except NoSuchEntity:
return render_template('404.html', entity='condition'), 404
except TooManyEntities:
return render_template('500.html', cause='too many conditions'), 500
return render_template('cond.html', cond=cond)
@app.route('/cond/<int:condid>/edit', methods=['GET', 'POST'])
def cond_edit(condid):
try:
cond = Condition.get_one(rowid=condid)
except NoSuchEntity:
return render_template('404.html', entity='condition'), 404
except TooManyEntities:
return render_template('500.html', cause='too many conditions'), 500
if request.method == 'POST':
cond.selector = request.values['selector']
cond.s1 = request.values['s1']
cond.s2 = request.values['s2']
cond.s3 = request.values['s3']
cond.test = request.values['test']
cond.t1 = request.values['t1']
cond.t2 = request.values['t2']
cond.t3 = request.values['t3']
cond.invert = checkbox(request, 'invert')
cond.update()
return redirect('/cond/%d'%(cond.rowid,))
return render_template('cond_edit.html', cond=cond)
@app.route('/cond/<int:condid>/delete', methods=['GET', 'POST'])
def cond_delete(condid):
try:
cond = Condition.get_one(rowid=condid)
except NoSuchEntity:
return render_template('404.html', entity='condition'), 404
except TooManyEntities:
return render_template('500.html', cause='too many conditions'), 500
if request.method == 'POST':
cond.delete()
return redirect('/hook/%d'%(cond.hook,))
return render_template('cond_delete.html', cond=cond)
@app.route('/act/<int:actid>')
def act_obj(actid):
try:
act = Action.get_one(rowid=actid)
except NoSuchEntity:
return render_template('404.html', entity='action'), 404
except TooManyEntities:
return render_template('500.html', cause='too many actions'), 500
return render_template('act.html', act=act)
@app.route('/act/<int:actid>/edit', methods=['GET', 'POST'])
def act_edit(actid):
try:
act = Action.get_one(rowid=actid)
except NoSuchEntity:
return render_template('404.html', entity='action'), 404
except TooManyEntities:
return render_template('500.html', cause='too many actions'), 500
if request.method == 'POST':
act.action = request.values['action']
act.a1 = request.values['a1']
act.a2 = request.values['a2']
act.a3 = request.values['a3']
act.update()
return redirect('/act/%d'%(act.rowid,))
return render_template('act_edit.html', act=act)
@app.route('/act/<int:actid>/delete', methods=['GET', 'POST'])
def act_delete(actid):
try:
act = Action.get_one(rowid=actid)
except NoSuchEntity:
return render_template('404.html', entity='action'), 404
except TooManyEntities:
return render_template('500.html', cause='too many actions'), 500
if request.method == 'POST':
act.delete()
return redirect('/hook/%d'%(act.hook,))
return render_template('act_delete.html', act=act)
if __name__ == '__main__':
app.run(host='0.0.0.0', port=8080)

BIN
static/add.jpg

After

Width: 480  |  Height: 360  |  Size: 18 KiB

60
static/edit.svg

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 -256 1792 1792"
id="svg3001"
version="1.1"
inkscape:version="0.48.3.1 r9886"
width="100%"
height="100%"
sodipodi:docname="pencil_font_awesome.svg">
<metadata
id="metadata3011">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3009" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview3007"
showgrid="false"
inkscape:zoom="0.13169643"
inkscape:cx="896"
inkscape:cy="896"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg3001" />
<g
transform="matrix(1,0,0,-1,121.49153,1270.2373)"
id="g3003"
style="color: #ddd;">
<path
d="M 363,0 454,91 219,326 128,235 V 128 H 256 V 0 h 107 z m 523,928 q 0,22 -22,22 -10,0 -17,-7 L 305,401 q -7,-7 -7,-17 0,-22 22,-22 10,0 17,7 l 542,542 q 7,7 7,17 z M 832,1120 1248,704 416,-128 H 0 v 416 z m 683,-96 q 0,-53 -37,-90 l -166,-166 -416,416 166,165 q 36,38 90,38 53,0 91,-38 l 235,-234 q 37,-39 37,-91 z"
id="path3005"
inkscape:connector-curvature="0"
style="fill:currentColor" />
</g>
</svg>

59
static/home.svg

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 -256 1792 1792"
id="svg3013"
version="1.1"
inkscape:version="0.48.3.1 r9886"
width="100%"
height="100%"
sodipodi:docname="home_font_awesome.svg">
<metadata
id="metadata3023">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3021" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview3019"
showgrid="false"
inkscape:zoom="0.13169643"
inkscape:cx="896"
inkscape:cy="896"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg3013" />
<g
transform="matrix(1,0,0,-1,68.338983,1285.4237)"
id="g3015">
<path
d="M 1408,544 V 64 Q 1408,38 1389,19 1370,0 1344,0 H 960 V 384 H 704 V 0 H 320 q -26,0 -45,19 -19,19 -19,45 v 480 q 0,1 0.5,3 0.5,2 0.5,3 l 575,474 575,-474 q 1,-2 1,-6 z m 223,69 -62,-74 q -8,-9 -21,-11 h -3 q -13,0 -21,7 L 832,1112 140,535 q -12,-8 -24,-7 -13,2 -21,11 l -62,74 q -8,10 -7,23.5 1,13.5 11,21.5 l 719,599 q 32,26 76,26 44,0 76,-26 l 244,-204 v 195 q 0,14 9,23 9,9 23,9 h 192 q 14,0 23,-9 9,-9 9,-23 V 840 l 219,-182 q 10,-8 11,-21.5 1,-13.5 -7,-23.5 z"
id="path3017"
inkscape:connector-curvature="0"
style="fill:currentColor" />
</g>
</svg>

60
static/minus.svg

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 -256 1792 1792"
id="svg2989"
version="1.1"
inkscape:version="0.48.3.1 r9886"
width="100%"
height="100%"
sodipodi:docname="minus_sign_font_awesome.svg">
<metadata
id="metadata2999">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs2997" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview2995"
showgrid="false"
inkscape:zoom="0.13169643"
inkscape:cx="896"
inkscape:cy="896"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg2989" />
<g
transform="matrix(1,0,0,-1,129.08475,1277.8305)"
id="g2991"
style="color: #ddd;">
<path
d="m 1216,576 v 128 q 0,26 -19,45 -19,19 -45,19 H 384 q -26,0 -45,-19 -19,-19 -19,-45 V 576 q 0,-26 19,-45 19,-19 45,-19 h 768 q 26,0 45,19 19,19 19,45 z m 320,64 Q 1536,431 1433,254.5 1330,78 1153.5,-25 977,-128 768,-128 559,-128 382.5,-25 206,78 103,254.5 0,431 0,640 0,849 103,1025.5 206,1202 382.5,1305 559,1408 768,1408 977,1408 1153.5,1305 1330,1202 1433,1025.5 1536,849 1536,640 z"
id="path2993"
inkscape:connector-curvature="0"
style="fill:currentColor" />
</g>
</svg>

60
static/plus.svg

@ -0,0 +1,60 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 -256 1792 1792"
id="svg3025"
version="1.1"
inkscape:version="0.48.3.1 r9886"
width="100%"
height="100%"
sodipodi:docname="plus_sign_font_awesome.svg">
<metadata
id="metadata3035">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3033" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview3031"
showgrid="false"
inkscape:zoom="0.13169643"
inkscape:cx="896"
inkscape:cy="896"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg3025" />
<g
transform="matrix(1,0,0,-1,113.89831,1277.8305)"
id="g3027"
style="color: #ddd;">
<path
d="m 1216,576 v 128 q 0,26 -19,45 -19,19 -45,19 H 896 v 256 q 0,26 -19,45 -19,19 -45,19 H 704 q -26,0 -45,-19 -19,-19 -19,-45 V 768 H 384 q -26,0 -45,-19 -19,-19 -19,-45 V 576 q 0,-26 19,-45 19,-19 45,-19 H 640 V 256 q 0,-26 19,-45 19,-19 45,-19 h 128 q 26,0 45,19 19,19 19,45 v 256 h 256 q 26,0 45,19 19,19 19,45 z m 320,64 Q 1536,431 1433,254.5 1330,78 1153.5,-25 977,-128 768,-128 559,-128 382.5,-25 206,78 103,254.5 0,431 0,640 0,849 103,1025.5 206,1202 382.5,1305 559,1408 768,1408 977,1408 1153.5,1305 1330,1202 1433,1025.5 1536,849 1536,640 z"
id="path3029"
inkscape:connector-curvature="0"
style="fill:currentColor" />
</g>
</svg>

59
static/search.svg

@ -0,0 +1,59 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
viewBox="0 -256 1792 1792"
id="svg3025"
version="1.1"
inkscape:version="0.48.3.1 r9886"
width="100%"
height="100%"
sodipodi:docname="search_font_awesome.svg">
<metadata
id="metadata3035">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
</cc:Work>
</rdf:RDF>
</metadata>
<defs
id="defs3033" />
<sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="640"
inkscape:window-height="480"
id="namedview3031"
showgrid="false"
inkscape:zoom="0.13169643"
inkscape:cx="896"
inkscape:cy="896"
inkscape:window-x="0"
inkscape:window-y="25"
inkscape:window-maximized="0"
inkscape:current-layer="svg3025" />
<g
transform="matrix(1,0,0,-1,60.745763,1201.8983)"
id="g3027">
<path
d="m 1152,704 q 0,185 -131.5,316.5 Q 889,1152 704,1152 519,1152 387.5,1020.5 256,889 256,704 256,519 387.5,387.5 519,256 704,256 889,256 1020.5,387.5 1152,519 1152,704 z m 512,-832 q 0,-52 -38,-90 -38,-38 -90,-38 -54,0 -90,38 L 1103,124 Q 924,0 704,0 561,0 430.5,55.5 300,111 205.5,205.5 111,300 55.5,430.5 0,561 0,704 q 0,143 55.5,273.5 55.5,130.5 150,225 94.5,94.5 225,150 130.5,55.5 273.5,55.5 143,0 273.5,-55.5 130.5,-55.5 225,-150 94.5,-94.5 150,-225 Q 1408,847 1408,704 1408,484 1284,305 l 343,-343 q 37,-37 37,-90 z"
id="path3029"
inkscape:connector-curvature="0"
style="fill:currentColor" />
</g>
</svg>

69
static/site.js

@ -0,0 +1,69 @@
const ACTION_SCHEMA = {
'post': ['URL template', 'POST data template', 'Headers JSON template'],
'gitlab': ['API subpath', 'POST data JSON template', 'Headers JSON template'],
'system': ['Command', "Don't fork"],
'udp': ['Destination', 'Packet Data', 'Codec'],
'tcp': ['Destination', 'Packet Data', 'Codec'],
'set_response': ['Content', 'Content Type'],
};
const SELECTOR_SCHEMA = {
'header': ['Header name'],
'JSON': ['Object path'],
'path': [],
};
const TEST_SCHEMA = {
'equal': ['String value'],
'inrange': ['Lower bound', 'Upper bound'],
'truthy': [],
'contains': ['String value'],
};
function reg_action_select(select, laba1, laba2, laba3) {
select.addEventListener("change", function() {
var schema = ACTION_SCHEMA[this.value];
if(!schema) {
laba1.textContent = "(Unknown schema)";
laba2.textContent = "(Unknown schema)";
laba3.textContent = "(Unknown schema)";
} else {
laba1.textContent = (schema.length > 0 ? schema[0] : "Unused");
laba2.textContent = (schema.length > 1 ? schema[1] : "Unused");
laba3.textContent = (schema.length > 2 ? schema[2] : "Unused");
}
});
select.dispatchEvent(new Event("change"));
}
function reg_selector_select(select, labs1, labs2, labs3) {
select.addEventListener("change", function() {
var schema = SELECTOR_SCHEMA[this.value];
if(!schema) {
labs1.textContent = "(Unknown schema)";
labs2.textContent = "(Unknown schema)";
labs3.textContent = "(Unknown schema)";
} else {
labs1.textContent = (schema.length > 0 ? schema[0] : "Unused");
labs2.textContent = (schema.length > 1 ? schema[1] : "Unused");
labs3.textContent = (schema.length > 2 ? schema[2] : "Unused");
}
});
select.dispatchEvent(new Event("change"));
}
function reg_test_select(select, labt1, labt2, labt3) {
select.addEventListener("change", function() {
var schema = TEST_SCHEMA[this.value];
if(!schema) {
labt1.textContent = "(Unknown schema)";
labt2.textContent = "(Unknown schema)";
labt3.textContent = "(Unknown schema)";
} else {
labt1.textContent = (schema.length > 0 ? schema[0] : "Unused");
labt2.textContent = (schema.length > 1 ? schema[1] : "Unused");
labt3.textContent = (schema.length > 2 ? schema[2] : "Unused");
}
});
select.dispatchEvent(new Event("change"));
}

195
static/style.css

@ -0,0 +1,195 @@
body {
background-color: #eee;
color: #222;
padding: 0 10%;
font-size: 125%;
}
@media (max-width: 768px) {
body {
padding: 0 2%;
}
}
table {
background-color: #ddd;
}
span.monospace {
font-family: monospace;
border: 1px solid #777;
border-radius: 3px;
white-space: pre;
display: inline-block;
padding: 5px;
background-color: #fff;
}
table {
border-collapse: collapse;
border: 1px solid #777;
}
table td, table th {
padding: 10px;
border: 1px solid #777;
}
#logs.debug {
font-size: 75%;
}
#logs .headers, #logs .data {
white-space: pre-wrap;
font-family: monospace;
background-color: #eee;
}
.nodata {
font-style: italic;
color: #700;
}
.positive {
color: #0a0;
}
.negative {
color: #a00;
}
div.error {
background-color: #edd;
border: 1px solid #a77;
border-radius: 15px;
width: 100%;
padding: 25px;
}
.posbutton a {
display: inline-block;
background-color: #080;
color: #ddd;
border: 2px solid #060;
border-radius: 5px;
padding: 10px;
margin: 5px;
font-weight: bolder;
text-decoration: none;
}
.posbutton a:hover {
background-color: #0a0;
}
.posbutton a:active {
background-color: #ddd;
color: #0a0;
}
.negbutton a {
display: inline-block;
background-color: #800;
color: #ddd;
border: 2px solid #600;
border-radius: 5px;
padding: 10px;
margin: 5px;
font-weight: bolder;
text-decoration: none;
}
.negbutton a:hover {
background-color: #a00;
}
.negbutton a:active {
background-color: #ddd;
color: #a00;
}
.neutbutton a {
display: inline-block;
background-color: #008;
color: #ddd;
border: 2px solid #006;
border-radius: 5px;
padding: 10px;
margin: 5px;
font-weight: bolder;
text-decoration: none;
}
.neutbutton a:hover {
background-color: #00a;
}
.neutbutton a:active {
background-color: #ddd;
color: #00a;
}
.addbutton a::after {
margin-left: 10px;
margin-bottom: -3px;
background-image: url('/static/plus.svg');
background-size: 25px 25px;
width: 25px;
height: 25px;
display: inline-block;
content: "";
color: #f0f;
}
.rembutton a::after {
margin-left: 10px;
margin-bottom: -5px;
background-image: url('/static/minus.svg');
background-size: 25px 25px;
width: 25px;
height: 25px;
display: inline-block;
content: "";
}
.edbutton a::after {
margin-left: 10px;
margin-bottom: -5px;
background-image: url('/static/edit.svg');
background-size: 25px 25px;
width: 25px;
height: 25px;
display: inline-block;
content: "";
}
.srchbutton a::after {
margin-left: 10px;
margin-bottom: -5px;
background-image: url('/static/search.svg');
background-size: 25px 25px;
width: 25px;
height: 25px;
display: inline-block;
content: "";
}
.hacks-flr {
float: right;
}
#home {
width: 46px;
height: 46px;
padding-right: 10px;
float: left;
}
.flex-center {
display: flex;
justify-content: center;
}
.flex-center > div {
margin: 0 50px;
}

8
templates/404.html

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block title %}Pluto - Error 404{% endblock %}
{% block content %}
<div class="error">
There is no such {{ entity | default('entity') }} identified by your request.
</div>
{% endblock %}

13
templates/500.html

@ -0,0 +1,13 @@
{% extends "base.html" %}
{% block title %}Pluto - Error 500{% endblock %}
{% block content %}
<div class="error">
An error occurred while processing your request.<br/>
The cause is:
{% if cause %}
<span class="monospace">{{ cause }}</span>
{% else %}
<span class="nodata">No cause given.</span>
{% endif %}
</div>
{% endblock %}

8
templates/act.html

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block title %}Pluto - Action {{act.rowid}}{% endblock %}
{% block content %}
<p>This is action {{ act.rowid }} for {{ macros.hook(act.get_hook()) }}:</p>
<div class="neutbutton edbutton hacks-flr"><a href="/act/{{ act.rowid }}/edit">Edit</a></div>
<div class="negbutton rembutton hacks-flr"><a href="/act/{{ act.rowid }}/delete">Delete</a></div>
<p>Action: {{ act.action }} ({{ act.a1 | pprint }}, {{ act.a2 | pprint }}, {{ act.a3 | pprint }})</p>
{% endblock %}

9
templates/act_delete.html

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block title %}Pluto - Delete Action {{ act.rowid }}{% endblock %}
{% block content %}
<p>Are you sure you want to delete {{ macros.act(act) }} of {{ macros.hook(act.get_hook()) }}?</p>
<form action="?" method="POST">
<button type="submit">Confirm</button>
</form>
{% endblock %}

9
templates/act_edit.html

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block title %}Pluto - Edit Action {{ act.rowid }}{% endblock %}
{% block content %}
<p>Edit action {{ macros.act(act) }}:</p>
<form action="?" method="POST">
{{ macros.act_params(act) }}
<button type="submit">Submit</button>
</form>
{% endblock %}

22
templates/api_hit.txt

@ -0,0 +1,22 @@
{
"version": "1.0",
"sessionAttributes": {},
"response": {
"outputSpeech": {
"type": "PlainText",
"text": "Welcome to the Alexa Skills Kit sample, Please tell me your favorite color by saying, my favorite color is red"
},
"card": {
"type": "Simple",
"title": "SessionSpeechlet - Welcome",
"content": "SessionSpeechlet - Welcome to the Alexa Skills Kit sample, Please tell me your favorite color by saying, my favorite color is red"
},
"reprompt": {
"outputSpeech": {
"type": "PlainText",
"text": "Please tell me your favorite color by saying, my favorite color is red"
}
},
"shouldEndSession": false
}
}

21
templates/base.html

@ -0,0 +1,21 @@
{% import "macros.html" as macros %}
<!DOCTYPE HTML>
<html>
<head>
{% block head %}
<title>{% block title %}{% endblock %}</title>
<link rel="stylesheet" type="text/css" href="/static/style.css"/>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<script type="text/javascript" src="/static/site.js"></script>
{% endblock %}
</head>
<body>
{% block header %}
<h1><a href="/"><img id="home" src="/static/home.svg"></a></h1>
<h1>{{ self.title() }}</h1>
{% endblock %}
<div style="clear: left;"></div>
{% block content %}
{% endblock %}
</body>
</html>

10
templates/cond.html

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block title %}Pluto - Condition {{cond.rowid}}{% endblock %}
{% block content %}
<p>This is condition {{ cond.rowid }} for {{ macros.hook(cond.get_hook()) }}:</p>
<div class="neutbutton edbutton hacks-flr"><a href="/cond/{{ cond.rowid }}/edit">Edit</a></div>
<div class="negbutton rembutton hacks-flr"><a href="/cond/{{ cond.rowid }}/delete">Delete</a></div>
<p>Selector: {{ cond.selector }} ({{ cond.s1 | pprint }}, {{ cond.s2 | pprint }}, {{ cond.s3 | pprint }})</p>
<p>Test: {{ cond.test }} ({{ cond.t1 | pprint }}, {{ cond.t2 | pprint }}, {{ cond.t3 | pprint }})</p>
<p>Inverted: {% if cond.invert %}Yes{% else %}No{% endif %}</p>
{% endblock %}

8
templates/cond_delete.html

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block title %}Pluto - Delete Condition {{ cond.rowid }}{% endblock %}
{% block content %}
<p>Are you sure you want to delete {{ macros.cond(cond) }} of {{ macros.hook(cond.get_hook()) }}?</p>
<form action="?" method="POST">
<button type="submit">Confirm</button>
</form>
{% endblock %}

10
templates/cond_edit.html

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block title %}Pluto - Edit Condition {{ cond.rowid }}{% endblock %}
{% block content %}
<p>Edit condition {{ macros.cond(cond) }}:</p>
<form action="?" method="POST">
{{ macros.cond_params(cond) }}
<button type="submit">Submit</button>
</form>
{% endblock %}

4
templates/cond_new.html

@ -0,0 +1,4 @@
{% extends "base.html" %}
{% block title %}Pluto - New Condition{% endblock %}
{% block content %}
<p>

44
templates/debuglogs.html

@ -0,0 +1,44 @@
{% extends "base.html" %}
{% block title %}Pluto - Logs{% endblock %}
{% block content %}
<p>
Here are the most recent {{ n }} entries in the debug log:
</p>
<table id="logs" class="debug">
<tr>
<th>Time</th>
<th>Path</th>
<th>Headers</th>
<th>Data</th>
<th>Hook</th>
<th>Actor</th>
<th>Success</th>
<th>Message</th>
</tr>
{% for log in logs %}
<tr>
<td class="time">{{ log.time | ctime }}</td>
<td class="path">{{ log.path }}</td>
<td class="headers">{{ log.headers | hloads | pprint(True) }}</td>
<td class="data">{{ log.data | jloads | pprint(True) }}</td>
<td class="hook">{{ macros.hook_id(log.hook) }}</td>
<td class="actor">
{% if log.cond %}
{{ macros.cond_id(log.cond) }}
{% elif log.act %}
{{ macros.act_id(log.act) }}
{% else %}
{{ macros.hook_id(log.hook) }}
{% endif %}
</td>
<td class="success">{{ macros.yesno(log.success) }}</td>
<td class="message">{{ log.message }}</td>
</tr>
{% else %}
<tr>
<td colspan="8" class="nodata">No data.</td>
</tr>
{% endfor %}
</table>
{% endblock %}

25
templates/hook.html

@ -0,0 +1,25 @@
{% extends "base.html" %}
{% block title %}Pluto - Hook {{hook.name}}{% endblock %}
{% block content %}
<p>This is hook {{ hook.rowid }}, named "{{ hook.name }}", presently {% if hook.disabled %}<span class="negative">disabled</span>{% else %}<span class="positive">enabled</span>{% endif %}, presently {% if hook.debugged %}<span class="positive">debugged (<a href="/debuglogs">see entries</a>)</span>{% else %}<span class="negative">not debugged</span>{% endif %}.</p>
<div class="neutbutton edbutton hacks-flr"><a href="/hook/{{ hook.rowid }}/edit">Edit</a></div>
<div class="negbutton rembutton hacks-flr"><a href="/hook/{{ hook.rowid }}/delete">Delete</a></div>
<p>Conditions for activation:</p>
<ul>
{% for cond in g.Condition.for_hook(hook) %}
<li>{{ macros.cond(cond) }}</li>
{% else %}
<li class="nodata">No conditions</li>
{% endfor %}
<li class="posbutton addbutton"><a href="/hook/{{ hook.rowid }}/newcond">Add condition</a></li>
</ul>
<p>Actions upon activation:</p>
<ul>
{% for act in g.Action.for_hook(hook) %}
<li>{{ macros.act(act) }}</li>
{% else %}
<li class="nodata">No actions</li>
{% endfor %}
<li class="posbutton addbutton"><a href="/hook/{{ hook.rowid }}/newact">Add action</a></li>
</ul>
{% endblock %}

8
templates/hook_delete.html

@ -0,0 +1,8 @@
{% extends "base.html" %}
{% block title %}Pluto - Delete Hook {{ hook.name }}{% endblock %}
{% block content %}
<p>Are you sure you want to delete {{ macros.hook(hook) }}?</p>
<form action="?" method="POST">
<button type="submit">Confirm</button>
</form>
{% endblock %}

10
templates/hook_edit.html

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block title %}Pluto - Edit Hook {{ hook.name }}{% endblock %}
{% block content %}
<p>Edit hook {{ macros.hook(hook) }}:</p>
<form action="?" method="POST">
{{ macros.hook_params(hook) }}
<button type="submit">Submit</button>
</form>
{% endblock %}

10
templates/hook_new_act.html

@ -0,0 +1,10 @@
{% extends "base.html" %}
{% block title %}Pluto - New Action{% endblock %}
{% block content %}
<p>Add an action to {{ macros.hook(hook) }}:</p>
<form action="?" method="POST">
{{ macros.act_params(None) }}
<button type="submit">Add</button>
</form>
{% endblock %}

9
templates/hook_new_cond.html

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block title %}Pluto - New Condition{% endblock %}
{% block content %}
<p>Add a condition to {{ macros.hook(hook) }}:</p>
<form action="?" method="POST">
{{ macros.cond_params(None) }}
<button type="submit">Add</button>
</form>
{% endblock %}

16
templates/hooks.html

@ -0,0 +1,16 @@
{% extends "base.html" %}
{% block title %}Pluto - Hooks{% endblock %}
{% block content %}
<p>
Here are the active hooks:
</p>
<ul>
{% for hook in hooks %}
<li>{{ macros.hook(hook) }}</li>
{% else %}
<li class="nodata">No data.</li>
{% endfor %}
<li class="posbutton addbutton"><a href="/hooks/new">Add hook</a></li>
</ul>
{% endblock %}

9
templates/hooks_new.html

@ -0,0 +1,9 @@
{% extends "base.html" %}
{% block title %}Pluto - New Hook{% endblock %}
{% block content %}
<p>Add a new hook:</p>
<form action="?" method="POST">
{{ macros.hook_params(None) }}
<button type="submit">Add</button>
</form>
{% endblock %}

30
templates/index.html

@ -0,0 +1,30 @@
{% extends "base.html" %}
{% block title %}Pluto{% endblock %}
{% block content %}
<h2>A Webhook Hacking Server</h2>
<h3>Sponsored by <a href="https://gitlab.cosi.clarkson.edu/COSI/">COSI</a></h3>
<p>
Welcome to Pluto! This server is a test server useful for deploying
quick, one-off scripts to integrate with "webhooks"&mdash;HTTP
requests initiated by services in response to events.
</p>
<div class="flex-center">
<div class="neutbutton srchbutton"><a href="/logs">Logs</a></div>
<div class="neutbutton srchbutton"><a href="/debuglogs">Debug Logs</a></div>
<div class="neutbutton srchbutton"><a href="/hooks">Hooks</a></div>
</div>
<p>
If you'd like to use Pluto for one of your projects, point the URL
for a webhook at <span
class="monospace">http://pluto.cslabs.clarkson.edu/hook</span> or
<span
class="monospace">https://pluto.cslabs.clarkson.edu/hook</span>,
then actuate the hook a few times, and <a href="/logs">check the
logs</a> to see what kind of requests are being sent. Then, <a
href="/hooks">add a hook</a>
in the hooks table.
</p>
{% endblock %}

40
templates/logs.html

@ -0,0 +1,40 @@
{% extends "base.html" %}
{% block title %}Pluto - Logs{% endblock %}
{% block content %}
<p>
Here are the last {{ n }} requests observed to <span class="monospace">/hook</span>:
</p>
<table id="logs">
<tr>
<th>Time</th>
<th>Path</th>
<th>Headers</th>
<th>Data</th>