Added support for GMCP out-of-band messaging, as a backup handler to MSDP. Starting to rework the oob system to be more straightforward to follow and understand.
This commit is contained in:
parent
f1b6a4e212
commit
eda15ccc45
9 changed files with 416 additions and 555 deletions
238
evennia/server/portal/telnet_oob.py
Normal file
238
evennia/server/portal/telnet_oob.py
Normal file
|
|
@ -0,0 +1,238 @@
|
|||
"""
|
||||
|
||||
Telnet OOB (Out of band communication)
|
||||
|
||||
This implements the following telnet oob protocols:
|
||||
MSDP (Mud Server Data Protocol)
|
||||
GMCP (Generic Mud Communication Protocol)
|
||||
|
||||
This implements the MSDP protocol as per
|
||||
http://tintin.sourceforge.net/msdp/ and the GMCP 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
|
||||
client supports MSDP and if not, we fallback to GMCP with a MSDP
|
||||
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.
|
||||
|
||||
"""
|
||||
import re
|
||||
import json
|
||||
from evennia.utils.utils import to_str
|
||||
|
||||
# MSDP-relevant telnet cmd/opt-codes
|
||||
MSDP = chr(69)
|
||||
MSDP_VAR = chr(1)
|
||||
MSDP_VAL = chr(2)
|
||||
MSDP_TABLE_OPEN = chr(3)
|
||||
MSDP_TABLE_CLOSE = chr(4)
|
||||
MSDP_ARRAY_OPEN = chr(5)
|
||||
MSDP_ARRAY_CLOSE = chr(6)
|
||||
|
||||
GMCP = chr(200)
|
||||
|
||||
IAC = chr(255)
|
||||
SB = chr(250)
|
||||
SE = chr(240)
|
||||
|
||||
force_str = lambda inp: to_str(inp, force_string=True)
|
||||
|
||||
# pre-compiled regexes
|
||||
# returns 2-tuple
|
||||
msdp_regex_array = re.compile(r"%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_CLOSE))
|
||||
msdp_regex_var = re.compile(MSDP_VAR)
|
||||
msdp_regex_val = re.compile(MSDP_VAL)
|
||||
|
||||
# Msdp object handler
|
||||
|
||||
class Telnet_OOB(object):
|
||||
"""
|
||||
Implements the MSDP and GMCP protocols.
|
||||
"""
|
||||
|
||||
def __init__(self, protocol):
|
||||
"""
|
||||
Initiates by storing the protocol
|
||||
on itself and trying to determine
|
||||
if the client supports MSDP.
|
||||
"""
|
||||
self.protocol = protocol
|
||||
self.protocol.protocol_flags['OOB'] = False
|
||||
self.MSDP = False
|
||||
self.GMCP = False
|
||||
# detect MSDP first
|
||||
self.protocol.negotiationMap[MSDP] = self.data_in
|
||||
self.protocol.will(MSDP).addCallbacks(self.do_msdp, self.no_msdp)
|
||||
self.oob_reported = {}
|
||||
|
||||
def no_msdp(self, option):
|
||||
"No msdp supported or wanted"
|
||||
# no msdp, check GMCP
|
||||
self.protocol.negotiationMap[GMCP] = self.data_in
|
||||
self.protocol.will(GMCP).addCallbacks(self.do_oob, self.no_oob)
|
||||
|
||||
def do_msdp(self, option):
|
||||
"MSDP supported by client"
|
||||
self.MSDP = True
|
||||
self.protocol.protocol_flags['OOB'] = True
|
||||
self.protocol.handshake_done()
|
||||
|
||||
def no_gmcp(self, option):
|
||||
"Neither MSDP nor GMCP supported"
|
||||
self.protocol.handshake_done()
|
||||
|
||||
def do_gmcp(self, option):
|
||||
"""
|
||||
Called when client confirms that it can do MSDP or GMCP.
|
||||
"""
|
||||
self.GMCP = True
|
||||
self.protocol.protocol_flags['OOB'] = True
|
||||
self.protocol.handshake_done()
|
||||
|
||||
# encoders
|
||||
|
||||
def encode_msdp(self, cmdname, *args, **kwargs):
|
||||
"""
|
||||
handle return data from cmdname by converting it to
|
||||
a proper msdp structure. These are the combinations we
|
||||
support:
|
||||
|
||||
cmdname string -> cmdname string
|
||||
cmdname *args -> cmdname MSDP_ARRAY
|
||||
cmdname **kwargs -> cmdname MSDP_TABLE
|
||||
|
||||
OBS - whereas there are also definitions for making arrays and tables in
|
||||
the specification, these are not actually used in the default
|
||||
msdp commands -- returns are implemented as simple lists or
|
||||
named lists (our name for them here, these un-bounded
|
||||
structures are not named in the specification). So for now,
|
||||
this routine will not explicitly create arrays nor tables,
|
||||
although there are helper methods ready should it be needed in
|
||||
the future.
|
||||
"""
|
||||
msdp_string = ""
|
||||
if args:
|
||||
if len(args) == 1:
|
||||
msdp_string = "%s %s" % (cmdname.upper(), args[0])
|
||||
else:
|
||||
msdp_string = "%s%s%s%s" % (MSDP_VAR, cmdname.upper(), "".join(
|
||||
"%s%s" % (MSDP_VAL, val) for val in args))
|
||||
elif kwargs:
|
||||
msdp_string = "%s%s%s" % (MSDP_VAR. cmdname.upper(), "".join(
|
||||
["%s%s%s%s" % (MSDP_VAR, key, MSDP_VAL, val) for key, val in kwargs.items()]))
|
||||
return msdp_string
|
||||
|
||||
def encode_gmcp(self, cmdname, *args, **kwargs):
|
||||
"""
|
||||
Gmcp messages are on one of the following outgoing forms:
|
||||
|
||||
cmdname string -> cmdname string
|
||||
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 args:
|
||||
gmcp_string = "%s %s" % (cmdname, json.dumps(args))
|
||||
elif kwargs:
|
||||
gmcp_string = "%s %s" % (cmdname, json.dumps(kwargs))
|
||||
return gmcp_string
|
||||
|
||||
def decode_msdp(self, data):
|
||||
"""
|
||||
Decodes incoming MSDP data
|
||||
|
||||
cmdname, var --> cmdname arg
|
||||
cmdname, array --> cmdname *args
|
||||
cmdname, table --> cmdname **kwargs
|
||||
|
||||
"""
|
||||
tables = {}
|
||||
arrays = {}
|
||||
variables = {}
|
||||
|
||||
if hasattr(data, "__iter__"):
|
||||
data = "".join(data)
|
||||
|
||||
#logger.log_infomsg("MSDP SUBNEGOTIATION: %s" % data)
|
||||
|
||||
# decode
|
||||
for key, table in msdp_regex_table.findall(data):
|
||||
tables[key] = {}
|
||||
for varval in msdp_regex_var.split(table):
|
||||
parts = msdp_regex_val.split(varval)
|
||||
tables[key].expand({parts[0]: tuple(parts[1:]) if len(parts) > 1 else ("",)})
|
||||
for key, array in msdp_regex_array.findall(data):
|
||||
arrays[key] = []
|
||||
for val in msdp_regex_val.split(array):
|
||||
arrays[key].append(val)
|
||||
arrays[key] = tuple(arrays[key])
|
||||
for varval in msdp_regex_var.split(msdp_regex_array.sub("", msdp_regex_table.sub("", data))):
|
||||
# get remaining varvals after cleaning away tables/arrays
|
||||
parts = msdp_regex_val.split(varval)
|
||||
variables[parts[0].upper()] = tuple(parts[1:]) if len(parts) > 1 else ("", )
|
||||
|
||||
# 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):
|
||||
"""
|
||||
Decodes incoming GMCP data on the form 'varname <structure>'
|
||||
|
||||
cmdname string -> cmdname arg
|
||||
cmdname [arg, arg,...] -> cmdname *args
|
||||
cmdname {key:arg, key:arg, ...} -> cmdname **kwargs
|
||||
|
||||
"""
|
||||
if data:
|
||||
splits = data.split(" ", 1)
|
||||
cmdname = splits[0]
|
||||
if len(splits) < 2:
|
||||
self.protocol.data_in(oob=(cmdname, (), {}))
|
||||
else:
|
||||
struct = json.loads(splits[1])
|
||||
self.protocol.data_in(oob=(cmdname,
|
||||
struct if isinstance(struct, list) else (),
|
||||
struct if isinstance(struct, dict) else {}))
|
||||
|
||||
# access methods
|
||||
|
||||
def data_out(self, cmdname, *args, **kwargs):
|
||||
"""
|
||||
Return a msdp-valid subnegotiation across the protocol.
|
||||
"""
|
||||
if self.MSDP:
|
||||
encoded_oob = force_str(self.encode_msdp(cmdname, *args, **kwargs))
|
||||
self.protocol._write(IAC + SB + MSDP + encoded_oob + IAC + SE)
|
||||
else:
|
||||
encoded_oob = force_str(self.encode_gmcp(cmdname, *args, **kwargs))
|
||||
self.protocol._write(IAC + SB + GMCP + encoded_oob + IAC + SE)
|
||||
|
||||
def data_in(self, data):
|
||||
"""
|
||||
Send oob data to Evennia. The self.decode_* methods send to
|
||||
protocol.data_in() themselves.
|
||||
"""
|
||||
if self.MSDP:
|
||||
self.decode_msdp(data)
|
||||
else:
|
||||
self.decode_gmcp(data)
|
||||
Loading…
Add table
Add a link
Reference in a new issue