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:
Griatch 2016-02-03 17:35:12 +01:00
parent 2d826df2f4
commit 7ad1229a7b
6 changed files with 473 additions and 462 deletions

View file

@ -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)

View file

@ -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):
"""

View file

@ -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!

View file

@ -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))

View 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()

View file

@ -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))