evennia/src/server/webclient.py

287 lines
9.9 KiB
Python
Raw Normal View History

"""
Web client server resource.
The Evennia web client consists of two components running
on twisted and django. They are both a part of the Evennia
website url tree (so the testing website might be located
on http://localhost:8020/, whereas the webclient can be
found on http://localhost:8020/webclient.)
/webclient - this url is handled through django's template
system and serves the html page for the client
itself along with its javascript chat program.
/webclientdata - this url is called by the ajax chat using
POST requests (long-polling when necessary)
The WebClient resource in this module will
handle these requests and act as a gateway
to sessions connected over the webclient.
"""
from twisted.web import server, resource
from twisted.internet import defer
from django.utils import simplejson
from django.utils.functional import Promise
from django.utils.encoding import force_unicode
from django.conf import settings
from src.utils import utils
from src.utils.ansi2html import parse_html
from src.config.models import ConnectScreen
from src.server import session, sessionhandler
SERVERNAME = settings.SERVERNAME
ENCODINGS = settings.ENCODINGS
# defining a simple json encoder for returning
# django data to the client. Might need to
# extend this if one wants to send more
# complex database objects too.
class LazyEncoder(simplejson.JSONEncoder):
def default(self, obj):
if isinstance(obj, Promise):
return force_unicode(obj)
return super(LazyEncoder, self).default(obj)
def jsonify(obj):
return simplejson.dumps(obj, ensure_ascii=False, cls=LazyEncoder)
#
# WebClient resource - this is called by the ajax client
# using POST requests to /webclientdata.
#
class WebClient(resource.Resource):
"""
An ajax/comet long-polling transport protocol for
"""
isLeaf = True
allowedMethods = ('POST',)
def __init__(self):
self.requests = {}
self.databuffer = {}
def getChild(self, path, request):
"""
This is the place to put dynamic content.
"""
return self
def _responseFailed(self, failure, suid, request):
"callback if a request is lost/timed out"
try:
self.requests.get(suid, []).remove(request)
except ValueError:
pass
def lineSend(self, suid, string, data=None):
"""
This adds the data to the buffer and/or sends it to
the client as soon as possible.
"""
requests = self.requests.get(suid, None)
if requests:
request = requests.pop(0)
# we have a request waiting. Return immediately.
request.write(jsonify({'msg':string, 'data':data}))
request.finish()
self.requests[suid] = requests
else:
# no waiting request. Store data in buffer
dataentries = self.databuffer.get(suid, [])
dataentries.append(jsonify({'msg':string, 'data':data}))
self.databuffer[suid] = dataentries
def disconnect(self, suid):
"Disconnect session"
sess = sessionhandler.SESSIONS.session_from_suid(suid)
if sess:
sess[0].session_disconnect()
if self.requests.has_key(suid):
for request in self.requests.get(suid, []):
request.finish()
del self.requests[suid]
if self.databuffer.has_key(suid):
del self.databuffer[suid]
def mode_init(self, request):
"""
This is called by render_POST when the client
requests an init mode operation (at startup)
"""
csess = request.getSession() # obs, this is a cookie, not an evennia session!
#csees.expireCallbacks.append(lambda : )
suid = csess.uid
remote_addr = request.getClientIP()
host_string = "%s (%s:%s)" % (SERVERNAME, request.getHost().host, request.getHost().port)
self.requests[suid] = []
self.databuffer[suid] = []
sess = sessionhandler.SESSIONS.session_from_suid(suid)
if not sess:
sess = WebClientSession()
sess.client = self
sess.session_connect(remote_addr, suid)
sessionhandler.SESSIONS.add_unloggedin_session(sess)
return jsonify({'msg':host_string})
def mode_input(self, request):
"""
This is called by render_POST when the client
is sending data to the server.
"""
string = request.args.get('msg', [''])[0]
data = request.args.get('data', [None])[0]
suid = request.getSession().uid
sess = sessionhandler.SESSIONS.session_from_suid(suid)
if sess:
sess[0].at_data_in(string, data)
return ''
def mode_receive(self, request):
"""
This is called by render_POST when the client is telling us
that it is ready to receive data as soon as it is
available. This is the basis of a long-polling (comet)
mechanism: the server will wait to reply until data is
available.
"""
suid = request.getSession().uid
dataentries = self.databuffer.get(suid, [])
if dataentries:
return dataentries.pop(0)
reqlist = self.requests.get(suid, [])
request.notifyFinish().addErrback(self._responseFailed, suid, request)
reqlist.append(request)
self.requests[suid] = reqlist
return server.NOT_DONE_YET
def render_POST(self, request):
"""
This function is what Twisted calls with POST requests coming
in from the ajax client. The requests should be tagged with
different modes depending on what needs to be done, such as
initializing or sending/receving data through the request. It
uses a long-polling mechanism to avoid sending data unless
there is actual data available.
"""
dmode = request.args.get('mode', [None])[0]
if dmode == 'init':
# startup. Setup the server.
return self.mode_init(request)
elif dmode == 'input':
# input from the client to the server
return self.mode_input(request)
elif dmode == 'receive':
# the client is waiting to receive data.
return self.mode_receive(request)
else:
# this should not happen if client sends valid data.
return ''
#
# A session type handling communication over the
# web client interface.
#
class WebClientSession(session.Session):
"""
This represents a session running in a webclient.
"""
def at_connect(self):
"""
Show the banner screen. Grab from the 'connect_screen'
config directive. If more than one connect screen is
defined in the ConnectScreen attribute, it will be
random which screen is used.
"""
# show screen
screen = ConnectScreen.objects.get_random_connect_screen()
string = parse_html(screen.text)
self.client.lineSend(self.suid, string)
def at_login(self):
"""
Called after authentication. self.logged_in=True at this point.
"""
if self.player.has_attribute('telnet_markup'):
self.telnet_markup = self.player.get_attribute("telnet_markup")
else:
self.telnet_markup = True
def at_disconnect(self, reason=None):
"""
Disconnect from server
"""
if reason:
self.lineSend(self.suid, reason)
self.client.disconnect(self.suid)
def at_data_out(self, string='', data=None):
"""
Data Evennia -> Player access hook.
data argument may be used depending on
the client-server implementation.
"""
if data:
# treat data?
pass
# string handling is similar to telnet
if self.encoding:
try:
string = utils.to_str(string, encoding=self.encoding)
#self.client.lineSend(self.suid, ansi.parse_ansi(string, strip_ansi=True))
self.client.lineSend(self.suid, parse_html(string))
return
except Exception:
pass
# malformed/wrong encoding defined on player - try some defaults
for encoding in ENCODINGS:
try:
string = utils.to_str(string, encoding=encoding)
err = None
break
except Exception, e:
err = str(e)
continue
if err:
self.client.lineSend(self.suid, err)
else:
#self.client.lineSend(self.suid, ansi.parse_ansi(string, strip_ansi=True))
self.client.lineSend(self.suid, parse_html(string))
def at_data_in(self, string, data=None):
"""
Input from Player -> Evennia (called by client).
Use of 'data' is up to the client - server implementation.
"""
# treat data?
if data:
pass
# the string part is identical to telnet
if self.encoding:
try:
string = utils.to_unicode(string, encoding=self.encoding)
self.execute_cmd(string)
return
except Exception, e:
err = str(e)
print err
# malformed/wrong encoding defined on player - try some defaults
for encoding in ENCODINGS:
try:
string = utils.to_unicode(string, encoding=encoding)
err = None
break
except Exception, e:
err = str(e)
continue
self.execute_cmd(string)