Fixed up the remotes documentation.

Set up routing and view methods in HttpConnection.
This commit is contained in:
Raoul Snyman 2011-02-25 23:43:20 +02:00
parent 40a1d59a07
commit fde2547dee
2 changed files with 164 additions and 71 deletions

View File

@ -17,3 +17,9 @@ Helper Classes & Functions
.. automodule:: openlp.plugins.remotes.lib .. automodule:: openlp.plugins.remotes.lib
:members: :members:
.. autoclass:: openlp.plugins.remotes.lib.httpserver.HttpConnection
:members:
.. autoclass:: openlp.plugins.remotes.lib.httpserver.HttpResponse
:members:

View File

@ -28,6 +28,7 @@ import logging
import os import os
import urlparse import urlparse
import re import re
from pprint import pformat
try: try:
import json import json
@ -45,12 +46,18 @@ class HttpResponse(object):
""" """
A simple object to encapsulate a pseudo-http response. A simple object to encapsulate a pseudo-http response.
""" """
content = u'' code = '200 OK'
mimetype = None content = ''
headers = {
'Content-Type': 'text/html; charset="utf-8"\r\n'
}
def __init__(self, content=u'', mimetype=None): def __init__(self, content='', headers={}, code=None):
self.content = content self.content = content
self.mimetype = mimetype for key, value in headers.iteritems():
self.headers[key] = value
if code:
self.code = code
class HttpServer(object): class HttpServer(object):
@ -110,6 +117,7 @@ class HttpServer(object):
Item (song) change listener. Store the slide and tell the clients Item (song) change listener. Store the slide and tell the clients
""" """
self.current_item = items[0].title self.current_item = items[0].title
log.debug(pformat(items[0].__dict__, 2))
self.send_poll() self.send_poll()
def send_poll(self): def send_poll(self):
@ -148,20 +156,86 @@ class HttpConnection(object):
""" """
A single connection, this handles communication between the server A single connection, this handles communication between the server
and the client and the client
Routes:
``/``
Go to the web interface.
``/files/{filename}``
Serve a static file.
``/api/poll``
Poll to see if there are any changes. Returns a JSON-encoded dict of
any changes that occurred::
{"results": {"type": "controller"}}
Or, if there were no results, False::
{"results": False}
``/api/controller/{live|preview}/{action}``
Perform ``{action}`` on the live or preview controller. Valid actions
are:
``next``
Load the next slide.
``previous``
Load the previous slide.
``jump``
Jump to a specific slide. Requires an id return in a JSON-encoded
dict like so::
{"request": {"id": 1}}
``first``
Load the first slide.
``last``
Load the last slide.
``text``
Request the text of the current slide.
``/api/service/{action}``
Perform ``{action}`` on the service manager (e.g. go live). Data is
passed as a json-encoded ``data`` parameter. Valid actions are:
``next``
Load the next item in the service.
``previous``
Load the previews item in the service.
``jump``
Jump to a specific item in the service. Requires an id returned in
a JSON-encoded dict like so::
{"request": {"id": 1}}
``list``
Request a list of items in the service.
""" """
def __init__(self, parent, socket): def __init__(self, parent, socket):
""" """
Initialise the http connection. Listen out for socket signals Initialise the http connection. Listen out for socket signals.
""" """
log.debug(u'Initialise HttpConnection: %s' % log.debug(u'Initialise HttpConnection: %s' %
socket.peerAddress().toString()) socket.peerAddress().toString())
self.socket = socket self.socket = socket
self.parent = parent self.parent = parent
self.routes = [ self.routes = [
(u'^/$', self.serve_file, [u'filename'], {u'filename': u''}), (u'^/$', self.serve_file),
(r'^/files/(?P<filename>.*)$', self.serve_file, [u'filename'], None), (r'^/files/(.*)$', self.serve_file),
(r'^/send/(?P<name>.*)$', self.process_event, [u'name', u'parameters'], None), #(r'^/send/(.*)$', self.process_event),
(r'^/request/(?P<name>.*)$', self.process_request, [u'name', u'parameters'], None) #(r'^/request/(.*)$', self.process_request),
(r'^/api/poll$', self.poll),
(r'^/api/controller/(live|preview)/(.*)$', self.controller),
(r'^/api/service/(.*)$', self.service)
] ]
QtCore.QObject.connect(self.socket, QtCore.SIGNAL(u'readyRead()'), QtCore.QObject.connect(self.socket, QtCore.SIGNAL(u'readyRead()'),
self.ready_read) self.ready_read)
@ -180,57 +254,34 @@ class HttpConnection(object):
response = None response = None
if words[0] == u'GET': if words[0] == u'GET':
url = urlparse.urlparse(words[1]) url = urlparse.urlparse(words[1])
params = self.load_params(url.query) self.url_params = urlparse.parse_qs(url.query)
# Loop through the routes we set up earlier and execute them # Loop through the routes we set up earlier and execute them
for route, func, kws, defaults in self.routes: for route, func in self.routes:
match = re.match(route, url.path) match = re.match(route, url.path)
if match: if match:
log.debug(u'Matched on "%s" from "%s"', route, url.path) log.debug('Route "%s" matched "%s"', route, url.path)
log.debug(u'Groups: %s', match.groups()) args = []
kwargs = {} for param in match.groups():
# Loop through all the keywords supplied args.append(param)
for keyword in kws: response = func(*args)
groups = match.groupdict()
if keyword in groups:
# If we find a valid keyword in our URL, use it
kwargs[keyword] = groups[keyword]
elif defaults and keyword in defaults:
# Otherwise, if we have defaults, use them
kwargs[keyword] = defaults[keyword]
else:
# Lastly, set our parameter to None.
kwargs[keyword] = None
if u'parameters' in kwargs:
kwargs[u'parameters'] = params
log.debug(u'Keyword arguments: %s', kwargs)
response = func(**kwargs)
break break
"""
folders = url.path.split(u'/')
if folders[1] == u'':
mimetype, html = self.serve_file(u'')
elif folders[1] == u'files':
mimetype, html = self.serve_file(os.sep.join(folders[2:]))
elif folders[1] == u'send':
html = self.process_event(folders[2], params)
elif folders[1] == u'request':
if self.process_request(folders[2], params):
return
"""
if response: if response:
self.send_response(response)
"""
if hasattr(response, u'mimetype'): if hasattr(response, u'mimetype'):
self.send_200_ok(response.mimetype) self.send_200_ok(response.mimetype)
else: else:
self.send_200_ok() self.send_200_ok()
if hasattr(response, u'content'): if hasattr(response, u'content'):
self.socket.write(response.content) self.socket.write(response.content)
else: elif isinstance(response, basestring):
self.socket.write(response) self.socket.write(response)
"""
else: else:
self.send_404_not_found() self.send_response(HttpResponse(code='404 Not Found'))
self.close() self.close()
def serve_file(self, **kwargs): def serve_file(self, filename=None):
""" """
Send a file to the socket. For now, just a subset of file types Send a file to the socket. For now, just a subset of file types
and must be top level inside the html folder. and must be top level inside the html folder.
@ -239,13 +290,12 @@ class HttpConnection(object):
Ultimately for i18n, this could first look for xx/file.html before Ultimately for i18n, this could first look for xx/file.html before
falling back to file.html... where xx is the language, e.g. 'en' falling back to file.html... where xx is the language, e.g. 'en'
""" """
filename = kwargs.get(u'filename', u'')
log.debug(u'serve file request %s' % filename) log.debug(u'serve file request %s' % filename)
if not filename: if not filename:
filename = u'index.html' filename = u'index.html'
path = os.path.normpath(os.path.join(self.parent.html_dir, filename)) path = os.path.normpath(os.path.join(self.parent.html_dir, filename))
if not path.startswith(self.parent.html_dir): if not path.startswith(self.parent.html_dir):
return None return HttpResponse(code=u'404 Not Found')
ext = os.path.splitext(filename)[1] ext = os.path.splitext(filename)[1]
if ext == u'.html': if ext == u'.html':
mimetype = u'text/html' mimetype = u'text/html'
@ -260,30 +310,62 @@ class HttpConnection(object):
elif ext == u'.png': elif ext == u'.png':
mimetype = u'image/png' mimetype = u'image/png'
else: else:
return (None, None) mimetype = u'text/plain'
file_handle = None file_handle = None
try: try:
file_handle = open(path, u'rb') file_handle = open(path, u'rb')
log.debug(u'Opened %s' % path) log.debug(u'Opened %s' % path)
html = file_handle.read() content = file_handle.read()
except IOError: except IOError:
log.exception(u'Failed to open %s' % path) log.exception(u'Failed to open %s' % path)
return None return HttpResponse(code=u'404 Not Found')
finally: finally:
if file_handle: if file_handle:
file_handle.close() file_handle.close()
return HttpResponse(content=html, mimetype=mimetype) return HttpResponse(content, {u'Content-Type': mimetype})
def load_params(self, query): def poll(self):
""" """
Decode the query string parameters sent from the browser Poll OpenLP to determine the current slide number and item name.
""" """
log.debug(u'loading params %s' % query) return HttpResponse(json.dumps({'slide': self.parent.current_slide,
params = urlparse.parse_qs(query) 'item': self.parent.current_item}),
if not params: {'Content-Type': 'application/json'})
return None
def controller(self, type, action):
"""
Perform an action on the slide controller.
``type``
This is the type of slide controller, either ``preview`` or
``live``.
``action``
The action to perform.
"""
event = u'slidecontroller_%s_%s' % (type, action)
if action == u'text':
event += u'_request'
Receiver.send_message(event)
json_data = {u'results': {u'success': True}}
#if action == u'text':
# json_data = {u'results': }
return HttpResponse(json.dumps(json_data),
{'Content-Type': 'application/json'})
def service(self, action):
event = u'servicemanager_%s' % action
if action == u'list':
event += u'_request'
else: else:
return params['q'] event += u'_item'
if self.url_params and self.url_params.get(u'data'):
data = json.loads(self.url_params[u'data'][0])
Receiver.send_message(event, data[u'request'][u'id'])
else:
Receiver.send_message(event)
return HttpResponse(json.dumps({'results': {u'success': True}}),
{'Content-Type': 'application/json'})
def process_event(self, **kwargs): def process_event(self, **kwargs):
""" """
@ -291,11 +373,10 @@ class HttpConnection(object):
Currently lets anything through. Later we should restrict and perform Currently lets anything through. Later we should restrict and perform
basic parameter checking, otherwise rogue clients could crash openlp basic parameter checking, otherwise rogue clients could crash openlp
""" """
event = kwargs.get(u'name') event = kwargs.get(u'event')
params = kwargs.get(u'parameters')
log.debug(u'Processing event %s' % event) log.debug(u'Processing event %s' % event)
if params: if self.url_params:
Receiver.send_message(event, params) Receiver.send_message(event, self.url_params)
else: else:
Receiver.send_message(event) Receiver.send_message(event)
return json.dumps([u'OK']) return json.dumps([u'OK'])
@ -317,11 +398,10 @@ class HttpConnection(object):
``params`` ``params``
Parameters sent with the event. Parameters sent with the event.
""" """
event = kwargs.get(u'name') event = kwargs.get(u'event')
params = kwargs.get(u'parameters')
log.debug(u'Processing request %s' % event) log.debug(u'Processing request %s' % event)
if not event.endswith(u'_request'): if not event.endswith(u'_request'):
return False return None
self.event = event self.event = event
response = event.replace(u'_request', u'_response') response = event.replace(u'_request', u'_response')
QtCore.QObject.connect(Receiver.get_receiver(), QtCore.QObject.connect(Receiver.get_receiver(),
@ -334,11 +414,11 @@ class HttpConnection(object):
self.timer.start(60000) self.timer.start(60000)
else: else:
self.timer.start(10000) self.timer.start(10000)
if params: if self.url_params:
Receiver.send_message(event, params) Receiver.send_message(event, self.url_params)
else: else:
Receiver.send_message(event) Receiver.send_message(event)
return True return None
def process_response(self, data): def process_response(self, data):
""" """
@ -349,11 +429,18 @@ class HttpConnection(object):
if not self.socket: if not self.socket:
return return
self.timer.stop() self.timer.stop()
html = json.dumps(data) json_data = json.dumps(data)
self.send_200_ok() self.send_200_ok()
self.socket.write(html) self.socket.write(json_data)
self.close() self.close()
def send_response(self, response):
http = u'HTTP/1.1 %s\r\n' % response.code
for header in response.headers.iteritems():
http += '%s: %s\r\n' % header
http += '\r\n'
self.socket.write(response.content)
def send_200_ok(self, mimetype='text/html; charset="utf-8"'): def send_200_ok(self, mimetype='text/html; charset="utf-8"'):
""" """
Successful request. Send OK headers. Assume html for now. Successful request. Send OK headers. Assume html for now.