diff --git a/evennia/__init__.py b/evennia/__init__.py index 080114f7a0..81d99e8764 100644 --- a/evennia/__init__.py +++ b/evennia/__init__.py @@ -109,6 +109,7 @@ TASK_HANDLER = None TICKER_HANDLER = None MONITOR_HANDLER = None CHANNEL_HANDLER = None +VALID_HANDLER = None def _create_version(): @@ -152,7 +153,7 @@ def _init(): global search_object, search_script, search_account, search_channel, search_help, search_tag, search_message global create_object, create_script, create_account, create_channel, create_message, create_help_entry global settings, lockfuncs, logger, utils, gametime, ansi, spawn, managers - global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER, CHANNEL_HANDLER, TASK_HANDLER + global contrib, TICKER_HANDLER, MONITOR_HANDLER, SESSION_HANDLER, CHANNEL_HANDLER, TASK_HANDLER, VALID_HANDLER global EvMenu, EvTable, EvForm, EvMore, EvEditor global ANSIString @@ -213,6 +214,7 @@ def _init(): from .server.sessionhandler import SESSION_HANDLER from .comms.channelhandler import CHANNEL_HANDLER from .scripts.monitorhandler import MONITOR_HANDLER + from .utils.valid import VALID_HANDLER # initialize the doc string global __doc__ diff --git a/evennia/settings_default.py b/evennia/settings_default.py index 4e582b11e3..cccdf3c03e 100644 --- a/evennia/settings_default.py +++ b/evennia/settings_default.py @@ -521,7 +521,7 @@ TIME_GAME_EPOCH = None TIME_IGNORE_DOWNTIMES = False ###################################################################### -# Inlinefunc & PrototypeFuncs +# Inlinefunc, PrototypeFuncs, and ValidFuncs ###################################################################### # Evennia supports inline function preprocessing. This allows users # to supply inline calls on the form $func(arg, arg, ...) to do @@ -538,6 +538,10 @@ INLINEFUNC_MODULES = ["evennia.utils.inlinefuncs", PROTOTYPEFUNC_MODULES = ["evennia.utils.prototypefuncs", "server.conf.prototypefuncs"] +# Module holding validator functions. functions in later modules will +# override those in earlier ones. +VALIDFUNC_MODULES = ['evennia.utils.validfuncs', ] + ###################################################################### # Default Account setup and access ###################################################################### diff --git a/evennia/utils/valid.py b/evennia/utils/valid.py new file mode 100644 index 0000000000..29000821ee --- /dev/null +++ b/evennia/utils/valid.py @@ -0,0 +1,16 @@ +from django.conf import settings +from evennia.utils.utils import callables_from_module + + +class ValidHandler(object): + + def __init__(self): + self.valid_storage = {} + for module in settings.VALIDFUNC_MODULES: + self.valid_storage.update(callables_from_module(module)) + + def __getitem__(self, item): + return self.valid_storage.get(item, None) + + +VALID_HANDLER = ValidHandler() diff --git a/evennia/utils/validfuncs.py b/evennia/utils/validfuncs.py new file mode 100644 index 0000000000..4265b94347 --- /dev/null +++ b/evennia/utils/validfuncs.py @@ -0,0 +1,175 @@ +""" +Contains all the validation functions. + +All validation functions must have a checker (probably a session) and entry arg. + +They can employ more paramters at your leisure. +""" + +import re as _re +import pytz as _pytz +import datetime as _dt +from django.core.exceptions import ValidationError as _error +from django.core.validators import validate_email as _val_email +from evennia.utils.ansi import ANSIString as _ansi +from evennia.utils.utils import string_partial_matching as _partial + +_TZ_DICT = {str(tz): _pytz.timezone(tz) for tz in _pytz.common_timezones} + + +def color(entry, thing_name='Color'): + if not entry: + raise ValueError(f"Nothing entered for a {thing_name}!") + test_str = _ansi('|%s|n' % entry) + if len(test_str): + raise ValueError(f"'{entry}' is not a valid {thing_name}.") + return entry + + +def datetime(entry, thing_name='Datetime', from_tz=None): + """ + + Args: + entry (str): A date string from a user. + thing_name (str): Name to display this datetime as. + from_tz (pytz): An instance of pytz from the user. + + Returns: + datetime in utc. + """ + if not entry: + raise ValueError(f"No {thing_name} entered!") + if not from_tz: + from_tz = _pytz['UTC'] + utc = _pytz['UTC'] + now = _dt.datetime.utcnow().replace(tzinfo=utc) + cur_year = now.strftime('%Y') + split_time = entry.split(' ') + if len(split_time) == 3: + entry = f"{split_time[0]} {split_time[1]} {split_time[2]} {cur_year}" + elif len(split_time) == 4: + entry = f"{split_time[0]} {split_time[1]} {split_time[2]} {split_time[3]}" + else: + raise ValueError(f"{thing_name} must be entered in a 24-hour format such as: {now.strftime('%b %d %H:%H')}") + try: + local = _dt.datetime.strptime(input, '%b %d %H:%M %Y') + except ValueError: + raise ValueError(f"{thing_name} must be entered in a 24-hour format such as: {now.strftime('%b %d %H:%H')}") + local_tz = from_tz.localize(local) + return local_tz.astimezone(utc) + + +def duration(entry, thing_name='Duration'): + """ + Take a string and derive a datetime timedelta from it. + + Args: + entry (string): This is a string from user-input. The intended format is, for example: "5d 2w 90s" for + 'five days, two weeks, and ninety seconds.' Invalid sections are ignored. + thing_name (str): Name to display this query as. + + Returns: + timedelta + + """ + time_string = entry.split(" ") + seconds = 0 + minutes = 0 + hours = 0 + days = 0 + weeks = 0 + + for interval in time_string: + if _re.match(r'^[\d]+s$', interval.lower()): + seconds =+ int(interval.lower().rstrip("s")) + elif _re.match(r'^[\d]+m$', interval): + minutes =+ int(interval.lower().rstrip("m")) + elif _re.match(r'^[\d]+h$', interval): + hours =+ int(interval.lower().rstrip("h")) + elif _re.match(r'^[\d]+d$', interval): + days =+ int(interval.lower().rstrip("d")) + elif _re.match(r'^[\d]+w$', interval): + weeks =+ int(interval.lower().rstrip("w")) + elif _re.match(r'^[\d]+y$', interval): + days =+ int(interval.lower().rstrip("y")) * 365 + else: + raise ValueError(f"Could not convert section '{interval}' to a {thing_name}.") + + return _dt.timedelta(days, seconds, 0, 0, minutes, hours, weeks) + + +def future(entry, thing_name="Future Datetime", from_tz=None): + time = datetime(entry, thing_name) + if time < _dt.datetime.utcnow(): + raise ValueError(f"That {thing_name} is in the past! Must give a Future datetime!") + return time + + +def signed_integer(entry, thing_name="Signed Integer"): + if not entry: + raise ValueError(f"Must enter a whole number for {thing_name}!") + try: + num = int(entry) + except ValueError: + raise ValueError(f"Could not convert '{entry}' to a whole number for {thing_name}!") + return num + + +def positive_integer(entry, thing_name="Positive Integer"): + num = signed_integer(entry, thing_name) + if not num >= 1: + raise ValueError(f"Must enter a whole number greater than 0 for {thing_name}!") + return num + + +def unsigned_integer(entry, thing_name="Unsigned Integer"): + num = signed_integer(entry, thing_name) + if not num >= 0: + raise ValueError(f"{thing_name} must be a whole number greater than or equal to 0!") + return num + + +def boolean(entry, thing_name="True/False"): + entry = entry.upper() + error = f"Must enter 0 (false) or 1 (true) for {thing_name}. Also accepts True, False, On, Off, Yes, No, Enabled, and Disabled" + if not entry: + raise ValueError(error) + if entry in ('1', 'TRUE', 'ON', 'ENABLED', 'ENABLE', 'YES'): + return True + if entry in ('0', 'FALSE', 'OFF', 'DISABLED', 'DISABLE', 'NO'): + return False + raise ValueError(error) + + +def timezone(entry, thing_name="Timezone"): + if not entry: + raise ValueError(f"No {thing_name} entered!") + found = _partial(_TZ_DICT.keys(), entry) + if found: + return _TZ_DICT[found] + raise ValueError(f"Could not find timezone '{entry}' for {thing_name}!") + + +def email(entry, thing_name="Email Address"): + try: + _val_email(entry) #offloading the hard work to Django! + except _error: + raise ValueError(f"That isn't a valid {thing_name}!") + return entry + + +def lockstring(entry, thing_name='lockstring', options=None): + entry = entry.strip() + if not entry: + raise ValueError(f"No {thing_name} entered to set!") + for locksetting in entry.split(';'): + access_type, lockfunc = locksetting.split(':', 1) + if not access_type: + raise ValueError("Must enter an access type!") + if options: + accmatch = _partial(options, access_type) + if not accmatch: + raise ValueError(f"Access type must be one of: {', '.join(options)}") + if not lockfunc: + raise ValueError("Lock func not entered.") + return entry \ No newline at end of file