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 = ""
|
||||
if WEBCLIENT_ENABLED:
|
||||
# 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
|
||||
web_root.putChild("webclientdata", webclient)
|
||||
webclientstr = "\n + client (ajax only)"
|
||||
|
|
@ -308,7 +308,7 @@ if WEBSERVER_ENABLED:
|
|||
if WEBSOCKET_CLIENT_ENABLED and not websocket_started:
|
||||
# start websocket client port for the webclient
|
||||
# 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
|
||||
|
||||
interface = WEBSOCKET_CLIENT_INTERFACE
|
||||
|
|
@ -318,7 +318,7 @@ if WEBSERVER_ENABLED:
|
|||
ifacestr = "-%s" % interface
|
||||
pstring = "%s:%s" % (ifacestr, port)
|
||||
factory = protocol.ServerFactory()
|
||||
factory.protocol = websocket_client.WebSocketClient
|
||||
factory.protocol = webclient.WebSocketClient
|
||||
factory.sessionhandler = PORTAL_SESSIONS
|
||||
websocket_service = internet.TCPServer(port, WebSocketFactory(factory), interface=interface)
|
||||
websocket_service.setName('EvenniaWebSocket%s' % pstring)
|
||||
|
|
|
|||
|
|
@ -294,9 +294,13 @@ class PortalSessionHandler(SessionHandler):
|
|||
Args:
|
||||
message (str): Message to relay.
|
||||
|
||||
Notes:
|
||||
This will create an on-the fly text-type
|
||||
send command.
|
||||
|
||||
"""
|
||||
for session in self.values():
|
||||
session.data_out(text=message)
|
||||
session.data_out(text=(message,))
|
||||
|
||||
def data_in(self, session, **kwargs):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -279,17 +279,17 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
|||
Args:
|
||||
text (str): The first argument is always the text string to send. No other arguments
|
||||
are considered.
|
||||
*options (str): All other arguments are considered option flags.
|
||||
Available flags are (if not set, TTYPE will be used, turning on if available):
|
||||
mxp: Enforce MXP link support.
|
||||
ansi: Enforce no ANSI colors.
|
||||
xterm256: Enforce xterm256 colors, regardless of TTYPE.
|
||||
noxterm256: Enforce no xterm256 color support, regardless of TTYPE.
|
||||
nomarkup: Strip all ANSI markup. This is the same as noxterm256,noansi
|
||||
raw: Pass string through without any ansi processing
|
||||
Kwargs:
|
||||
options (dict): Send-option flags
|
||||
- mxp: Enforce MXP link support.
|
||||
- ansi: Enforce no ANSI colors.
|
||||
- xterm256: Enforce xterm256 colors, regardless of TTYPE.
|
||||
- noxterm256: Enforce no xterm256 color support, regardless of TTYPE.
|
||||
- nomarkup: Strip all ANSI markup. This is the same as noxterm256,noansi
|
||||
- raw: Pass string through without any ansi processing
|
||||
(i.e. include Evennia ansi markers but do not
|
||||
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.
|
||||
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
|
||||
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.)
|
||||
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();
|
||||
|
||||
/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
|
||||
|
||||
from hashlib import md5
|
||||
|
||||
from twisted.web import server, resource
|
||||
|
||||
from django.utils.functional import Promise
|
||||
from django.utils.encoding import force_unicode
|
||||
from twisted.internet.protocol import Protocol
|
||||
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.server import session
|
||||
|
||||
SERVERNAME = settings.SERVERNAME
|
||||
ENCODINGS = settings.ENCODINGS
|
||||
_RE_SCREENREADER_REGEX = re.compile(r"%s" % settings.SCREENREADER_REGEX_STRIP, re.DOTALL + re.MULTILINE)
|
||||
|
||||
# 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):
|
||||
class WebSocketClient(Protocol, Session):
|
||||
"""
|
||||
An ajax/comet long-polling transport
|
||||
|
||||
Implements the server-side of the Websocket connection.
|
||||
"""
|
||||
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):
|
||||
def connectionMade(self):
|
||||
"""
|
||||
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.
|
||||
This is called when the connection is first established.
|
||||
|
||||
"""
|
||||
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
|
||||
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 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.
|
||||
"""
|
||||
self.datamap = {"text": self.send_text,
|
||||
"prompt": self.send_prompt,
|
||||
"_default": self.data_oob}
|
||||
|
||||
def disconnect(self, reason=None):
|
||||
"""
|
||||
Disconnect from server.
|
||||
Generic hook for the engine to call in order to
|
||||
disconnect this protocol.
|
||||
|
||||
Args:
|
||||
reason (str): Motivation for the disconnect.
|
||||
reason (str): Motivation for the disconnection.
|
||||
|
||||
"""
|
||||
if reason:
|
||||
self.client.lineSend(self.suid, reason)
|
||||
self.client.client_disconnect(self.suid)
|
||||
self.data_out(text=reason)
|
||||
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:
|
||||
raw (bool): No parsing at all (leave ansi-to-html markers unparsed).
|
||||
nomarkup (bool): Clean out all ansi/html markers and tokens.
|
||||
kwargs (any): Options ot the protocol
|
||||
"""
|
||||
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
|
||||
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()
|
||||
if args:
|
||||
text = args.pop(0)
|
||||
if text is None:
|
||||
return
|
||||
options = kwargs.get("options", {})
|
||||
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