diff --git a/evennia/server/oobhandler.py b/evennia/server/inputhandler.py similarity index 100% rename from evennia/server/oobhandler.py rename to evennia/server/inputhandler.py diff --git a/evennia/server/oob_cmds.py b/evennia/server/inputhandler_funcs.py similarity index 83% rename from evennia/server/oob_cmds.py rename to evennia/server/inputhandler_funcs.py index 3301f751cf..ea2c08e608 100644 --- a/evennia/server/oob_cmds.py +++ b/evennia/server/inputhandler_funcs.py @@ -59,16 +59,57 @@ from future.utils import viewkeys from django.conf import settings from evennia.utils.utils import to_str -from evennia.server.oobhandler import OOB_HANDLER +from evennia.commands.cmdhandler import cmdhandler +#from evennia.server.inputhandler import INPUT_HANDLER +_IDLE_COMMAND = settings.IDLE_COMMAND _GA = object.__getattribute__ _SA = object.__setattr__ _NA = lambda o: "N/A" +def text(session, *args, **kwargs): + """ + Main text input from the client. This will execute a command + string on the server. + + Args: + text (str): First arg is used as text-command input. Other + arguments are ignored. + + """ + #from evennia.server.profiling.timetrace import timetrace + #text = timetrace(text, "ServerSession.data_in") + + text = args[0] if args else None + + #explicitly check for None since text can be an empty string, which is + #also valid + if text is None: + return + # this is treated as a command input + #text = to_unicode(escape_control_sequences(text), encoding=self.encoding) + # handle the 'idle' command + if text.strip() == _IDLE_COMMAND: + session.update_session_counters(idle=True) + return + if session.player: + # nick replacement + puppet = session.puppet + if puppet: + text = puppet.nicks.nickreplace(text, + categories=("inputline", "channel"), include_player=True) + else: + text = session.player.nicks.nickreplace(text, + categories=("inputline", "channels"), include_player=False) + cmdhandler(session, text, callertype="session", session=session) + session.update_session_counters() + + +#------------------------------------------------------------------------------------ #------------------------------------------------------------ # All OOB commands must be on the form -# cmdname(oobhandler, session, *args, **kwargs) +# cmdname(session, *args, **kwargs) #------------------------------------------------------------ # @@ -126,7 +167,7 @@ def oob_repeat(session, oobfuncname, interval, *args, **kwargs): interval = 20 if not interval else (max(5, interval)) obj = session.get_puppet_or_player() if obj and oobfuncname != "REPEAT": - OOB_HANDLER.add_repeater(obj, session, oobfuncname, interval, *args, **kwargs) + INPUT_HANDLER.add_repeater(obj, session, oobfuncname, interval, *args, **kwargs) ##OOB{"UNREPEAT":10} @@ -147,7 +188,7 @@ def oob_unrepeat(session, oobfuncname, interval): """ obj = session.get_puppet_or_player() if obj: - OOB_HANDLER.remove_repeater(obj, session, oobfuncname, interval) + INPUT_HANDLER.remove_repeater(obj, session, oobfuncname, interval) # @@ -165,7 +206,7 @@ def oob_unrepeat(session, oobfuncname, interval): # mapping from MSDP standard names to Evennia variables -OOB_SENDABLE = { +_OOB_SENDABLE = { "CHARACTER_NAME": lambda o: o.key, "SERVER_ID": lambda o: settings.SERVERNAME, "ROOM_NAME": lambda o: o.db_location.key, @@ -210,7 +251,7 @@ def oob_send(session, *args, **kwargs): # mapping standard MSDP keys to Evennia field names -OOB_REPORTABLE = { +_OOB_REPORTABLE = { "CHARACTER_NAME": "db_key", "ROOM_NAME": "db_location", "TEST" : "test" @@ -252,10 +293,10 @@ def oob_report(session, *args, **kwargs): oob_error(session, "No Reportable property '%s'. Use LIST REPORTABLE_VARIABLES." % propname) # the field_monitors require an oob function as a callback when they report a change. elif propname.startswith("db_"): - OOB_HANDLER.add_field_monitor(obj, session, propname, "return_field_report") + INPUT_HANDLER.add_field_monitor(obj, session, propname, "return_field_report") ret.append(to_str(_GA(obj, propname), force_string=True)) else: - OOB_HANDLER.add_attribute_monitor(obj, session, propname, "return_attribute_report") + INPUT_HANDLER.add_attribute_monitor(obj, session, propname, "return_attribute_report") ret.append(_GA(obj, "db_value")) session.msg(oob=("MSDP_ARRAY", ret)) else: @@ -314,9 +355,9 @@ def oob_unreport(session, *args, **kwargs): if not propname: oob_error(session, "No Un-Reportable property '%s'. Use LIST REPORTABLE_VARIABLES." % propname) elif propname.startswith("db_"): - OOB_HANDLER.remove_field_monitor(obj, session, propname, "oob_return_field_report") + INPUT_HANDLER.remove_field_monitor(obj, session, propname, "oob_return_field_report") else: # assume attribute - OOB_HANDLER.remove_attribute_monitor(obj, session, propname, "oob_return_attribute_report") + INPUT_HANDLER.remove_attribute_monitor(obj, session, propname, "oob_return_attribute_report") else: oob_error(session, "You must log in first.") @@ -359,7 +400,7 @@ def oob_list(session, mode, *args, **kwargs): # we need to check so as to use the right return value depending on if it is # an Attribute (identified by tracking the db_value field) or a normal database field # reported is a list of tuples (obj, propname, args, kwargs) - reported = OOB_HANDLER.get_all_monitors(session) + reported = INPUT_HANDLER.get_all_monitors(session) reported = [rep[0].key if rep[1] == "db_value" else rep[1] for rep in reported] session.msg(oob=("REPORTED_VARIABLES", reported)) elif mode == "SENDABLE_VARIABLES": @@ -381,17 +422,17 @@ def oob_list(session, mode, *args, **kwargs): # this maps the commands to the names available to use from # the oob call. The standard MSDP commands are capitalized # as per the protocol, Evennia's own commands are not. -CMD_MAP = {"oob_error": oob_error, # will get error messages - "return_field_report": oob_return_field_report, - "return_attribute_report": oob_return_attribute_report, - # MSDP - "REPEAT": oob_repeat, - "UNREPEAT": oob_unrepeat, - "SEND": oob_send, - "ECHO": oob_echo, - "REPORT": oob_report, - "UNREPORT": oob_unreport, - "LIST": oob_list, - # GMCP - } +#CMD_MAP = {"oob_error": oob_error, # will get error messages +# "return_field_report": oob_return_field_report, +# "return_attribute_report": oob_return_attribute_report, +# # MSDP +# "REPEAT": oob_repeat, +# "UNREPEAT": oob_unrepeat, +# "SEND": oob_send, +# "ECHO": oob_echo, +# "REPORT": oob_report, +# "UNREPORT": oob_unreport, +# "LIST": oob_list, +# # GMCP +# } diff --git a/evennia/server/portal/portalsessionhandler.py b/evennia/server/portal/portalsessionhandler.py index 0563e503c8..81737900a3 100644 --- a/evennia/server/portal/portalsessionhandler.py +++ b/evennia/server/portal/portalsessionhandler.py @@ -356,8 +356,10 @@ class PortalSessionHandler(SessionHandler): session (Session): Session sending data. Kwargs: - text (str): Text from protocol. - kwargs (any): Other data from protocol. + kwargs (any): Each key is a command instruction to the + protocol on the form key = [[args],{kwargs}]. This will + call a method send_ on the protocol. If no such + method exixts, it send the data to a method send_default. """ #from evennia.server.profiling.timetrace import timetrace @@ -365,13 +367,12 @@ class PortalSessionHandler(SessionHandler): # distribute outgoing data to the correct session methods. if session: - print ("portalsessionhandler.data_out:", session, kwargs, session.datamap) - for cmdname, args in kwargs.items(): + print ("portalsessionhandler.data_out:", session, kwargs) + for cmdname, (cmdargs, cmdkwargs) in kwargs.iteritems(): try: - if cmdname in session.datamap: - session.datamap[cmdname](session, *args) - else: - session.datamap["_default"](session, *args) + getattr(session, "send_%s" % cmdname)(session, *cmdargs, **cmdkwargs) + except AttributeError: + session.send_default(session, *cmdargs, **cmdkwargs) except Exception: log_trace() diff --git a/evennia/server/portal/webclient.py b/evennia/server/portal/webclient.py index eff226b07e..e402ac0fe2 100644 --- a/evennia/server/portal/webclient.py +++ b/evennia/server/portal/webclient.py @@ -90,7 +90,7 @@ class WebSocketClient(Protocol, Session): Method called when data is coming in over the websocket connection. This is always a JSON object on the following form: - [cmdname, arg, arg2, ...] + [cmdname, [args], {kwargs}] """ diff --git a/evennia/server/serversession.py b/evennia/server/serversession.py index 44510c402a..35eb603830 100644 --- a/evennia/server/serversession.py +++ b/evennia/server/serversession.py @@ -18,11 +18,9 @@ from evennia.utils import logger from evennia.utils.inlinefunc import parse_inlinefunc from evennia.utils.nested_inlinefuncs import parse_inlinefunc as parse_nested_inlinefunc from evennia.utils.utils import make_iter, lazy_property -from evennia.commands.cmdhandler import cmdhandler from evennia.commands.cmdsethandler import CmdSetHandler from evennia.server.session import Session -_IDLE_COMMAND = settings.IDLE_COMMAND _GA = object.__getattribute__ _SA = object.__setattr__ _ObjectDB = None @@ -32,7 +30,6 @@ _INLINEFUNC_ENABLED = settings.INLINEFUNC_ENABLED # i18n from django.utils.translation import ugettext as _ - # Handlers for Session.db/ndb operation class NDbHolder(object): @@ -168,8 +165,6 @@ class ServerSession(Session): self.player = None self.cmdset_storage_string = "" self.cmdset = CmdSetHandler(self, True) - self.datamap = {"text": self.recv_text, - "_default": self.recv_text} def __cmdset_storage_get(self): return [path.strip() for path in self.cmdset_storage_string.split(',')] @@ -337,42 +332,6 @@ class ServerSession(Session): # Player-visible idle time, not used in idle timeout calcs. self.cmd_last_visible = self.cmd_last - @staticmethod - def recv_text(session, *args, **kwargs): - """ - Recv command data User->Evennia. This will in effect execute a command - string on the server. - - Args: - text (str): First arg is used as text-command input. Other - arguments are ignored. - - """ - #from evennia.server.profiling.timetrace import timetrace - #text = timetrace(text, "ServerSession.data_in") - - text = args[0] if args else None - - #explicitly check for None since text can be an empty string, which is - #also valid - if text is not None: - # this is treated as a command input - #text = to_unicode(escape_control_sequences(text), encoding=self.encoding) - # handle the 'idle' command - if text.strip() == _IDLE_COMMAND: - session.update_session_counters(idle=True) - return - if session.player: - # nick replacement - puppet = session.puppet - if puppet: - text = puppet.nicks.nickreplace(text, - categories=("inputline", "channel"), include_player=True) - else: - text = session.player.nicks.nickreplace(text, - categories=("inputline", "channels"), include_player=False) - cmdhandler(session, text, callertype="session", session=session) - session.update_session_counters() def data_out(self, text=None, **kwargs): """ @@ -384,6 +343,11 @@ class ServerSession(Session): by their keys. Or "options", carrying options for the protocol(s). + Notes: + We need to handle inlinefunc-parsing at this point + since we must have access to the database and the + Server Session. + """ print "serversession.data_out:", text, kwargs if text: @@ -392,7 +356,7 @@ class ServerSession(Session): else: text, args = text, [] print("kwargs", kwargs, kwargs.get("options", {})) - options = kwargs.get("options", None) or {} + options = kwargs.get("options", [None, {}])[1] raw = options.get("raw", False) strip_inlinefunc = options.get("strip_inlinefunc", False) if _INLINEFUNC_ENABLED and not raw: diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index e5532f851e..482b13cc24 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -19,14 +19,22 @@ from time import time from django.conf import settings from evennia.commands.cmdhandler import CMD_LOGINSTART from evennia.utils.logger import log_trace -from evennia.utils.utils import variable_from_module, is_iter, \ - to_str, to_unicode, strip_control_sequences, make_iter +from evennia.utils.utils import (variable_from_module, is_iter, + to_str, to_unicode, + make_iter, + callables_from_module) try: import cPickle as pickle except ImportError: import pickle +# input handlers + +_INPUT_HANDLER_FUNCS = {} +for modname in make_iter(settings.INPUT_HANDLER_MODULES): + _INPUT_HANDLER_FUNCS.update(callables_from_module(modname)) + # delayed imports _PlayerDB = None _ServerSession = None @@ -123,15 +131,24 @@ class SessionHandler(dict): Args: session (Session): The relevant session instance. - kwargs (dict): Every keyword represents a send-instruction. + kwargs (dict) Each keyword represents a + send-instruction, with the keyword itself being the name + of the instruction (like "text"). Suitable values for each + keyword are: + - arg -> [[arg], {}] + - [args] -> [[args], {}] + - {kwargs} -> [[], {kwargs}] + - [args, {kwargs}] -> [[arg], {kwargs}] + - [[args], {kwargs}] -> [[args], {kwargs}] Returns: - kwargs (dict): A cleaned dictionary of cmdname:args pairs, - where the keys and args have all been converted to + kwargs (dict): A cleaned dictionary of cmdname:[[args],{kwargs}] pairs, + where the keys, args and kwargs have all been converted to send-safe entities (strings or numbers). """ def _validate(data): + "Helper function to convert data to AMP-safe (picketable) values" if isinstance(data, dict): newdict = {} for key, part in data.items(): @@ -140,23 +157,36 @@ class SessionHandler(dict): elif hasattr(data, "__iter__"): return [_validate(part) for part in data] elif isinstance(data, basestring): + # make sure strings are in a valid encoding try: return data and to_str(to_unicode(data), encoding=session.encoding) except LookupError: # wrong encoding set on the session. Set it to a safe one session.encoding = "utf-8" return to_str(to_unicode(data), encoding=session.encoding) - elif hasattr(data, "id") and hasattr(data, "db_date_created") and hasattr(data, '__dbclass__'): + elif hasattr(data, "id") and hasattr(data, "db_date_created") \ + and hasattr(data, '__dbclass__'): # convert database-object to their string representation. return _validate(unicode(data)) else: return data - clean_kwargs = {"options":kwargs.pop("options", {})} - for key in kwargs: - args = _validate(kwargs[key]) - clean_kwargs[_validate(key)] = (args,) if args is not None and \ - not hasattr(args, "__iter__") else args - return clean_kwargs + + rkwargs = {} + for key, data in kwargs.iteritems(): + if not data: + rkwargs[key] = [ [], {} ] + elif isinstance(data, dict): + rkwargs[key] = [ [], _validate(data) ] + elif hasattr(data, "__iter__"): + if isinstance(data[-1], dict): + # last entry is a kwarg dict + rkwargs[key] = [ _validate(data[:-1]), _validate(data[-1]) ] + else: + rkwargs[key] = [ _validate(data), {} ] + else: + rkwargs[key] = [ [_validate(data)], {} ] + + return rkwargs #------------------------------------------------------------ @@ -574,13 +604,13 @@ class ServerSessionHandler(SessionHandler): # distribute incoming data to the correct receiving methods. if session: - for cmdname, args in kwargs.items(): + for cmdname, (cmdargs, cmdkwargs) in kwargs.iteritems(): try: - if cmdname in session.datamap: - print "sessionhandler: data_in", cmdname, args - session.datamap[cmdname](session, *args) + if cmdname in _INPUT_HANDLER_FUNCS: + print "sessionhandler: data_in", cmdname, cmdargs, cmdkwargs + _INPUT_HANDLER_FUNCS[cmdname](session, *cmdargs, **cmdkwargs) else: - session.datamap["_default"](session, *args) + _INPUT_HANDLER_FUNCS["_default"](session, *cmdargs, **cmdkwargs) except Exception: log_trace() diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 5d2c245130..bf00cb5f04 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -33,7 +33,7 @@ TELNET_INTERFACES = ['0.0.0.0'] # special commands and data with enabled Telnet clients. This is used # to create custom client interfaces over a telnet connection. To make # full use of OOB, you need to prepare functions to handle the data -# server-side (see OOB_FUNC_MODULE). TELNET_ENABLED is required for this +# server-side (see INPUT_HANDLER_MODULES). TELNET_ENABLED is required for this # to work. TELNET_OOB_ENABLED = False # Start the evennia django+twisted webserver so you can @@ -276,10 +276,10 @@ MSSP_META_MODULE = "server.conf.mssp" # Tuple of modules implementing lock functions. All callable functions # inside these modules will be available as lock functions. LOCK_FUNC_MODULES = ("evennia.locks.lockfuncs", "server.conf.lockfuncs",) -# 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_MODULES = ["evennia.server.oob_cmds", "server.conf.oobfuncs"] +# Module holding handlers for managing incoming data from the client. These +# will be loaded in order, meaning functions in later modules may overload +# previous ones if having the same name. +INPUT_HANDLER_MODULES = ["evennia.server.inputhandler_funcs", "server.conf.inputhandler_funcs"] # Module holding settings/actions for the dummyrunner program (see the # dummyrunner for more information) DUMMYRUNNER_SETTINGS_MODULE = "evennia.server.profiling.dummyrunner_settings"