OOB MSDP working with direct sending of data from various parts of the system. Tracking as well as support for the default MSDP commands (LIST, REPORT etc) are not yet tested/implemented.

This commit is contained in:
Griatch 2013-10-15 20:00:18 +02:00
parent 16bbe009c3
commit bdcc8de5bc
10 changed files with 390 additions and 298 deletions

View file

@ -240,7 +240,7 @@ class AMPProtocol(amp.AMP):
def errback(self, e, info):
"error handler, to avoid dropping connections on server tracebacks."
e.trap(Exception)
f = e.trap(Exception)
print "AMP Error for %(info)s: %(e)s" % {'info': info, 'e': e.getErrorMessage()}
def send_split_msg(self, sessid, msg, data, command):
@ -286,7 +286,7 @@ class AMPProtocol(amp.AMP):
data comes in multiple chunks; if so (nparts>1) we buffer the data
and wait for the remaining parts to arrive before continuing.
"""
#print "msg portal -> server (server side):", sessid, msg
#print "msg portal -> server (server side):", sessid, msg, data
global MSGBUFFER
if nparts > 1:
# a multipart message
@ -311,7 +311,7 @@ class AMPProtocol(amp.AMP):
try:
return self.callRemote(MsgPortal2Server,
sessid=sessid,
msg=msg,
msg=to_str(msg) if msg!=None else "",
ipart=0,
nparts=1,
data=dumps(data)).addErrback(self.errback, "MsgPortal2Server")
@ -351,7 +351,7 @@ class AMPProtocol(amp.AMP):
try:
return self.callRemote(MsgServer2Portal,
sessid=sessid,
msg=to_str(msg),
msg=to_str(msg) if msg!=None else "",
ipart=0,
nparts=1,
data=dumps(data)).addErrback(self.errback, "MsgServer2Portal")

View file

@ -22,6 +22,7 @@ oob trackers should inherit from the OOBTracker class in this
"""
from inspect import isfunction
from django.conf import settings
from src.server.models import ServerConfig
from src.server.sessionhandler import SESSIONS
@ -29,16 +30,15 @@ from src.scripts.scripts import Script
from src.utils.create import create_script
from src.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj
from src.utils import logger
from src.utils.utils import variable_from_module, to_str, is_iter, make_iter
from src.utils.utils import all_from_module, to_str, is_iter, make_iter
_SA = object.__setattr__
_GA = object.__getattribute__
_DA = object.__delattribute__
_DA = object.__delattr__
# trackers track property changes and keep returning until they are removed
_OOB_TRACKERS = variable_from_module(settings.OBB_PLUGIN_MODULE, "OBB_TRACKERS", default={})
# functions return immediately
_OOB_FUNCS = variable_from_module(settings.OBB_PLUGIN_MODULE, "OBB_FUNCS", default={})
# load from plugin module
_OOB_FUNCS = dict((key, func) for key, func in all_from_module(settings.OOB_PLUGIN_MODULE).items() if isfunction(func))
_OOB_ERROR = _OOB_FUNCS.get("oob_error", None)
class TrackerHandler(object):
@ -123,10 +123,8 @@ class OOBTracker(TrackerBase):
def update(self, new_value, *args, **kwargs):
"Called by cache when updating the tracked entitiy"
SESSIONS.session_from_sessid(self.sessid).msg(oob={"cmdkey":"trackreturn",
"name":self.fieldname,
"value":new_value})
SESSIONS.session_from_sessid(self.sessid).msg(oob=("trackreturn",
(self.fieldname, new_value)))
class _RepeaterPool(object):
"""
@ -194,14 +192,6 @@ class _RepeaterPool(object):
self.scripts[interval].stop()
# Default OOB funcs
def OOB_get_attr_val(caller, attrname):
"Get the given attrback from caller"
caller.msg(oob={"cmdkey":"get_attr",
"name":attrname,
"value":to_str(caller.attributes.get(attrname))})
# Main OOB Handler
class OOBHandler(object):
@ -214,6 +204,7 @@ class OOBHandler(object):
"""
Initialize handler
"""
self.sessionhandler = SESSIONS
self.oob_tracker_storage = {}
self.oob_repeat_storage = {}
self.oob_tracker_pool = _RepeaterPool()
@ -247,7 +238,7 @@ class OOBHandler(object):
self.repeat(caller, func_key, interval, *args, **kwargs)
def track(self, obj, sessid, fieldname, tracker_key, *args, **kwargs):
def track(self, obj, sessid, fieldname, oobclass, *args, **kwargs):
"""
Create an OOB obj of class _oob_MAPPING[tracker_key] on obj. args,
kwargs will be used to initialize the OOB hook before adding
@ -307,17 +298,6 @@ class OOBHandler(object):
oob_tracker_name = "_track_db_value_change"
self.track(attrobj, tracker_key, attr_name, sessid, property_name=oob_tracker_name)
def execute_cmd(self, func_key, *args, **kwargs):
"""
Retrieve oobfunc from OOB_FUNCS and execute it immediately
using *args and **kwargs
"""
oobfunc = _OOB_FUNCS[func_key] # raise traceback if not found
try:
oobfunc(*args, **kwargs)
except Exception:
logger.log_trace()
def repeat(self, caller, func_key, interval=20, *args, **kwargs):
"""
Start a repeating action. Every interval seconds,
@ -339,7 +319,31 @@ class OOBHandler(object):
self.oob_tracker_pool.remove(store_key, interval)
self.oob_repeat_storage.pop(store_key, None)
def msg(self, sessid, funcname, *args, **kwargs):
"Shortcut to relay oob data back to portal"
session = self.sessionhandler.session_from_sessid(sessid)
if session:
session.msg(oob=(funcname, args, kwargs))
def execute_cmd(self, session, func_key, *args, **kwargs):
"""
Retrieve oobfunc from OOB_FUNCS and execute it immediately
using *args and **kwargs
"""
try:
oobfunc = _OOB_FUNCS[func_key] # raise traceback if not found
oobfunc(self, session, *args, **kwargs)
except KeyError:
errmsg = "OOB Error: function '%s' not recognized." % func_key
if _OOB_ERROR:
_OOB_ERROR(self, session, errmsg, *args, **kwargs)
else:
logger.log_trace(errmsg)
except Exception, err:
errmsg = "OOB Error: Exception in '%s'(%s, %s):\n%s" % (func_key, args, kwargs, err)
if _OOB_ERROR:
_OOB_ERROR(self, session, errmsg, *args, **kwargs)
else:
logger.log_trace(errmsg)
# access object
OOB_HANDLER = OOBHandler()

View file

@ -12,7 +12,7 @@ etc.
"""
import re
from django.conf import settings
from src.utils.utils import make_iter, mod_import
from src.utils.utils import make_iter, mod_import, to_str
from src.utils import logger
# MSDP-relevant telnet cmd/opt-codes
@ -28,10 +28,13 @@ IAC = chr(255)
SB = chr(250)
SE = chr(240)
force_str = lambda inp: to_str(inp, force_string=True)
# pre-compiled regexes
regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, MSDP_ARRAY_OPEN, MSDP_ARRAY_CLOSE)) # return 2-tuple
regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, MSDP_TABLE_OPEN, MSDP_TABLE_CLOSE)) # return 2-tuple (may be nested)
regex_varval = re.compile(r"%s(.*?)%s(.*)" % (MSDP_VAR, MSDP_VAL)) # return 2-tuple
regex_var = re.compile(MSDP_VAR)
regex_val = re.compile(MSDP_VAL)
# MSDP default definition commands supported by Evennia (can be supplemented with custom commands as well)
MSDP_COMMANDS = ("LIST", "REPORT", "RESET", "SEND", "UNREPORT")
@ -138,59 +141,75 @@ class Msdp(object):
def no_msdp(self, option):
"No msdp supported or wanted"
print "No msdp supported"
pass
def do_msdp(self, option):
"""
Called when client confirms that it can do MSDP.
"""
print "msdp supported"
self.protocol.protocol_flags['MSDP'] = True
def evennia_to_msdp(self, cmdname, data):
def evennia_to_msdp(self, cmdname, *args, **kwargs):
"""
handle return data from cmdname by converting it to
a proper msdp structure. data can either be a single value (will be
converted to a string), a list (will be converted to an MSDP_ARRAY),
or a dictionary (will be converted to MSDP_TABLE).
OBS - this supports nested tables and even arrays nested
inside tables, as opposed to the receive method. Arrays
cannot hold tables by definition (the table must be named
with MSDP_VAR, and an array can only contain MSDP_VALs).
Obs - this normally only returns tables and lists (var val val ...) rather than
arrays. It will convert *args to lists and **kwargs to tables and
if both are given to this method, this will result in a list followed
by a table, both having the same names.
"""
def make_table(name, datadict, string):
def make_table(name, **kwargs):
"build a table that may be nested with other tables or arrays."
string += MSDP_VAR + name + MSDP_VAL + MSDP_TABLE_OPEN
for key, val in datadict.items():
if type(val) == type({}):
string += make_table(key, val, string)
string = MSDP_VAR + force_str(name) + MSDP_VAL + MSDP_TABLE_OPEN
for key, val in kwargs.items():
if isinstance(val, dict):
string += make_table(string, key, **val)
elif hasattr(val, '__iter__'):
string += make_array(key, val, string)
string += make_array(string, key, *val)
else:
string += MSDP_VAR + key + MSDP_VAL + val
string += MSDP_VAR + force_str(key) + MSDP_VAL + force_str(val)
string += MSDP_TABLE_CLOSE
return string
def make_array(name, datalist, string):
"build a simple array. Arrays may not nest tables by definition."
print "make_array", datalist, string
string += MSDP_VAR + name + MSDP_ARRAY_OPEN
for val in datalist:
string += MSDP_VAL + val
def make_array(name, *args):
"build a array. Arrays may not nest tables by definition."
string = MSDP_VAR + force_str(name) + MSDP_ARRAY_OPEN
string += MSDP_VAL.join(force_str(arg) for arg in args)
string += MSDP_ARRAY_CLOSE
return string
if isinstance(data, dict):
msdp_string = make_table(cmdname, data, "")
elif hasattr(data, '__iter__'):
msdp_string = make_array(cmdname, data, "")
else:
msdp_string = MSDP_VAR + cmdname + MSDP_VAL + data if data!=None else ""
return msdp_string
def make_list(name, *args):
"build a simple list - an array without start/end markers"
string = MSDP_VAR + force_str(name)
string += MSDP_VAL.join(force_str(arg) for arg in args)
return string
# Default MSDP commands
cupper = cmdname.upper()
if cupper == "LIST":
self.data_out(make_list("LIST", *args))
elif cupper == "REPORT":
self.data_out(make_list("REPORT", *args))
elif cupper == "UNREPORT":
self.data_out(make_list("UNREPORT", *args))
elif cupper == "RESET":
self.data_out(make_list("RESET", *args))
elif cupper == "SEND":
self.data_out(make_list("SEND", *args))
else:
# return list or tables. If both arg/kwarg is given, return one array and one table, both
# with the same name.
msdp_string = ""
if args:
msdp_string += make_list(cupper, *args)
if kwargs:
msdp_string += make_table(cupper, **kwargs)
self.data_out(msdp_string)
def msdp_to_evennia(self, data):
"""
@ -216,16 +235,24 @@ class Msdp(object):
if hasattr(data, "__iter__"):
data = "".join(data)
logger.log_infomsg("MSDP SUBNEGOTIATION: %s" % data)
#logger.log_infomsg("MSDP SUBNEGOTIATION: %s" % data)
for table in regex_table.findall(data):
tables[table[0].upper()] = dict(regex_varval.findall(table[1]))
for array in regex_array.findall(data):
arrays[array[0].upper()] = dict(regex_varval.findall(array[1]))
# get all stand-alone variables, but first we must clean out all tables and arrays (which also contain vars)
variables = dict((key.upper(), val) for key, val in regex_varval.findall(regex_array.sub("", regex_table.sub("", data))))
for key, table in regex_table.findall(data):
tables[key] = {}
for varval in regex_var.split(table):
parts = regex_val.split(varval)
tables[key].expand({parts[0] : tuple(parts[1:]) if len(parts)>1 else ("",)})
for key, array in regex_array.findall(data):
arrays[key] = []
for val in regex_val.split(array):
arrays[key].append(val)
arrays[key] = tuple(arrays[key])
for varval in regex_var.split(regex_array.sub("", regex_table.sub("", data))):
# get remaining varvals after cleaning away tables/arrays
parts = regex_val.split(varval)
variables[parts[0].upper()] = tuple(parts[1:]) if len(parts)>1 else ("", )
print "MSDP: table, array, variables:", tables, arrays, variables
#print "MSDP: table, array, variables:", tables, arrays, variables
# all variables sent through msdp to Evennia are considered commands with arguments.
# there are three forms of commands possible through msdp:
@ -235,122 +262,124 @@ class Msdp(object):
# TABLENAME TABLE VARNAME VAL VARNAME VAL ENDTABLE -> tablename(varname=val, varname=val)
#
ret = ""
# default MSDP functions
if "LIST" in variables:
ret += self.evennia_to_msdp("LIST", self.msdp_cmd_list(*(variables.pop("LIST"),)))
self.data_in("list", *variables.pop("LIST"))
if "REPORT" in variables:
ret += self.evennia_to_msdp("REPORT", self.msdp_cmd_report(*(variables.pop("REPORT"),)))
self.data_in("report", *variables.pop("REPORT"))
if "REPORT" in arrays:
ret += self.evennia_to_msdp("REPORT", self.msdp_cmd_report(*arrays.pop("REPORT")))
self.data_in("report", *(arrays.pop("REPORT")))
if "UNREPORT" in variables:
self.data_in("unreport", *(arrays.pop("UNREPORT")))
if "RESET" in variables:
ret += self.evennia_to_msdp("RESET", self.msdp_cmd_reset(*(variables.pop("RESET"),)))
self.data_in("reset", *variables.pop("RESET"))
if "RESET" in arrays:
ret += self.evennia_to_msdp("RESET", self.msdp_cmd_reset(*arrays.pop("RESET",)))
self.data_in("reset", *(arrays.pop("RESET")))
if "SEND" in variables:
ret += self.evennia_to_msdp("SEND", self.msdp_cmd_send(*(variables.pop("SEND",))))
self.data_in("send", *variables.pop("SEND"))
if "SEND" in arrays:
ret += self.evennia_to_msdp("SEND",self.msdp_cmd_send(*arrays.pop("SEND")))
self.data_in("send", *(arrays.pop("SEND")))
# if there are anything left consider it a call to a custom function
# if there are anything left we look for a custom function
for varname, var in variables.items():
# a simple function + argument
ooc_func = MSDP_COMMANDS_CUSTOM.get(varname.upper())
if ooc_func:
ret += self.evennia_to_msdp(varname, ooc_func(var))
self.data_in(varname, (var,))
for arrayname, array in arrays.items():
# we assume the array are multiple arguments to the function
ooc_func = MSDP_COMMANDS_CUSTOM.get(arrayname.upper())
if ooc_func:
ret += self.evennia_to_msdp(arrayname, ooc_func(*array))
self.data_in(arrayname, *array)
for tablename, table in tables.items():
# we assume tables are keyword arguments to the function
ooc_func = MSDP_COMMANDS_CUSTOM.get(arrayname.upper())
if ooc_func:
ret += self.evennia_to_msdp(tablename, ooc_func(**table))
# return any result
if ret:
self.data_out(ret)
self.data_in(tablename, **table)
def data_out(self, msdp_string):
"""
Return a msdp-valid subnegotiation across the protocol.
"""
self.protocol._write(IAC + SB + MSDP + msdp_string + IAC + SE)
#print "msdp data_out (without IAC SE):", msdp_string
self.protocol ._write(IAC + SB + MSDP + force_str(msdp_string) + IAC + SE)
# MSDP Commands
# Some given MSDP (varname, value) pairs can also be treated as command + argument.
# Generic msdp command map. The argument will be sent to the given command.
# See http://tintin.sourceforge.net/msdp/ for definitions of each command.
# These are client->server commands.
def msdp_cmd_list(self, arg):
def data_in(self, funcname, *args, **kwargs):
"""
The List command allows for retrieving various info about the server/client
Send oob data to Evennia
"""
if arg == 'COMMANDS':
return self.evennia_to_msdp(arg, MSDP_COMMANDS)
elif arg == 'LISTS':
return self.evennia_to_msdp(arg, ("COMMANDS", "LISTS", "CONFIGURABLE_VARIABLES",
"REPORTED_VARIABLES", "SENDABLE_VARIABLES"))
elif arg == 'CONFIGURABLE_VARIABLES':
return self.evennia_to_msdp(arg, ("CLIENT_NAME", "CLIENT_VERSION", "PLUGIN_ID"))
elif arg == 'REPORTABLE_VARIABLES':
return self.evennia_to_msdp(arg, MSDP_REPORTABLE.keys())
elif arg == 'REPORTED_VARIABLES':
# the dynamically set items to report
return self.evennia_to_msdp(arg, self.msdp_reported.keys())
elif arg == 'SENDABLE_VARIABLES':
return self.evennia_to_msdp(arg, MSDP_SENDABLE.keys())
else:
return self.evennia_to_msdp("LIST", arg)
#print "msdp data_in:", funcname, args, kwargs
self.protocol.data_in(text=None, oob=(funcname, args, kwargs))
# default msdp commands
# # MSDP Commands
# # Some given MSDP (varname, value) pairs can also be treated as command + argument.
# # Generic msdp command map. The argument will be sent to the given command.
# # See http://tintin.sourceforge.net/msdp/ for definitions of each command.
# # These are client->server commands.
# def msdp_cmd_list(self, arg):
# """
# The List command allows for retrieving various info about the server/client
# """
# if arg == 'COMMANDS':
# return self.evennia_to_msdp(arg, MSDP_COMMANDS)
# elif arg == 'LISTS':
# return self.evennia_to_msdp(arg, ("COMMANDS", "LISTS", "CONFIGURABLE_VARIABLES",
# "REPORTED_VARIABLES", "SENDABLE_VARIABLES"))
# elif arg == 'CONFIGURABLE_VARIABLES':
# return self.evennia_to_msdp(arg, ("CLIENT_NAME", "CLIENT_VERSION", "PLUGIN_ID"))
# elif arg == 'REPORTABLE_VARIABLES':
# return self.evennia_to_msdp(arg, MSDP_REPORTABLE.keys())
# elif arg == 'REPORTED_VARIABLES':
# # the dynamically set items to report
# return self.evennia_to_msdp(arg, self.msdp_reported.keys())
# elif arg == 'SENDABLE_VARIABLES':
# return self.evennia_to_msdp(arg, MSDP_SENDABLE.keys())
# else:
# return self.evennia_to_msdp("LIST", arg)
def msdp_cmd_report(self, *arg):
"""
The report command instructs the server to start reporting a
reportable variable to the client.
"""
try:
return MSDP_REPORTABLE[arg](report=True)
except Exception:
logger.log_trace()
# # default msdp commands
def msdp_cmd_unreport(self, arg):
"""
Unreport a previously reported variable
"""
try:
MSDP_REPORTABLE[arg](report=False)
except Exception:
self.logger.log_trace()
# def msdp_cmd_report(self, *arg):
# """
# The report command instructs the server to start reporting a
# reportable variable to the client.
# """
# try:
# return MSDP_REPORTABLE[arg](report=True)
# except Exception:
# logger.log_trace()
def msdp_cmd_reset(self, arg):
"""
The reset command resets a variable to its initial state.
"""
try:
MSDP_REPORTABLE[arg](reset=True)
except Exception:
logger.log_trace()
# def msdp_cmd_unreport(self, arg):
# """
# Unreport a previously reported variable
# """
# try:
# MSDP_REPORTABLE[arg](report=False)
# except Exception:
# self.logger.log_trace()
def msdp_cmd_send(self, arg):
"""
Request the server to send a particular variable
to the client.
# def msdp_cmd_reset(self, arg):
# """
# The reset command resets a variable to its initial state.
# """
# try:
# MSDP_REPORTABLE[arg](reset=True)
# except Exception:
# logger.log_trace()
arg - this is a list of variables the client wants.
"""
ret = []
if arg:
for var in make_iter(arg):
try:
ret.append(MSDP_REPORTABLE[var.upper()])# (send=True))
except Exception:
ret.append("ERROR")#logger.log_trace()
return ret
# def msdp_cmd_send(self, *args):
# """
# Request the server to send a particular variable
# to the client.
# arg - this is a list of variables the client wants.
# """
# ret = []
# for var in make_iter(arg)
# for var in make_iter(arg):
# try:
# ret.append(MSDP_REPORTABLE[var.upper()])# (send=True))
# except Exception:
# ret.append("ERROR")#logger.log_trace()
# return ret

View file

@ -127,7 +127,6 @@ class PortalSessionHandler(SessionHandler):
in from the protocol to the server. data is
serialized before passed on.
"""
#print "portal_data_in:", string
self.portal.amp_protocol.call_remote_MsgPortal2Server(session.sessid,
msg=text,
data=kwargs)

View file

@ -85,13 +85,6 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
be handled in line mode. Some clients also sends an erroneous
line break after IAC, which we must watch out for.
"""
#print "dataRcv (%s):" % data,
#try:
# for b in data:
# print ord(b),
# print ""
#except Exception, e:
# print str(e) + ":", str(data)
if data and data[0] == IAC or self.iaw_mode:
try:
@ -102,8 +95,16 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
else:
self.iaw_mode = False
return
except Exception:
logger.log_trace()
except Exception, err1:
conv = ""
try:
for b in data:
conv += " " + repr(ord(b))
except Exception, err2:
conv = str(err2) + ":", str(data)
out = "Telnet Error (%s): %s (%s)" % (err1, data, conv)
logger.log_trace(out)
return
# if we get to this point the command must end with a linebreak.
# We make sure to add it, to fix some clients messing this up.
data = data.rstrip("\r\n") + "\n"
@ -130,7 +131,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
Telnet method called when data is coming in over the telnet
connection. We pass it on to the game engine directly.
"""
self.sessionhandler.data_in(self, string)
self.data_in(text=string)
# Session hooks
@ -144,11 +145,17 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
self.data_out(reason)
self.connectionLost(reason)
def data_in(self, text=None, **kwargs):
"""
Data Telnet -> Server
"""
self.sessionhandler.data_in(self, text=text, **kwargs)
def data_out(self, text=None, **kwargs):
"""
Data Evennia -> Player.
generic hook method for engine to call in order to send data
through the telnet connection.
Data Evennia -> Player.
valid telnet kwargs:
raw=True - pass string through without any ansi processing (i.e. include Evennia
@ -165,11 +172,10 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
if "oob" in kwargs:
oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob"))
if "MSDP" in self.protocol_flags:
print "oobstruct:", oobstruct
for cmdname, args in oobstruct:
print "cmdname, args:", cmdname, args
msdp_string = self.msdp.func_to_msdp(cmdname, args)
print "msdp_string:", msdp_string
for cmdname, args, kwargs in oobstruct:
#print "cmdname, args, kwargs:", cmdname, args, kwargs
msdp_string = self.msdp.evennia_to_msdp(cmdname, *args, **kwargs)
#print "msdp_string:", msdp_string
self.msdp.data_out(msdp_string)
ttype = self.protocol_flags.get('TTYPE', {})

View file

@ -137,6 +137,14 @@ class ServerSession(Session):
return self.logged_in and self.puppet
get_character = get_puppet
def get_puppet_or_player(self):
"""
Returns session if not logged in; puppet if one exists, otherwise return the player.
"""
if self.logged_in:
return self.puppet if self.puppet else self.player
return None
def log(self, message, channel=True):
"""
Emits session info to the appropriate outputs and info channels.
@ -182,10 +190,11 @@ class ServerSession(Session):
# handle oob instructions
global _OOB_HANDLER
if not _OOB_HANDLER:
from src.servever.oobhandler import OOB_HANDLER as _OOB_HANDLER
from src.server.oobhandler import OOB_HANDLER as _OOB_HANDLER
oobstruct = self.sessionhandler.oobstruct_parser(kwargs.pop("oob", None))
for (funcname, args, kwargs) in oobstruct:
_OOBHANDLER.execute_cmd(funcname, *args, **kwargs)
if funcname:
_OOB_HANDLER.execute_cmd(self, funcname, *args, **kwargs)
execute_cmd = data_in # alias

View file

@ -97,6 +97,55 @@ class SessionHandler(object):
"""
return dict((sessid, sess.get_sync_data()) for sessid, sess in self.sessions.items())
def oobstruct_parser(self, oobstruct):
"""
Helper method for each session to use to parse oob structures
(The 'oob' kwarg of the msg() method)
allowed oob structures are
cmdname
((cmdname,), (cmdname,))
(cmdname,(arg, ))
(cmdname,(arg1,arg2))
(cmdname,{key:val,key2:val2})
(cmdname, (args,), {kwargs})
((cmdname, (arg1,arg2)), cmdname, (cmdname, (arg1,)))
outputs an ordered structure on the form
((cmdname, (args,), {kwargs}), ...), where the two last parts of each tuple may be empty
"""
def _parse(oobstruct):
slen = len(oobstruct)
if not oobstruct:
return tuple(None, (), {})
elif not hasattr(oobstruct, "__iter__"):
# a singular command name, without arguments or kwargs
return (oobstruct.lower(), (), {})
# regardless of number of args/kwargs, the first element must be the function name.
# we will not catch this error if not, but allow it to propagate.
if slen == 1:
return (oobstruct[0].lower(), (), {})
elif slen == 2:
if isinstance(oobstruct[1], dict):
# cmdname, {kwargs}
return (oobstruct[0].lower(), (), dict(oobstruct[1]))
elif isinstance(oobstruct[1], (tuple, list)):
# cmdname, (args,)
return (oobstruct[0].lower(), tuple(oobstruct[1]), {})
else:
# cmdname, (args,), {kwargs}
return (oobstruct[0].lower(), tuple(oobstruct[1]), dict(oobstruct[2]))
if hasattr(oobstruct, "__iter__"):
# differentiate between (cmdname, cmdname), (cmdname, args, kwargs) and ((cmdname,args,kwargs), (cmdname,args,kwargs), ...)
if oobstruct and isinstance(oobstruct[0], basestring):
return (tuple(_parse(oobstruct)),)
else:
out = []
for oobpart in oobstruct:
out.append(_parse(oobpart))
return (tuple(out),)
return (_parse(oobstruct),)
#------------------------------------------------------------
# Server-SessionHandler class
#------------------------------------------------------------
@ -358,48 +407,6 @@ class ServerSessionHandler(SessionHandler):
return self.sessions.get(sessid)
return None
def oobstruct_parser(self, oobstruct):
"""
Helper method for each session to use to parse oob structures
(The 'oob' kwarg of the msg() method)
allowed oob structures are
cmdname
(cmdname, cmdname)
(cmdname,(arg, ))
(cmdname,(arg1,arg2))
(cmdname,{key:val,key2:val2})
(cmdname, (args,), {kwargs})
((cmdname, (arg1,arg2)), cmdname, (cmdname, (arg1,)))
outputs an ordered structure on the form
((cmdname, (args,), {kwargs}), ...), where the two last parts of each tuple may be empty
"""
slen = len(oobstruct)
if not oobstruct:
return tuple(None, (), {})
elif not hasattr(oobstruct, "__iter__"):
# a singular command name, without arguments or kwargs
return (oobstruct.lower(), (), {})
# regardless of number of args/kwargs, the first element must be the function name.
# we will not catch this error if not, but allow it to propagate.
if slen == 1:
return (oobstruct[0].lower(), (), {})
elif slen == 2:
if isinstance(oobstruct[1], dict):
# cmdname, {kwargs}
return (oobstruct[0].lower(), (), dict((key.lower(), val) for key,val in oobstruct[1].items()))
elif isinstance(oobstruct[1], (tuple, list)):
# cmdname, (args,)
return (oobstruct[0].lower(), tuple(arg.lower() for arg in oobstruct[1]), {})
else:
# cmdname, (args,), {kwargs}
return (oobstruct[0].lower(), tuple(arg.lower for arg in oobstruct[1]),
dict((key.lower(), val) for key, val in oobstruct[2].items()))
# either multiple funcnames or multiple func tuples; descend recursively
out = []
for oobpart in oobstruct:
out.append(self.oobstruct_parser(oobpart)[0])
return tuple(out)
def announce_all(self, message):
"""

View file

@ -199,7 +199,7 @@ MSSP_META_MODULE = ""
# Module holding OOB (Out of Band) hook objects. This allows for customization
# and expansion of which hooks OOB protocols are allowed to call on the server
# protocols for attaching tracker hooks for when various object field change
OOB_PLUGIN_MODULE = ""
OOB_PLUGIN_MODULE = "src.server.oob_defaults"
# Tuple of modules implementing lock functions. All callable functions
# inside these modules will be available as lock functions.
LOCK_FUNC_MODULES = ("src.locks.lockfuncs",)

View file

@ -437,7 +437,7 @@ class NickHandler(AttributeHandler):
with categories nick_<nicktype>
"""
def has(self, key, category="inputline"):
categry = "nick_%s" % category
category = "nick_%s" % category
return super(NickHandler, self).has(key, category=category)
def get(self, key=None, category="inputline", **kwargs):
@ -462,6 +462,34 @@ class NickHandler(AttributeHandler):
return super(NickHandler, self).all(category=category)
return _GA(self.obj, self._m2m_fieldname).filter(db_category__startswith="nick_")
class NAttributeHandler(object):
"""
This stand-alone handler manages non-database saved properties by storing them
as properties on obj.ndb. It has the same methods as AttributeHandler, but they
are much simplified.
"""
def __init__(self, obj):
"initialized on the object"
self.ndb = _GA(obj, "ndb")
def has(self, key):
"Check if object has this attribute or not"
return _GA(self.ndb, key) # ndb returns None if not found
def get(self, key):
"Returns named key value"
return _GA(self.ndb, key)
def add(self, key, value):
"Add new key and value"
_SA(self.ndb, key, value)
def remove(self, key):
"Remove key from storage"
_DA(self.ndb, key)
def all(self):
"List all keys stored"
if callable(self.ndb.all):
return self.ndb.all()
else:
return [val for val in self.ndb.__dict__.keys() if not val.startswith('_')]
#------------------------------------------------------------
#
# Tags
@ -645,6 +673,7 @@ class TypedObject(SharedMemoryModel):
_SA(self, "dbobj", self) # this allows for self-reference
_SA(self, "locks", LockHandler(self))
_SA(self, "permissions", PermissionHandler(self))
_SA(self, "nattributes", NAttributeHandler(self))
class Meta:
"""
@ -1148,6 +1177,9 @@ class TypedObject(SharedMemoryModel):
if hperm in perms and hpos > ppos)
return False
#
# Memory management
#
def flush_from_cache(self):
"""
@ -1157,6 +1189,60 @@ class TypedObject(SharedMemoryModel):
"""
self.__class__.flush_cached_instance(self)
#
# Attribute storage
#
#@property db
def __db_get(self):
"""
Attribute handler wrapper. Allows for the syntax
obj.db.attrname = value
and
value = obj.db.attrname
and
del obj.db.attrname
and
all_attr = obj.db.all (unless there is no attribute named 'all', in which
case that will be returned instead).
"""
try:
return self._db_holder
except AttributeError:
class DbHolder(object):
"Holder for allowing property access of attributes"
def __init__(self, obj):
_SA(self, 'obj', obj)
_SA(self, "attrhandler", _GA(_GA(self, "obj"), "attributes"))
def __getattribute__(self, attrname):
if attrname == 'all':
# we allow to overload our default .all
attr = _GA(self, "attrhandler").get("all")
if attr:
return attr
return _GA(self, 'all')
return _GA(self, "attrhandler").get(attrname)
def __setattr__(self, attrname, value):
_GA(self, "attrhandler").add(attrname, value)
def __delattr__(self, attrname):
_GA(self, "attrhandler").remove(attrname)
def get_all(self):
return _GA(self, "attrhandler").all()
all = property(get_all)
self._db_holder = DbHolder(self)
return self._db_holder
#@db.setter
def __db_set(self, value):
"Stop accidentally replacing the db object"
string = "Cannot assign directly to db object! "
string += "Use db.attr=value instead."
raise Exception(string)
#@db.deleter
def __db_del(self):
"Stop accidental deletion."
raise Exception("Cannot delete the db object!")
db = property(__db_get, __db_set, __db_del)
#
# Non-persistent (ndb) storage
#
@ -1202,35 +1288,12 @@ class TypedObject(SharedMemoryModel):
raise Exception("Cannot delete the ndb object!")
ndb = property(__ndb_get, __ndb_set, __ndb_del)
#def nattr(self, attribute_name=None, value=None, delete=False):
# """
# This allows for assigning non-persistent data on the object using
# a method call. Will return None if trying to access a non-existing property.
# """
# if attribute_name == None:
# # act as a list method
# if callable(self.ndb.all):
# return self.ndb.all()
# else:
# return [val for val in self.ndb.__dict__.keys()
# if not val.startswith['_']]
# elif delete == True:
# if hasattr(self.ndb, attribute_name):
# _DA(_GA(self, "ndb"), attribute_name)
# elif value == None:
# # act as a getter.
# if hasattr(self.ndb, attribute_name):
# _GA(_GA(self, "ndb"), attribute_name)
# else:
# return None
# else:
# # act as a setter
# _SA(self.ndb, attribute_name, value)
#
# Attribute handler methods - DEPRECATED!
# ***** DEPRECATED METHODS BELOW *******
#
#
@ -1366,56 +1429,31 @@ class TypedObject(SharedMemoryModel):
# creating a new attribute - check access on storing object!
_GA(self, "attributes").add(attribute_name, value, accessing_obj=accessing_object, default_access=default_access_create)
#@property
def __db_get(self):
def nattr(self, attribute_name=None, value=None, delete=False):
"""
A second convenience wrapper for the the attribute methods. It
allows for the syntax
obj.db.attrname = value
and
value = obj.db.attrname
and
del obj.db.attrname
and
all_attr = obj.db.all (unless there is no attribute named 'all', in which
case that will be returned instead).
This allows for assigning non-persistent data on the object using
a method call. Will return None if trying to access a non-existing property.
"""
try:
return self._db_holder
except AttributeError:
class DbHolder(object):
"Holder for allowing property access of attributes"
def __init__(self, obj):
_SA(self, 'obj', obj)
_SA(self, "attrhandler", _GA(_GA(self, "obj"), "attributes"))
def __getattribute__(self, attrname):
if attrname == 'all':
# we allow to overload our default .all
attr = _GA(self, "attrhandler").get("all")
if attr:
return attr
return _GA(self, 'all')
return _GA(self, "attrhandler").get(attrname)
def __setattr__(self, attrname, value):
_GA(self, "attrhandler").add(attrname, value)
def __delattr__(self, attrname):
_GA(self, "attrhandler").remove(attrname)
def get_all(self):
return _GA(self, "attrhandler").all()
all = property(get_all)
self._db_holder = DbHolder(self)
return self._db_holder
#@db.setter
def __db_set(self, value):
"Stop accidentally replacing the db object"
string = "Cannot assign directly to db object! "
string += "Use db.attr=value instead."
raise Exception(string)
#@db.deleter
def __db_del(self):
"Stop accidental deletion."
raise Exception("Cannot delete the db object!")
db = property(__db_get, __db_set, __db_del)
logger.log_depmsg("obj.nattr() is deprecated. Use obj.nattributes instead.")
if attribute_name == None:
# act as a list method
if callable(self.ndb.all):
return self.ndb.all()
else:
return [val for val in self.ndb.__dict__.keys()
if not val.startswith['_']]
elif delete == True:
if hasattr(self.ndb, attribute_name):
_DA(_GA(self, "ndb"), attribute_name)
elif value == None:
# act as a getter.
if hasattr(self.ndb, attribute_name):
_GA(_GA(self, "ndb"), attribute_name)
else:
return None
else:
# act as a setter
_SA(self.ndb, attribute_name, value)

View file

@ -729,10 +729,10 @@ def mod_import(module):
def all_from_module(module):
"""
Return all global-level variables from a module
Return all global-level variables from a module as a dict
"""
mod = mod_import(module)
return [val for key, val in mod.__dict__.items() if not (key.startswith("_") or ismodule(val))]
return dict((key, val) for key, val in mod.__dict__.items() if not (key.startswith("_") or ismodule(val)))
def variable_from_module(module, variable=None, default=None):
"""