Made GMCP/MSDP work for tintin++. Mudlet seems to send the handshake differently.

This commit is contained in:
Griatch 2016-02-13 21:40:12 +01:00
parent 49c1254de7
commit 006e367330
6 changed files with 206 additions and 138 deletions

View file

@ -53,7 +53,9 @@ def text(session, *args, **kwargs):
session.update_session_counters() session.update_session_counters()
def echo(session, *args, **kwargs): def echo(session, *args, **kwargs):
session.data_out(text=(args, kwargs)) print "Inputfunc echo:", session, args, kwargs
session.data_out(text="Echo returns: ")
session.data_out(echo=(args, kwargs))
def default(session, cmdname, *args, **kwargs): def default(session, cmdname, *args, **kwargs):
""" """
@ -61,10 +63,10 @@ def default(session, cmdname, *args, **kwargs):
it will get `cmdname` as the first argument. it will get `cmdname` as the first argument.
""" """
err = "Input command not recognized:\n" \ err = "Session {sessid}: Input command not recognized:\n" \
" name: {cmdname}\n" \ " name: '{cmdname}'\n" \
" args, kwargs: {args}, {kwargs}" " args, kwargs: {args}, {kwargs}"
log_err(err.format(cmdname=cmdname, args=args, kwargs=kwargs)) log_err(err.format(sessid=session.sessid, cmdname=cmdname, args=args, kwargs=kwargs))
#------------------------------------------------------------------------------------ #------------------------------------------------------------------------------------

View file

@ -300,7 +300,7 @@ class PortalSessionHandler(SessionHandler):
""" """
for session in self.values(): for session in self.values():
self.data_out(session, text=message) self.data_out(session, text=[[message],{}])
def data_in(self, session, **kwargs): def data_in(self, session, **kwargs):
""" """
@ -334,9 +334,7 @@ class PortalSessionHandler(SessionHandler):
self.data_out(session, text=_ERROR_COMMAND_OVERFLOW) self.data_out(session, text=_ERROR_COMMAND_OVERFLOW)
return return
# scrub data # scrub data
print ("portalsessionhandler before clean:", session, kwargs)
kwargs = self.clean_senddata(session, kwargs) kwargs = self.clean_senddata(session, kwargs)
print ("portalsessionhandler after clean:", session, kwargs)
# relay data to Server # relay data to Server
self.command_counter += 1 self.command_counter += 1
@ -370,7 +368,7 @@ class PortalSessionHandler(SessionHandler):
# distribute outgoing data to the correct session methods. # distribute outgoing data to the correct session methods.
if session: if session:
for cmdname, (cmdargs, cmdkwargs) in kwargs.iteritems(): for cmdname, (cmdargs, cmdkwargs) in kwargs.iteritems():
funcname = "send_%s" % cmdname funcname = "send_%s" % cmdname.strip().lower()
if hasattr(session, funcname): if hasattr(session, funcname):
# better to use hassattr here over try..except # better to use hassattr here over try..except
# - avoids hiding AttributeErrors in the call. # - avoids hiding AttributeErrors in the call.

View file

@ -15,7 +15,8 @@ from evennia.server.session import Session
from evennia.server.portal import ttype, mssp, telnet_oob, naws from evennia.server.portal import ttype, mssp, telnet_oob, naws
from evennia.server.portal.mccp import Mccp, mccp_compress, MCCP from evennia.server.portal.mccp import Mccp, mccp_compress, MCCP
from evennia.server.portal.mxp import Mxp, mxp_parse from evennia.server.portal.mxp import Mxp, mxp_parse
from evennia.utils import utils, ansi, logger from evennia.utils import ansi, logger
from evennia.utils.utils import to_str
IAC = chr(255) IAC = chr(255)
NOP = chr(241) NOP = chr(241)
@ -289,11 +290,11 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
Note that it must be actively turned back on again! Note that it must be actively turned back on again!
""" """
print "telnet.send_text", args,kwargs #print "telnet.send_text", args,kwargs
if args: text = args[0] if args else ""
text = args[0] if text is None:
if text is None: return
return text = to_str(text, force_string=True)
# handle arguments # handle arguments
options = kwargs.get("options", {}) options = kwargs.get("options", {})
@ -354,4 +355,5 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
Send other oob data Send other oob data
""" """
if not cmdname == "options": if not cmdname == "options":
print "telnet.send_default not implemented yet! ", args print "telnet.send_default:", cmdname, args, kwargs
self.oob.data_out(cmdname, *args, **kwargs)

View file

@ -2,21 +2,28 @@
Telnet OOB (Out of band communication) Telnet OOB (Out of band communication)
This implements the following telnet oob protocols: MSDP (Mud Server OOB protocols allow for asynchronous communication between Evennia and
Data Protocol) GMCP (Generic Mud Communication Protocol) compliant telnet clients. The "text" type of send command will always
be sent "in-band", appearing in the client's main text output. OOB
commands, by contrast, can have many forms and it is up to the client
how and if they are handled. Examples of OOB instructions could be to
instruct the client to play sounds or to update a graphical health
bar.
This implements the MSDP protocol as per > Note that in Evennia's Web client, all send commands are "OOB
http://tintin.sourceforge.net/msdp/ and the GMCP protocol as per commands", (including the "text" one), there is no equivalence to
http://www.ironrealms.com/rapture/manual/files/FeatGMCP-txt.html#Generic_MUD_Communication_Protocol%28GMCP%29 MSDP/GMCP for the webclient since it doesn't need it.
This implements the following telnet OOB communication protocols:
- MSDP (Mud Server Data Protocol), as per
http://tintin.sourceforge.net/msdp/
- GMCP (Generic Mud Communication Protocol) as per
http://www.ironrealms.com/rapture/manual/files/FeatGMCP-txt.html#Generic_MUD_Communication_Protocol%28GMCP%29
Following the lead of KaVir's protocol snippet, we first check if Following the lead of KaVir's protocol snippet, we first check if
client supports MSDP and if not, we fallback to GMCP with a MSDP client supports MSDP and if not, we fallback to GMCP with a MSDP
header where applicable. header where applicable.
OOB manages out-of-band communication between the client and server,
for updating health bars etc. See also GMCP which is another standard
doing the same thing.
""" """
from builtins import object from builtins import object
import re import re
@ -25,12 +32,12 @@ from evennia.utils.utils import to_str
# MSDP-relevant telnet cmd/opt-codes # MSDP-relevant telnet cmd/opt-codes
MSDP = chr(69) MSDP = chr(69)
MSDP_VAR = chr(1) MSDP_VAR = chr(1) #^A
MSDP_VAL = chr(2) MSDP_VAL = chr(2) #^B
MSDP_TABLE_OPEN = chr(3) MSDP_TABLE_OPEN = chr(3) #^C
MSDP_TABLE_CLOSE = chr(4) MSDP_TABLE_CLOSE = chr(4) #^D
MSDP_ARRAY_OPEN = chr(5) MSDP_ARRAY_OPEN = chr(5) #^E
MSDP_ARRAY_CLOSE = chr(6) MSDP_ARRAY_CLOSE = chr(6) #^F
# GMCP # GMCP
GMCP = chr(201) GMCP = chr(201)
@ -44,15 +51,16 @@ force_str = lambda inp: to_str(inp, force_string=True)
# pre-compiled regexes # pre-compiled regexes
# returns 2-tuple # returns 2-tuple
msdp_regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, msdp_regex_table = re.compile(r"%s\s*(\w*?)\s*%s\s*%s(.*?)%s" % (MSDP_VAR, MSDP_VAL,
MSDP_ARRAY_OPEN,
MSDP_ARRAY_CLOSE))
# returns 2-tuple (may be nested)
msdp_regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL,
MSDP_TABLE_OPEN, MSDP_TABLE_OPEN,
MSDP_TABLE_CLOSE)) MSDP_TABLE_CLOSE))
msdp_regex_var = re.compile(MSDP_VAR) # returns 2-tuple
msdp_regex_val = re.compile(MSDP_VAL) msdp_regex_array = re.compile(r"%s\s*(\w*?)\s*%s\s*%s(.*?)%s" % (MSDP_VAR, MSDP_VAL,
MSDP_ARRAY_OPEN,
MSDP_ARRAY_CLOSE))
msdp_regex_var = re.compile(r"%s" % MSDP_VAR)
msdp_regex_val = re.compile(r"%s" % MSDP_VAL)
# Msdp object handler # Msdp object handler
@ -104,6 +112,7 @@ class TelnetOOB(object):
self.MSDP = True self.MSDP = True
self.protocol.protocol_flags['OOB'] = True self.protocol.protocol_flags['OOB'] = True
self.protocol.handshake_done() self.protocol.handshake_done()
print "Activated MSDP"
def no_gmcp(self, option): def no_gmcp(self, option):
""" """
@ -127,16 +136,16 @@ class TelnetOOB(object):
self.GMCP = True self.GMCP = True
self.protocol.protocol_flags['OOB'] = True self.protocol.protocol_flags['OOB'] = True
self.protocol.handshake_done() self.protocol.handshake_done()
print "Activated GMCP"
# encoders # encoders
def encode_msdp(self, cmdname, *args, **kwargs): def encode_msdp(self, cmdname, *args, **kwargs):
""" """
handle return data from cmdname by converting it to a proper Encode into a valid MSDP command.
msdp structure. These are the combinations we support:
Args: Args:
cmdname (str): Name of OOB command. cmdname (str): Name of send instruction.
args, kwargs (any): Arguments to OOB command. args, kwargs (any): Arguments to OOB command.
Examples: Examples:
@ -146,55 +155,72 @@ class TelnetOOB(object):
MSDP_ARRAY *args -> MSDP_ARRAY MSDP_ARRAY *args -> MSDP_ARRAY
MSDP_TABLE **kwargs -> MSDP_TABLE MSDP_TABLE **kwargs -> MSDP_TABLE
Notes:
The output of this encoding will always be an
MSDP structure on the form
```
MSDP_VAR cmdname
MSDP_VAL MSDP_ARRAY_OPEN
MSDP_VAL MSDP_ARRAY_OPEN
MSDP_VAL arg1
MSDP_VAL arg2
...
MSDP_ARRAY_CLOSE
MSDP_VAL MSDP_TABLE_OPEN
MSDP_VAR "key1" MSDP_VAL "val1"
MSDP_VAR "key2" MSDP_VAL "val2"
...
MSDP_TABLE_CLOSE
MSDP_ARRAY_CLOSE
```
That is, it's a sequence "cmdnmame [[args] {kwargs}]"
Further nesting is not supported, so if an argument consists
of an array (for example), that array will be json-converted
to a string.
""" """
msdp_string = "" msdp_msg = "{msdp_var}{msdp_cmdname}" \
if args: "{msdp_val}{msdp_array_open}" \
if cmdname == "MSDP_ARRAY": "{msdp_val}{msdp_array_open}" \
msdp_string = "".join(["%s%s" % (MSDP_VAL, val) for val in args]) "{msdp_args}" \
else: "{msdp_array_close}" \
msdp_string = "%s%s%s" % (MSDP_VAR, cmdname, "".join( "{msdp_val}{msdp_table_open}" \
"%s%s" % (MSDP_VAL, val) for val in args)) "{msdp_kwargs}" \
elif kwargs: "{msdp_table_close}" \
if cmdname == "MSDP_TABLE": "{msdp_array_close}".format(
msdp_string = "".join(["%s%s%s%s" % (MSDP_VAR, key, MSDP_VAL, val) msdp_var=MSDP_VAR, msdp_val=MSDP_VAL,
for key, val in kwargs.items()]) msdp_array_open=MSDP_ARRAY_OPEN,
else: msdp_array_close=MSDP_ARRAY_CLOSE,
msdp_string = "%s%s%s" % (MSDP_VAR. cmdname, "".join( msdp_table_open=MSDP_TABLE_OPEN,
["%s%s%s%s" % (MSDP_VAR, key, MSDP_VAL, val) for key, val in kwargs.items()])) msdp_table_close=MSDP_TABLE_CLOSE,
return force_str(msdp_string) msdp_cmdname = json.dumps(cmdname),
msdp_args = "".join("%s%s" % (MSDP_VAL, json.dumps(val)) for val in args),
msdp_kwargs = "".join("%s%s%s%s" % (MSDP_VAR, key, MSDP_VAL, json.dumps(val))
for key, val in kwargs.iteritems()))
return msdp_msg
def encode_gmcp(self, cmdname, *args, **kwargs): def encode_gmcp(self, cmdname, *args, **kwargs):
""" """
Encode GMCP messages. Encode into GMCP messages.
Args: Args:
cmdname (str): GMCP OOB command name. cmdname (str): GMCP OOB command name.
args, kwargs (any): Arguments to OOB command. args, kwargs (any): Arguments to OOB command.
Notes: Notes:
Gmcp messages are on one of the following outgoing forms: GMCP messages will be outgoing on the following
form (the non-JSON cmdname at the start is what
IRE games use, supposedly, and what clients appear
to have adopted):
- cmdname string -> cmdname string cmdname [[args], {kwargs}]
- cmdname *args -> cmdname [arg, arg, arg, ...]
- cmdname **kwargs -> cmdname {key:arg, key:arg, ...}
cmdname is generally recommended to be a string on the form
Module.Submodule.Function
""" """
if cmdname in ("SEND", "REPORT", "UNREPORT", "LIST"): print "GMCP out:", json.dumps([args, kwargs])
# we wrap the standard MSDP commands in a MSDP.submodule return json.dumps("%s %s" % (cmdname, json.dumps([args, kwargs])))
# here as far as GMCP is concerned.
cmdname = "MSDP.%s" % cmdname
elif cmdname in ("MSDP_ARRAY", "MSDP_TABLE"):
# no cmdname should accompany these, just the MSDP wrapper
cmdname = "MSDP"
gmcp_string = ""
if args:
gmcp_string = "%s %s" % (cmdname, json.dumps(args))
elif kwargs:
gmcp_string = "%s %s" % (cmdname, json.dumps(kwargs))
return force_str(gmcp_string).strip()
def decode_msdp(self, data): def decode_msdp(self, data):
""" """
@ -204,45 +230,79 @@ class TelnetOOB(object):
data (str or list): MSDP data. data (str or list): MSDP data.
Notes: Notes:
cmdname var --> cmdname arg Clients should always send MSDP data on
cmdname array --> cmdname *args one of the following forms:
cmdname table --> cmdname **kwargs
cmdname -> [cmdname, [], {}]
cmdname val -> [cmdname, [val], {}]
cmdname array -> [cmdname, [array], {}]
cmdname table -> [cmdname, [], {table}]
cmdname array cmdname table -> [cmdname, [array], {table}]
Observe that all MSDP_VARS are used to identify cmdnames,
so if there are multiple arrays with the same cmdname
given, they will be merged into one argument array, same
for tables. Different MSDP_VARS (outside tables) will be
identified as separate cmdnames.
""" """
if hasattr(data, "__iter__"):
data = "".join(data)
tables = {} tables = {}
arrays = {} arrays = {}
variables = {} variables = {}
if hasattr(data, "__iter__"): # decode tables
data = "".join(data)
# decode
for key, table in msdp_regex_table.findall(data): for key, table in msdp_regex_table.findall(data):
tables[key] = {} tables[key] = {} if not key in tables else tables[key]
for varval in msdp_regex_var.split(table): for varval in msdp_regex_var.split(table)[1:]:
parts = msdp_regex_val.split(varval) var, val = msdp_regex_val.split(varval, 1)
tables[key].expand({parts[0]: tuple(parts[1:]) if len(parts) > 1 else ("",)}) tables[key][var] = val
for key, array in msdp_regex_array.findall(data):
arrays[key] = [] # decode arrays from all that was not a table
for val in msdp_regex_val.split(array): data_no_tables = msdp_regex_table.sub("", data)
arrays[key].append(val) for key, array in msdp_regex_array.findall(data_no_tables):
arrays[key] = tuple(arrays[key]) arrays[key] = [] if not key in arrays else arrays[key]
for varval in msdp_regex_var.split(msdp_regex_array.sub("", msdp_regex_table.sub("", data))): parts = msdp_regex_val.split(array)
# get remaining varvals after cleaning away tables/arrays if len(parts) == 2:
parts = msdp_regex_val.split(varval) arrays[key].append(parts[1])
variables[parts[0]] = tuple(parts[1:]) if len(parts) > 1 else ("", ) elif len(parts) > 1:
arrays[key].extend(parts[1:])
# decode remainders from all that were not tables or arrays
data_no_tables_or_arrays = msdp_regex_array.sub("", data_no_tables)
for varval in msdp_regex_var.split(data_no_tables_or_arrays):
# get remaining varvals after cleaning away tables/arrays. If mathcing
# an existing key in arrays, it will be added as an argument to that command,
# otherwise it will be treated as a command without argument.
parts = msdp_regex_val.split(varval)
if len(parts) == 2:
variables[parts[0]] = parts[1]
elif len(parts) > 1:
variables[parts[0]] = parts[1:]
cmds = {}
# merge matching table/array/variables together
for key, table in tables.iteritems():
args, kwargs = [], table
if key in arrays:
args.extend(arrays.pop(key))
if key in variables:
args.append(variables.pop(key))
cmds[key] = [args, kwargs]
for key, arr in arrays.iteritems():
args, kwargs = arr, {}
if key in variables:
args.append(variables.pop(key))
cmds[key] = [args, kwargs]
for key, var in variables.iteritems():
cmds[key] = [[var], {}]
self.protocol.data_in(**cmds)
# send to the sessionhandler
if data:
for varname, var in variables.items():
# a simple function + argument
self.protocol.data_in(oob=(varname, var, {}))
for arrayname, array in arrays.items():
# we assume the array are multiple arguments to the function
self.protocol.data_in(oob=(arrayname, array, {}))
for tablename, table in tables.items():
# we assume tables are keyword arguments to the function
self.protocol.data_in(oob=(tablename, (), table))
def decode_gmcp(self, data): def decode_gmcp(self, data):
""" """
@ -252,39 +312,48 @@ class TelnetOOB(object):
data (str or list): GMCP data. data (str or list): GMCP data.
Notes: Notes:
cmdname string -> cmdname arg Clients tend to send data on the form "cmdname <structure>".
cmdname [arg, arg,...] -> cmdname *args We assume the structure is valid JSON.
cmdname {key:arg, key:arg, ...} -> cmdname **kwargs
The following is parsed into Evennia's formal structure:
cmdname -> [cmdname, [], {}]
cmdname string -> [cmdname, [string], {}]
cmdname [arg, arg,...] -> [cmdname, [args], {}]
cmdname {key:arg, key:arg, ...} -> [cmdname, [], {kwargs}]
cmdname [[args], {kwargs}] -> [cmdname, [args], {kwargs}]
""" """
if hasattr(data, "__iter__"): if hasattr(data, "__iter__"):
data = "".join(data) data = "".join(data)
print "decode_gmcp in:", data
if data: if data:
splits = data.split(None, 1) try:
cmdname = splits[0] cmdname, structure = data.split(None, 1)
if len(splits) < 2: except ValueError:
self.protocol.data_in(oob=(cmdname, (), {})) self.protocol.data_in(**{data: [[],{}]})
elif splits[1]: return
try: try:
struct = json.loads(splits[1]) structure = json.loads(structure)
except ValueError: except ValueError:
struct = splits[1] pass
args, kwargs = (), {} args, kwargs = [], {}
if hasattr(struct, "__iter__"): if hasattr(structure, "__iter__"):
if isinstance(struct, dict): if isinstance(structure, dict):
kwargs = struct kwargs = structure
else:
args = tuple(struct)
else: else:
args = (struct,) args = list(structure)
self.protocol.data_in(oob=(cmdname, args, kwargs)) else:
args = (structure,)
print "gmcp data in:", {cmdname: [args, kwargs]}
self.protocol.data_in(**{cmdname: [args, kwargs]})
# access methods # access methods
def data_out(self, cmdname, *args, **kwargs): def data_out(self, cmdname, *args, **kwargs):
""" """
Return a msdp-valid subnegotiation across the protocol. Return a MSDP- or GMCP-valid subnegotiation across the protocol.
Args: Args:
cmdname (str): OOB-command name. cmdname (str): OOB-command name.
@ -293,7 +362,9 @@ class TelnetOOB(object):
""" """
if self.MSDP: if self.MSDP:
encoded_oob = self.encode_msdp(cmdname, *args, **kwargs) encoded_oob = self.encode_msdp(cmdname, *args, **kwargs)
print "sending MSDP:", encoded_oob
self.protocol._write(IAC + SB + MSDP + encoded_oob + IAC + SE) self.protocol._write(IAC + SB + MSDP + encoded_oob + IAC + SE)
if self.GMCP: if self.GMCP:
encoded_oob = self.encode_gmcp(cmdname, *args, **kwargs) encoded_oob = self.encode_gmcp(cmdname, *args, **kwargs)
print "sending GMCP:", encoded_oob
self.protocol._write(IAC + SB + GMCP + encoded_oob + IAC + SE) self.protocol._write(IAC + SB + GMCP + encoded_oob + IAC + SE)

View file

@ -341,8 +341,6 @@ class ServerSession(Session):
for the protocol(s). for the protocol(s).
""" """
print "serversession.data_out:", kwargs
self.sessionhandler.data_out(self, **kwargs) self.sessionhandler.data_out(self, **kwargs)
def msg(self, text=None, **kwargs): def msg(self, text=None, **kwargs):

View file

@ -70,7 +70,6 @@ _MODEL_MAP = None
_INPUT_FUNCS = {} _INPUT_FUNCS = {}
for modname in make_iter(settings.INPUT_FUNC_MODULES): for modname in make_iter(settings.INPUT_FUNC_MODULES):
print modname
_INPUT_FUNCS.update(callables_from_module(modname)) _INPUT_FUNCS.update(callables_from_module(modname))
def delayed_import(): def delayed_import():
@ -187,7 +186,6 @@ class SessionHandler(dict):
rkwargs = {} rkwargs = {}
for key, data in kwargs.iteritems(): for key, data in kwargs.iteritems():
print "sessionhandler.clean_senddata:", key, data
key = _validate(key) key = _validate(key)
if not data: if not data:
rkwargs[key] = [ [], {} ] rkwargs[key] = [ [], {} ]
@ -604,11 +602,9 @@ class ServerSessionHandler(SessionHandler):
the wire here. the wire here.
""" """
# clean output for sending # clean output for sending
print "sessionhandler before clean_senddata:", kwargs
kwargs = self.clean_senddata(session, kwargs) kwargs = self.clean_senddata(session, kwargs)
# send across AMP # send across AMP
print "sessionhandler after clean_senddata:", kwargs
self.server.amp_protocol.send_MsgServer2Portal(session, self.server.amp_protocol.send_MsgServer2Portal(session,
**kwargs) **kwargs)
@ -628,12 +624,13 @@ class ServerSessionHandler(SessionHandler):
# distribute incoming data to the correct receiving methods. # distribute incoming data to the correct receiving methods.
if session: if session:
for cmdname, (cmdargs, cmdkwargs) in kwargs.iteritems(): for cmdname, (cmdargs, cmdkwargs) in kwargs.iteritems():
cname = cmdname.strip().lower()
print "sessionhandler.data_in:", session, kwargs
try: try:
if cmdname in _INPUT_FUNCS: if cname in _INPUT_FUNCS:
print "sessionhandler: data_in", cmdname, cmdargs, cmdkwargs _INPUT_FUNCS[cname](session, *cmdargs, **cmdkwargs)
_INPUT_FUNCS[cmdname](session, *cmdargs, **cmdkwargs)
else: else:
_INPUT_FUNCS["default"](session, cmdname, *cmdargs, **cmdkwargs) _INPUT_FUNCS["default"](session, cname, *cmdargs, **cmdkwargs)
except Exception: except Exception:
log_trace() log_trace()