Started refactor the webclient to use new system server-side. Renamed webclient.py to webclient_ajax.py and websocket_client.py to simpy webclient.py, since websocket is the standard now.
This commit is contained in:
parent
2d826df2f4
commit
7ad1229a7b
6 changed files with 473 additions and 462 deletions
|
|
@ -298,9 +298,9 @@ if WEBSERVER_ENABLED:
|
||||||
webclientstr = ""
|
webclientstr = ""
|
||||||
if WEBCLIENT_ENABLED:
|
if WEBCLIENT_ENABLED:
|
||||||
# create ajax client processes at /webclientdata
|
# create ajax client processes at /webclientdata
|
||||||
from evennia.server.portal.webclient import WebClient
|
from evennia.server.portal import webclient_ajax
|
||||||
|
|
||||||
webclient = WebClient()
|
webclient = webclient_ajax.WebClient()
|
||||||
webclient.sessionhandler = PORTAL_SESSIONS
|
webclient.sessionhandler = PORTAL_SESSIONS
|
||||||
web_root.putChild("webclientdata", webclient)
|
web_root.putChild("webclientdata", webclient)
|
||||||
webclientstr = "\n + client (ajax only)"
|
webclientstr = "\n + client (ajax only)"
|
||||||
|
|
@ -308,7 +308,7 @@ if WEBSERVER_ENABLED:
|
||||||
if WEBSOCKET_CLIENT_ENABLED and not websocket_started:
|
if WEBSOCKET_CLIENT_ENABLED and not websocket_started:
|
||||||
# start websocket client port for the webclient
|
# start websocket client port for the webclient
|
||||||
# we only support one websocket client
|
# we only support one websocket client
|
||||||
from evennia.server.portal import websocket_client
|
from evennia.server.portal import webclient
|
||||||
from evennia.utils.txws import WebSocketFactory
|
from evennia.utils.txws import WebSocketFactory
|
||||||
|
|
||||||
interface = WEBSOCKET_CLIENT_INTERFACE
|
interface = WEBSOCKET_CLIENT_INTERFACE
|
||||||
|
|
@ -318,7 +318,7 @@ if WEBSERVER_ENABLED:
|
||||||
ifacestr = "-%s" % interface
|
ifacestr = "-%s" % interface
|
||||||
pstring = "%s:%s" % (ifacestr, port)
|
pstring = "%s:%s" % (ifacestr, port)
|
||||||
factory = protocol.ServerFactory()
|
factory = protocol.ServerFactory()
|
||||||
factory.protocol = websocket_client.WebSocketClient
|
factory.protocol = webclient.WebSocketClient
|
||||||
factory.sessionhandler = PORTAL_SESSIONS
|
factory.sessionhandler = PORTAL_SESSIONS
|
||||||
websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=interface)
|
websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=interface)
|
||||||
websocket_service.setName('EvenniaWebSocket%s' % pstring)
|
websocket_service.setName('EvenniaWebSocket%s' % pstring)
|
||||||
|
|
|
||||||
|
|
@ -294,9 +294,13 @@ class PortalSessionHandler(SessionHandler):
|
||||||
Args:
|
Args:
|
||||||
message (str): Message to relay.
|
message (str): Message to relay.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
This will create an on-the fly text-type
|
||||||
|
send command.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
for session in self.values():
|
for session in self.values():
|
||||||
session.data_out(text=message)
|
session.data_out(text=(message,))
|
||||||
|
|
||||||
def data_in(self, session, **kwargs):
|
def data_in(self, session, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -279,17 +279,17 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
||||||
Args:
|
Args:
|
||||||
text (str): The first argument is always the text string to send. No other arguments
|
text (str): The first argument is always the text string to send. No other arguments
|
||||||
are considered.
|
are considered.
|
||||||
*options (str): All other arguments are considered option flags.
|
Kwargs:
|
||||||
Available flags are (if not set, TTYPE will be used, turning on if available):
|
options (dict): Send-option flags
|
||||||
mxp: Enforce MXP link support.
|
- mxp: Enforce MXP link support.
|
||||||
ansi: Enforce no ANSI colors.
|
- ansi: Enforce no ANSI colors.
|
||||||
xterm256: Enforce xterm256 colors, regardless of TTYPE.
|
- xterm256: Enforce xterm256 colors, regardless of TTYPE.
|
||||||
noxterm256: Enforce no xterm256 color support, regardless of TTYPE.
|
- noxterm256: Enforce no xterm256 color support, regardless of TTYPE.
|
||||||
nomarkup: Strip all ANSI markup. This is the same as noxterm256,noansi
|
- nomarkup: Strip all ANSI markup. This is the same as noxterm256,noansi
|
||||||
raw: Pass string through without any ansi processing
|
- raw: Pass string through without any ansi processing
|
||||||
(i.e. include Evennia ansi markers but do not
|
(i.e. include Evennia ansi markers but do not
|
||||||
convert them into ansi tokens)
|
convert them into ansi tokens)
|
||||||
echo: Turn on/off line echo on the client. Turn
|
- echo: Turn on/off line echo on the client. Turn
|
||||||
off line echo for client, for example for password.
|
off line echo for client, for example for password.
|
||||||
Note that it must be actively turned back on again!
|
Note that it must be actively turned back on again!
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,288 +1,196 @@
|
||||||
"""
|
"""
|
||||||
Web client server resource.
|
Webclient based on websockets.
|
||||||
|
|
||||||
The Evennia web client consists of two components running
|
This implements a webclient with WebSockets (http://en.wikipedia.org/wiki/WebSocket)
|
||||||
on twisted and django. They are both a part of the Evennia
|
by use of the txws implementation (https://github.com/MostAwesomeDude/txWS). It is
|
||||||
website url tree (so the testing website might be located
|
used together with evennia/web/media/javascript/evennia_websocket_webclient.js.
|
||||||
on http://localhost:8000/, whereas the webclient can be
|
|
||||||
found on http://localhost:8000/webclient.)
|
Thanks to Ricard Pillosu whose Evennia plugin inspired this module.
|
||||||
|
|
||||||
|
Communication over the websocket interface is done with normal text
|
||||||
|
communication. A special case is OOB-style communication; to do this
|
||||||
|
the client must send data on the following form:
|
||||||
|
|
||||||
|
OOB{"func1":[args], "func2":[args], ...}
|
||||||
|
|
||||||
|
where the dict is JSON encoded. The initial OOB-prefix is used to
|
||||||
|
identify this type of communication, all other data is considered
|
||||||
|
plain text (command input).
|
||||||
|
|
||||||
|
Example of call from a javascript client:
|
||||||
|
|
||||||
|
var websocket = new WebSocket("ws://localhost:8021");
|
||||||
|
var msg1 = "WebSocket Test";
|
||||||
|
websocket.send(msg1);
|
||||||
|
var msg2 = JSON.stringify({ testfunc: [[1, 2, 3], { kwarg: "val" }] });
|
||||||
|
websocket.send("OOB" + msg2);
|
||||||
|
websocket.close();
|
||||||
|
|
||||||
/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.
|
|
||||||
"""
|
"""
|
||||||
import time
|
import re
|
||||||
import json
|
import json
|
||||||
|
from twisted.internet.protocol import Protocol
|
||||||
from hashlib import md5
|
|
||||||
|
|
||||||
from twisted.web import server, resource
|
|
||||||
|
|
||||||
from django.utils.functional import Promise
|
|
||||||
from django.utils.encoding import force_unicode
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from evennia.utils import utils, logger
|
from evennia.server.session import Session
|
||||||
|
from evennia.utils.logger import log_trace
|
||||||
|
from evennia.utils.utils import to_str
|
||||||
|
from evennia.utils.ansi import parse_ansi
|
||||||
from evennia.utils.text2html import parse_html
|
from evennia.utils.text2html import parse_html
|
||||||
from evennia.server import session
|
|
||||||
|
|
||||||
SERVERNAME = settings.SERVERNAME
|
_RE_SCREENREADER_REGEX = re.compile(r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE)
|
||||||
ENCODINGS = settings.ENCODINGS
|
|
||||||
|
|
||||||
# defining a simple json encoder for returning
|
class WebSocketClient(Protocol, Session):
|
||||||
# django data to the client. Might need to
|
|
||||||
# extend this if one wants to send more
|
|
||||||
# complex database objects too.
|
|
||||||
|
|
||||||
class LazyEncoder(json.JSONEncoder):
|
|
||||||
def default(self, obj):
|
|
||||||
if isinstance(obj, Promise):
|
|
||||||
return force_unicode(obj)
|
|
||||||
return super(LazyEncoder, self).default(obj)
|
|
||||||
|
|
||||||
|
|
||||||
def jsonify(obj):
|
|
||||||
return utils.to_str(json.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
|
Implements the server-side of the Websocket connection.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def connectionMade(self):
|
||||||
|
"""
|
||||||
|
This is called when the connection is first established.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
isLeaf = True
|
client_address = self.transport.client
|
||||||
allowedMethods = ('POST',)
|
self.init_session("websocket", client_address, self.factory.sessionhandler)
|
||||||
|
# watch for dead links
|
||||||
|
self.transport.setTcpKeepAlive(1)
|
||||||
|
self.sessionhandler.connect(self)
|
||||||
|
|
||||||
def __init__(self):
|
self.datamap = {"text": self.send_text,
|
||||||
self.requests = {}
|
"prompt": self.send_prompt,
|
||||||
self.databuffer = {}
|
"_default": self.data_oob}
|
||||||
|
|
||||||
#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:
|
|
||||||
del self.requests[suid]
|
|
||||||
except KeyError:
|
|
||||||
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.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
suid (int): Session id.
|
|
||||||
string (str): The text to send.
|
|
||||||
data (dict): Optional data.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
The `data` keyword is deprecated.
|
|
||||||
|
|
||||||
"""
|
|
||||||
request = self.requests.get(suid)
|
|
||||||
if request:
|
|
||||||
# we have a request waiting. Return immediately.
|
|
||||||
request.write(jsonify({'msg': string, 'data': data}))
|
|
||||||
request.finish()
|
|
||||||
del self.requests[suid]
|
|
||||||
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 client_disconnect(self, suid):
|
|
||||||
"""
|
|
||||||
Disconnect session with given suid.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
suid (int): Session id.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if suid in self.requests:
|
|
||||||
self.requests[suid].finish()
|
|
||||||
del self.requests[suid]
|
|
||||||
if suid in self.databuffer:
|
|
||||||
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)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request (Request): Incoming request.
|
|
||||||
|
|
||||||
"""
|
|
||||||
#csess = request.getSession() # obs, this is a cookie, not
|
|
||||||
# an evennia session!
|
|
||||||
#csees.expireCallbacks.append(lambda : )
|
|
||||||
suid = request.args.get('suid', ['0'])[0]
|
|
||||||
|
|
||||||
remote_addr = request.getClientIP()
|
|
||||||
host_string = "%s (%s:%s)" % (SERVERNAME, request.getRequestHostname(), request.getHost().port)
|
|
||||||
if suid == '0':
|
|
||||||
# creating a unique id hash string
|
|
||||||
suid = md5(str(time.time())).hexdigest()
|
|
||||||
self.databuffer[suid] = []
|
|
||||||
|
|
||||||
sess = WebClientSession()
|
|
||||||
sess.client = self
|
|
||||||
sess.init_session("webclient", remote_addr, self.sessionhandler)
|
|
||||||
sess.suid = suid
|
|
||||||
sess.sessionhandler.connect(sess)
|
|
||||||
return jsonify({'msg': host_string, 'suid': suid})
|
|
||||||
|
|
||||||
def mode_input(self, request):
|
|
||||||
"""
|
|
||||||
This is called by render_POST when the client
|
|
||||||
is sending data to the server.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request (Request): Incoming request.
|
|
||||||
|
|
||||||
"""
|
|
||||||
suid = request.args.get('suid', ['0'])[0]
|
|
||||||
if suid == '0':
|
|
||||||
return ''
|
|
||||||
sess = self.sessionhandler.session_from_suid(suid)
|
|
||||||
if sess:
|
|
||||||
sess = sess[0]
|
|
||||||
text = request.args.get('msg', [''])[0]
|
|
||||||
data = request.args.get('data', [None])[0]
|
|
||||||
sess.sessionhandler.data_in(sess, text, data=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.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request (Request): Incoming request.
|
|
||||||
|
|
||||||
"""
|
|
||||||
suid = request.args.get('suid', ['0'])[0]
|
|
||||||
if suid == '0':
|
|
||||||
return ''
|
|
||||||
|
|
||||||
dataentries = self.databuffer.get(suid, [])
|
|
||||||
if dataentries:
|
|
||||||
return dataentries.pop(0)
|
|
||||||
request.notifyFinish().addErrback(self._responseFailed, suid, request)
|
|
||||||
if suid in self.requests:
|
|
||||||
self.requests[suid].finish() # Clear any stale request.
|
|
||||||
self.requests[suid] = request
|
|
||||||
return server.NOT_DONE_YET
|
|
||||||
|
|
||||||
def mode_close(self, request):
|
|
||||||
"""
|
|
||||||
This is called by render_POST when the client is signalling
|
|
||||||
that it is about to be closed.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request (Request): Incoming request.
|
|
||||||
|
|
||||||
"""
|
|
||||||
suid = request.args.get('suid', ['0'])[0]
|
|
||||||
if suid == '0':
|
|
||||||
self.client_disconnect(suid)
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
sess = self.sessionhandler.session_from_suid(suid)[0]
|
|
||||||
sess.sessionhandler.disconnect(sess)
|
|
||||||
except IndexError:
|
|
||||||
self.client_disconnect(suid)
|
|
||||||
pass
|
|
||||||
return ''
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request (Request): Incoming request.
|
|
||||||
|
|
||||||
"""
|
|
||||||
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)
|
|
||||||
elif dmode == 'close':
|
|
||||||
# the client is closing
|
|
||||||
return self.mode_close(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 disconnect(self, reason=None):
|
def disconnect(self, reason=None):
|
||||||
"""
|
"""
|
||||||
Disconnect from server.
|
Generic hook for the engine to call in order to
|
||||||
|
disconnect this protocol.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
reason (str): Motivation for the disconnect.
|
reason (str): Motivation for the disconnection.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if reason:
|
if reason:
|
||||||
self.client.lineSend(self.suid, reason)
|
self.data_out(text=reason)
|
||||||
self.client.client_disconnect(self.suid)
|
self.connectionLost(reason)
|
||||||
|
|
||||||
def data_out(self, text=None, **kwargs):
|
def connectionLost(self, reason):
|
||||||
"""
|
"""
|
||||||
Data Evennia -> User access hook.
|
This is executed when the connection is lost for whatever
|
||||||
|
reason. it can also be called directly, from the disconnect
|
||||||
|
method
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reason (str): Motivation for the lost connection.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.sessionhandler.disconnect(self)
|
||||||
|
self.transport.close()
|
||||||
|
|
||||||
|
def dataReceived(self, string):
|
||||||
|
"""
|
||||||
|
Method called when data is coming in over the websocket
|
||||||
|
connection. This is always a JSON object on the following
|
||||||
|
form:
|
||||||
|
[cmdname, arg, arg2, ...]
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
cmdarray = json.loads(string)
|
||||||
|
if cmdarray:
|
||||||
|
self.data_in(**{cmdarray[0]:cmdarray[1:]})
|
||||||
|
|
||||||
|
def sendLine(self, line):
|
||||||
|
"""
|
||||||
|
Send data to client.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
line (str): Text to send.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.transport.write(line)
|
||||||
|
|
||||||
|
def data_in(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Data User > Evennia.
|
||||||
|
|
||||||
|
Args::
|
||||||
|
text (str): Incoming text.
|
||||||
|
kwargs (any): Options from protocol.
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.sessionhandler.data_in(self, **kwargs)
|
||||||
|
|
||||||
|
def data_out(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Data Evennia->User.
|
||||||
|
|
||||||
Kwargs:
|
Kwargs:
|
||||||
raw (bool): No parsing at all (leave ansi-to-html markers unparsed).
|
kwargs (any): Options ot the protocol
|
||||||
nomarkup (bool): Clean out all ansi/html markers and tokens.
|
"""
|
||||||
|
self.sessionhandler.data_out(self, **kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def send_text(session, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Send text data. This will pre-process the text for
|
||||||
|
color-replacement, conversion to html etc.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): Text to send.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
options (dict): Options-dict with the following keys understood:
|
||||||
|
- raw (bool): No parsing at all (leave ansi-to-html markers unparsed).
|
||||||
|
- nomarkup (bool): Clean out all ansi/html markers and tokens.
|
||||||
|
- screenreader (bool): Use Screenreader mode.
|
||||||
|
- send_prompt (bool): Send a prompt with parsed html
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# string handling is similar to telnet
|
if args:
|
||||||
try:
|
text = args.pop(0)
|
||||||
text = utils.to_str(text if text else "", encoding=self.encoding)
|
if text is None:
|
||||||
raw = kwargs.get("raw", False)
|
|
||||||
nomarkup = kwargs.get("nomarkup", False)
|
|
||||||
if raw:
|
|
||||||
self.client.lineSend(self.suid, text)
|
|
||||||
else:
|
|
||||||
self.client.lineSend(self.suid,
|
|
||||||
parse_html(text, strip_ansi=nomarkup))
|
|
||||||
return
|
return
|
||||||
except Exception:
|
options = kwargs.get("options", {})
|
||||||
logger.log_trace()
|
raw = options.get("raw", False)
|
||||||
|
nomarkup = options.get("nomarkup", False)
|
||||||
|
screenreader = options.get("screenreader", False)
|
||||||
|
prompt = options.get("send_prompt", False)
|
||||||
|
|
||||||
|
if screenreader:
|
||||||
|
# screenreader mode cleans up output
|
||||||
|
text = parse_ansi(text, strip_ansi=True, xterm256=False, mxp=False)
|
||||||
|
text = _RE_SCREENREADER_REGEX.sub("", text)
|
||||||
|
|
||||||
|
cmd = "prompt" if prompt else "text"
|
||||||
|
|
||||||
|
if raw:
|
||||||
|
# no processing
|
||||||
|
data = json.dumps((text,) + args)
|
||||||
|
else:
|
||||||
|
# send normally, with html processing
|
||||||
|
data = json.dumps((cmd, parse_html(text, strip_ansi=nomarkup)) + args)
|
||||||
|
session.sendLine(data)
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def send_prompt(session, *args, **kwargs):
|
||||||
|
kwargs["options"].update({"send_prompt": True})
|
||||||
|
session.send_text(*args, **kwargs)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def send_oob(session, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Data Evennia -> User.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
cmd (str): The first argument will always be the oob cmd name.
|
||||||
|
*args (any): Remaining args will be arguments for `cmd`.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
options (dict): These are ignored for oob commands. Use command
|
||||||
|
arguments (which can hold dicts) to send instructions to the
|
||||||
|
client instead.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if args:
|
||||||
|
session.sendLine(json.dumps(args))
|
||||||
|
|
|
||||||
288
evennia/server/portal/webclient_ajax.py
Normal file
288
evennia/server/portal/webclient_ajax.py
Normal file
|
|
@ -0,0 +1,288 @@
|
||||||
|
"""
|
||||||
|
AJAX fallback webclient
|
||||||
|
|
||||||
|
The AJAX 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:8000/, whereas the webclient can be
|
||||||
|
found on http://localhost:8000/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.
|
||||||
|
"""
|
||||||
|
import time
|
||||||
|
import json
|
||||||
|
|
||||||
|
from hashlib import md5
|
||||||
|
|
||||||
|
from twisted.web import server, resource
|
||||||
|
|
||||||
|
from django.utils.functional import Promise
|
||||||
|
from django.utils.encoding import force_unicode
|
||||||
|
from django.conf import settings
|
||||||
|
from evennia.utils import utils, logger
|
||||||
|
from evennia.utils.text2html import parse_html
|
||||||
|
from evennia.server import session
|
||||||
|
|
||||||
|
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(json.JSONEncoder):
|
||||||
|
def default(self, obj):
|
||||||
|
if isinstance(obj, Promise):
|
||||||
|
return force_unicode(obj)
|
||||||
|
return super(LazyEncoder, self).default(obj)
|
||||||
|
|
||||||
|
|
||||||
|
def jsonify(obj):
|
||||||
|
return utils.to_str(json.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
|
||||||
|
|
||||||
|
"""
|
||||||
|
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:
|
||||||
|
del self.requests[suid]
|
||||||
|
except KeyError:
|
||||||
|
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.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
suid (int): Session id.
|
||||||
|
string (str): The text to send.
|
||||||
|
data (dict): Optional data.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
The `data` keyword is deprecated.
|
||||||
|
|
||||||
|
"""
|
||||||
|
request = self.requests.get(suid)
|
||||||
|
if request:
|
||||||
|
# we have a request waiting. Return immediately.
|
||||||
|
request.write(jsonify({'msg': string, 'data': data}))
|
||||||
|
request.finish()
|
||||||
|
del self.requests[suid]
|
||||||
|
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 client_disconnect(self, suid):
|
||||||
|
"""
|
||||||
|
Disconnect session with given suid.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
suid (int): Session id.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if suid in self.requests:
|
||||||
|
self.requests[suid].finish()
|
||||||
|
del self.requests[suid]
|
||||||
|
if suid in self.databuffer:
|
||||||
|
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)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Request): Incoming request.
|
||||||
|
|
||||||
|
"""
|
||||||
|
#csess = request.getSession() # obs, this is a cookie, not
|
||||||
|
# an evennia session!
|
||||||
|
#csees.expireCallbacks.append(lambda : )
|
||||||
|
suid = request.args.get('suid', ['0'])[0]
|
||||||
|
|
||||||
|
remote_addr = request.getClientIP()
|
||||||
|
host_string = "%s (%s:%s)" % (SERVERNAME, request.getRequestHostname(), request.getHost().port)
|
||||||
|
if suid == '0':
|
||||||
|
# creating a unique id hash string
|
||||||
|
suid = md5(str(time.time())).hexdigest()
|
||||||
|
self.databuffer[suid] = []
|
||||||
|
|
||||||
|
sess = WebClientSession()
|
||||||
|
sess.client = self
|
||||||
|
sess.init_session("webclient", remote_addr, self.sessionhandler)
|
||||||
|
sess.suid = suid
|
||||||
|
sess.sessionhandler.connect(sess)
|
||||||
|
return jsonify({'msg': host_string, 'suid': suid})
|
||||||
|
|
||||||
|
def mode_input(self, request):
|
||||||
|
"""
|
||||||
|
This is called by render_POST when the client
|
||||||
|
is sending data to the server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Request): Incoming request.
|
||||||
|
|
||||||
|
"""
|
||||||
|
suid = request.args.get('suid', ['0'])[0]
|
||||||
|
if suid == '0':
|
||||||
|
return ''
|
||||||
|
sess = self.sessionhandler.session_from_suid(suid)
|
||||||
|
if sess:
|
||||||
|
sess = sess[0]
|
||||||
|
text = request.args.get('msg', [''])[0]
|
||||||
|
data = request.args.get('data', [None])[0]
|
||||||
|
sess.sessionhandler.data_in(sess, text, data=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.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Request): Incoming request.
|
||||||
|
|
||||||
|
"""
|
||||||
|
suid = request.args.get('suid', ['0'])[0]
|
||||||
|
if suid == '0':
|
||||||
|
return ''
|
||||||
|
|
||||||
|
dataentries = self.databuffer.get(suid, [])
|
||||||
|
if dataentries:
|
||||||
|
return dataentries.pop(0)
|
||||||
|
request.notifyFinish().addErrback(self._responseFailed, suid, request)
|
||||||
|
if suid in self.requests:
|
||||||
|
self.requests[suid].finish() # Clear any stale request.
|
||||||
|
self.requests[suid] = request
|
||||||
|
return server.NOT_DONE_YET
|
||||||
|
|
||||||
|
def mode_close(self, request):
|
||||||
|
"""
|
||||||
|
This is called by render_POST when the client is signalling
|
||||||
|
that it is about to be closed.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Request): Incoming request.
|
||||||
|
|
||||||
|
"""
|
||||||
|
suid = request.args.get('suid', ['0'])[0]
|
||||||
|
if suid == '0':
|
||||||
|
self.client_disconnect(suid)
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
sess = self.sessionhandler.session_from_suid(suid)[0]
|
||||||
|
sess.sessionhandler.disconnect(sess)
|
||||||
|
except IndexError:
|
||||||
|
self.client_disconnect(suid)
|
||||||
|
pass
|
||||||
|
return ''
|
||||||
|
|
||||||
|
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.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (Request): Incoming request.
|
||||||
|
|
||||||
|
"""
|
||||||
|
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)
|
||||||
|
elif dmode == 'close':
|
||||||
|
# the client is closing
|
||||||
|
return self.mode_close(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 disconnect(self, reason=None):
|
||||||
|
"""
|
||||||
|
Disconnect from server.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
reason (str): Motivation for the disconnect.
|
||||||
|
"""
|
||||||
|
if reason:
|
||||||
|
self.client.lineSend(self.suid, reason)
|
||||||
|
self.client.client_disconnect(self.suid)
|
||||||
|
|
||||||
|
def data_out(self, text=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Data Evennia -> User access hook.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
raw (bool): No parsing at all (leave ansi-to-html markers unparsed).
|
||||||
|
nomarkup (bool): Clean out all ansi/html markers and tokens.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# string handling is similar to telnet
|
||||||
|
try:
|
||||||
|
text = utils.to_str(text if text else "", encoding=self.encoding)
|
||||||
|
raw = kwargs.get("raw", False)
|
||||||
|
nomarkup = kwargs.get("nomarkup", False)
|
||||||
|
if raw:
|
||||||
|
self.client.lineSend(self.suid, text)
|
||||||
|
else:
|
||||||
|
self.client.lineSend(self.suid,
|
||||||
|
parse_html(text, strip_ansi=nomarkup))
|
||||||
|
return
|
||||||
|
except Exception:
|
||||||
|
logger.log_trace()
|
||||||
|
|
@ -1,189 +0,0 @@
|
||||||
"""
|
|
||||||
Websocket-webclient
|
|
||||||
|
|
||||||
This implements a webclient with WebSockets (http://en.wikipedia.org/wiki/WebSocket)
|
|
||||||
by use of the txws implementation (https://github.com/MostAwesomeDude/txWS). It is
|
|
||||||
used together with evennia/web/media/javascript/evennia_websocket_webclient.js.
|
|
||||||
|
|
||||||
Thanks to Ricard Pillosu whose Evennia plugin inspired this module.
|
|
||||||
|
|
||||||
Communication over the websocket interface is done with normal text
|
|
||||||
communication. A special case is OOB-style communication; to do this
|
|
||||||
the client must send data on the following form:
|
|
||||||
|
|
||||||
OOB{"func1":[args], "func2":[args], ...}
|
|
||||||
|
|
||||||
where the dict is JSON encoded. The initial OOB-prefix is used to
|
|
||||||
identify this type of communication, all other data is considered
|
|
||||||
plain text (command input).
|
|
||||||
|
|
||||||
Example of call from a javascript client:
|
|
||||||
|
|
||||||
var websocket = new WebSocket("ws://localhost:8021");
|
|
||||||
var msg1 = "WebSocket Test";
|
|
||||||
websocket.send(msg1);
|
|
||||||
var msg2 = JSON.stringify({ testfunc: [[1, 2, 3], { kwarg: "val" }] });
|
|
||||||
websocket.send("OOB" + msg2);
|
|
||||||
websocket.close();
|
|
||||||
|
|
||||||
"""
|
|
||||||
import json
|
|
||||||
from twisted.internet.protocol import Protocol
|
|
||||||
from evennia.server.session import Session
|
|
||||||
from evennia.utils.logger import log_trace
|
|
||||||
from evennia.utils.utils import to_str
|
|
||||||
from evennia.utils.text2html import parse_html
|
|
||||||
|
|
||||||
|
|
||||||
class WebSocketClient(Protocol, Session):
|
|
||||||
"""
|
|
||||||
Implements the server-side of the Websocket connection.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def connectionMade(self):
|
|
||||||
"""
|
|
||||||
This is called when the connection is first established.
|
|
||||||
|
|
||||||
"""
|
|
||||||
client_address = self.transport.client
|
|
||||||
self.init_session("websocket", client_address, self.factory.sessionhandler)
|
|
||||||
# watch for dead links
|
|
||||||
self.transport.setTcpKeepAlive(1)
|
|
||||||
self.sessionhandler.connect(self)
|
|
||||||
|
|
||||||
def disconnect(self, reason=None):
|
|
||||||
"""
|
|
||||||
Generic hook for the engine to call in order to
|
|
||||||
disconnect this protocol.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
reason (str): Motivation for the disconnection.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if reason:
|
|
||||||
self.data_out(text=reason)
|
|
||||||
self.connectionLost(reason)
|
|
||||||
|
|
||||||
def connectionLost(self, reason):
|
|
||||||
"""
|
|
||||||
This is executed when the connection is lost for whatever
|
|
||||||
reason. it can also be called directly, from the disconnect
|
|
||||||
method
|
|
||||||
|
|
||||||
Args:
|
|
||||||
reason (str): Motivation for the lost connection.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.sessionhandler.disconnect(self)
|
|
||||||
self.transport.close()
|
|
||||||
|
|
||||||
def dataReceived(self, string):
|
|
||||||
"""
|
|
||||||
Method called when data is coming in over the websocket
|
|
||||||
connection.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
string (str): Type of data is identified by a 3-character
|
|
||||||
prefix:
|
|
||||||
- "OOB" This is an Out-of-band instruction. If so,
|
|
||||||
the remaining string should be a json-packed
|
|
||||||
string on the form {oobfuncname: [args, ], ...}
|
|
||||||
- "CMD" plain text data, to be treated like a game
|
|
||||||
input command.
|
|
||||||
"""
|
|
||||||
mode = string[:3]
|
|
||||||
data = string[3:]
|
|
||||||
|
|
||||||
if mode == "OOB":
|
|
||||||
# an out-of-band command
|
|
||||||
self.json_decode(data)
|
|
||||||
elif mode == "CMD":
|
|
||||||
# plain text input
|
|
||||||
self.data_in(text=data)
|
|
||||||
|
|
||||||
def sendLine(self, line):
|
|
||||||
"""
|
|
||||||
Send data to client.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
line (str): Text to send.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self.transport.write(line)
|
|
||||||
|
|
||||||
def json_decode(self, data):
|
|
||||||
"""
|
|
||||||
Decodes incoming data from the client.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
data (JSON): JSON object to unpack.
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
Exception: If receiving a malform OOB request.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
[cmdname, [args],{kwargs}] -> cmdname *args **kwargs
|
|
||||||
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
cmdname, args, kwargs = json.loads(data)
|
|
||||||
except Exception:
|
|
||||||
log_trace("Websocket malformed OOB request: %s" % data)
|
|
||||||
raise
|
|
||||||
self.sessionhandler.data_in(self, oob=(cmdname, args, kwargs))
|
|
||||||
|
|
||||||
def json_encode(self, cmdname, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Encode OOB data for sending to client.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
cmdname (str): OOB command name.
|
|
||||||
args, kwargs (any): Arguments to oob command.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
cmdname *args -> cmdname [json array]
|
|
||||||
cmdname **kwargs -> cmdname {json object}
|
|
||||||
|
|
||||||
"""
|
|
||||||
cmdtuple = [cmdname, list(args), kwargs]
|
|
||||||
self.sendLine("OOB" + json.dumps(cmdtuple))
|
|
||||||
|
|
||||||
def data_in(self, text=None, **kwargs):
|
|
||||||
"""
|
|
||||||
Data User > Evennia.
|
|
||||||
|
|
||||||
Kwargs:
|
|
||||||
text (str): Incoming text.
|
|
||||||
kwargs (any): Options from protocol.
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.sessionhandler.data_in(self, text=text, **kwargs)
|
|
||||||
|
|
||||||
def data_out(self, text=None, **kwargs):
|
|
||||||
"""
|
|
||||||
Data Evennia -> User. A generic hook method for engine to call
|
|
||||||
in order to send data through the websocket connection.
|
|
||||||
|
|
||||||
Kwargs:
|
|
||||||
oob (str or tuple): Supply an Out-of-Band instruction.
|
|
||||||
raw (bool): No parsing at all (leave ansi-to-html markers unparsed).
|
|
||||||
nomarkup (bool): Clean out all ansi/html markers and tokens.
|
|
||||||
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
text = to_str(text if text else "", encoding=self.encoding)
|
|
||||||
except Exception as e:
|
|
||||||
self.sendLine(str(e))
|
|
||||||
if "oob" in kwargs:
|
|
||||||
for cmdname, args, okwargs in kwargs["oob"]:
|
|
||||||
self.json_encode(cmdname, *args, **okwargs)
|
|
||||||
|
|
||||||
raw = kwargs.get("raw", False)
|
|
||||||
nomarkup = kwargs.get("nomarkup", False)
|
|
||||||
if "prompt" in kwargs:
|
|
||||||
self.sendLine("PRT" + parse_html(kwargs["prompt"], strip_ansi=nomarkup))
|
|
||||||
if raw:
|
|
||||||
self.sendLine("CMD" + text)
|
|
||||||
else:
|
|
||||||
self.sendLine("CMD" + parse_html(text, strip_ansi=nomarkup))
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue