From 7ad1229a7b968e3e810ba27078f60e01c4c91889 Mon Sep 17 00:00:00 2001 From: Griatch Date: Wed, 3 Feb 2016 17:35:12 +0100 Subject: [PATCH] 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. --- evennia/server/portal/portal.py | 8 +- evennia/server/portal/portalsessionhandler.py | 6 +- evennia/server/portal/telnet.py | 18 +- evennia/server/portal/webclient.py | 426 +++++++----------- evennia/server/portal/webclient_ajax.py | 288 ++++++++++++ evennia/server/portal/websocket_client.py | 189 -------- 6 files changed, 473 insertions(+), 462 deletions(-) create mode 100644 evennia/server/portal/webclient_ajax.py delete mode 100644 evennia/server/portal/websocket_client.py diff --git a/evennia/server/portal/portal.py b/evennia/server/portal/portal.py index 079639c86..5dc9d6653 100644 --- a/evennia/server/portal/portal.py +++ b/evennia/server/portal/portal.py @@ -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) diff --git a/evennia/server/portal/portalsessionhandler.py b/evennia/server/portal/portalsessionhandler.py index 58c17c04e..0563e503c 100644 --- a/evennia/server/portal/portalsessionhandler.py +++ b/evennia/server/portal/portalsessionhandler.py @@ -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): """ diff --git a/evennia/server/portal/telnet.py b/evennia/server/portal/telnet.py index 8c5d00035..a1258c0e3 100644 --- a/evennia/server/portal/telnet.py +++ b/evennia/server/portal/telnet.py @@ -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! diff --git a/evennia/server/portal/webclient.py b/evennia/server/portal/webclient.py index e81c6578a..eff226b07 100644 --- a/evennia/server/portal/webclient.py +++ b/evennia/server/portal/webclient.py @@ -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)) diff --git a/evennia/server/portal/webclient_ajax.py b/evennia/server/portal/webclient_ajax.py new file mode 100644 index 000000000..afe432e8e --- /dev/null +++ b/evennia/server/portal/webclient_ajax.py @@ -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() diff --git a/evennia/server/portal/websocket_client.py b/evennia/server/portal/websocket_client.py deleted file mode 100644 index d6da0f963..000000000 --- a/evennia/server/portal/websocket_client.py +++ /dev/null @@ -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)) -