Update CHANGELOG, pep8 fixes
This commit is contained in:
parent
0b6d869902
commit
b6b07ccdb5
10 changed files with 290 additions and 264 deletions
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -10,6 +10,30 @@
|
||||||
- Add the Portal uptime to the `@time` command.
|
- Add the Portal uptime to the `@time` command.
|
||||||
- Make the `@link` command first make a local search before a global search.
|
- Make the `@link` command first make a local search before a global search.
|
||||||
|
|
||||||
|
### Typeclasses
|
||||||
|
|
||||||
|
- Add new methods on all typeclasses, useful specifically for viewing the object in the web/admin:
|
||||||
|
+ `web_get_admin_url()`: Returns a path that, if followed, will display the object in the Admin backend.
|
||||||
|
+ `web_get_create_url()`: Returns a path for a view allowing the creation of new instances of this object.
|
||||||
|
+ `web_get_absolute_url()`: Django construct; returns a path that should display the object in a DetailView.
|
||||||
|
+ `web_get_update_url()`: Returns a path that should display the object in an UpdateView.
|
||||||
|
+ `web_get_delete_url()`: Returns a path that should display the object in a DeleteView.
|
||||||
|
- All typeclasses has new helper class method `create`, which encompasses useful functionality
|
||||||
|
that used to be embedded for example in the respective `@create` or `@connect` commands.
|
||||||
|
- DefaultAccount now has new class methods implementing many things that used to be in unloggedin
|
||||||
|
commands (these can now be customized on the class instead):
|
||||||
|
+ `is_banned()`: Checks if a given username or IP is banned.
|
||||||
|
+ `get_username_validators`: Return list of validators for username validation (see
|
||||||
|
`settings.AUTH_USERNAME_VALIDATORS`)
|
||||||
|
+ `authenticate`: Method to check given username/password.
|
||||||
|
+ `normalize_username`: Normalizes names so you can't fake names with similar-looking Unicode
|
||||||
|
chars.
|
||||||
|
+ `validate_username`: Mechanism for validating a username.
|
||||||
|
+ `validate_password`: Mechanism for validating a password.
|
||||||
|
+ `set_password`: Apply password to account, using validation checks.
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
### Utils
|
### Utils
|
||||||
|
|
||||||
- Added more unit tests.
|
- Added more unit tests.
|
||||||
|
|
@ -34,7 +58,7 @@
|
||||||
to terminal and can be stopped with Ctrl-C. Using `evennia reload`, or reloading in-game, will
|
to terminal and can be stopped with Ctrl-C. Using `evennia reload`, or reloading in-game, will
|
||||||
return Server to normal daemon operation.
|
return Server to normal daemon operation.
|
||||||
- For validating passwords, use safe Django password-validation backend instead of custom Evennia one.
|
- For validating passwords, use safe Django password-validation backend instead of custom Evennia one.
|
||||||
- Alias `evennia restart` to mean the same as `evennia reload`.
|
- Alias `evennia restart` to mean the same as `evennia reload`.
|
||||||
|
|
||||||
### Prototype changes
|
### Prototype changes
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -370,40 +370,40 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
def is_banned(cls, **kwargs):
|
def is_banned(cls, **kwargs):
|
||||||
"""
|
"""
|
||||||
Checks if a given username or IP is banned.
|
Checks if a given username or IP is banned.
|
||||||
|
|
||||||
Kwargs:
|
Kwargs:
|
||||||
ip (str, optional): IP address.
|
ip (str, optional): IP address.
|
||||||
username (str, optional): Username.
|
username (str, optional): Username.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
is_banned (bool): Whether either is banned or not.
|
is_banned (bool): Whether either is banned or not.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
ip = kwargs.get('ip', '').strip()
|
ip = kwargs.get('ip', '').strip()
|
||||||
username = kwargs.get('username', '').lower().strip()
|
username = kwargs.get('username', '').lower().strip()
|
||||||
|
|
||||||
# Check IP and/or name bans
|
# Check IP and/or name bans
|
||||||
bans = ServerConfig.objects.conf("server_bans")
|
bans = ServerConfig.objects.conf("server_bans")
|
||||||
if bans and (any(tup[0] == username for tup in bans if username) or
|
if bans and (any(tup[0] == username for tup in bans if username) or
|
||||||
any(tup[2].match(ip) for tup in bans if ip and tup[2])):
|
any(tup[2].match(ip) for tup in bans if ip and tup[2])):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_username_validators(cls, validator_config=getattr(settings, 'AUTH_USERNAME_VALIDATORS', [])):
|
def get_username_validators(cls, validator_config=getattr(settings, 'AUTH_USERNAME_VALIDATORS', [])):
|
||||||
"""
|
"""
|
||||||
Retrieves and instantiates validators for usernames.
|
Retrieves and instantiates validators for usernames.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
validator_config (list): List of dicts comprising the battery of
|
validator_config (list): List of dicts comprising the battery of
|
||||||
validators to apply to a username.
|
validators to apply to a username.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
validators (list): List of instantiated Validator objects.
|
validators (list): List of instantiated Validator objects.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
objs = []
|
objs = []
|
||||||
for validator in validator_config:
|
for validator in validator_config:
|
||||||
try:
|
try:
|
||||||
|
|
@ -413,49 +413,49 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
raise ImproperlyConfigured(msg % validator['NAME'])
|
raise ImproperlyConfigured(msg % validator['NAME'])
|
||||||
objs.append(klass(**validator.get('OPTIONS', {})))
|
objs.append(klass(**validator.get('OPTIONS', {})))
|
||||||
return objs
|
return objs
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def authenticate(cls, username, password, ip='', **kwargs):
|
def authenticate(cls, username, password, ip='', **kwargs):
|
||||||
"""
|
"""
|
||||||
Checks the given username/password against the database to see if the
|
Checks the given username/password against the database to see if the
|
||||||
credentials are valid.
|
credentials are valid.
|
||||||
|
|
||||||
Note that this simply checks credentials and returns a valid reference
|
Note that this simply checks credentials and returns a valid reference
|
||||||
to the user-- it does not log them in!
|
to the user-- it does not log them in!
|
||||||
|
|
||||||
To finish the job:
|
To finish the job:
|
||||||
After calling this from a Command, associate the account with a Session:
|
After calling this from a Command, associate the account with a Session:
|
||||||
- session.sessionhandler.login(session, account)
|
- session.sessionhandler.login(session, account)
|
||||||
|
|
||||||
...or after calling this from a View, associate it with an HttpRequest:
|
...or after calling this from a View, associate it with an HttpRequest:
|
||||||
- django.contrib.auth.login(account, request)
|
- django.contrib.auth.login(account, request)
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
username (str): Username of account
|
username (str): Username of account
|
||||||
password (str): Password of account
|
password (str): Password of account
|
||||||
ip (str, optional): IP address of client
|
ip (str, optional): IP address of client
|
||||||
|
|
||||||
Kwargs:
|
Kwargs:
|
||||||
session (Session, optional): Session requesting authentication
|
session (Session, optional): Session requesting authentication
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
account (DefaultAccount, None): Account whose credentials were
|
account (DefaultAccount, None): Account whose credentials were
|
||||||
provided if not banned.
|
provided if not banned.
|
||||||
errors (list): Error messages of any failures.
|
errors (list): Error messages of any failures.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
errors = []
|
errors = []
|
||||||
if ip: ip = str(ip)
|
if ip: ip = str(ip)
|
||||||
|
|
||||||
# See if authentication is currently being throttled
|
# See if authentication is currently being throttled
|
||||||
if ip and LOGIN_THROTTLE.check(ip):
|
if ip and LOGIN_THROTTLE.check(ip):
|
||||||
errors.append('Too many login failures; please try again in a few minutes.')
|
errors.append('Too many login failures; please try again in a few minutes.')
|
||||||
|
|
||||||
# With throttle active, do not log continued hits-- it is a
|
# With throttle active, do not log continued hits-- it is a
|
||||||
# waste of storage and can be abused to make your logs harder to
|
# waste of storage and can be abused to make your logs harder to
|
||||||
# read and/or fill up your disk.
|
# read and/or fill up your disk.
|
||||||
return None, errors
|
return None, errors
|
||||||
|
|
||||||
# Check IP and/or name bans
|
# Check IP and/or name bans
|
||||||
banned = cls.is_banned(username=username, ip=ip)
|
banned = cls.is_banned(username=username, ip=ip)
|
||||||
if banned:
|
if banned:
|
||||||
|
|
@ -465,19 +465,19 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
logger.log_sec('Authentication Denied (Banned): %s (IP: %s).' % (username, ip))
|
logger.log_sec('Authentication Denied (Banned): %s (IP: %s).' % (username, ip))
|
||||||
LOGIN_THROTTLE.update(ip, 'Too many sightings of banned artifact.')
|
LOGIN_THROTTLE.update(ip, 'Too many sightings of banned artifact.')
|
||||||
return None, errors
|
return None, errors
|
||||||
|
|
||||||
# Authenticate and get Account object
|
# Authenticate and get Account object
|
||||||
account = authenticate(username=username, password=password)
|
account = authenticate(username=username, password=password)
|
||||||
if not account:
|
if not account:
|
||||||
# User-facing message
|
# User-facing message
|
||||||
errors.append('Username and/or password is incorrect.')
|
errors.append('Username and/or password is incorrect.')
|
||||||
|
|
||||||
# Log auth failures while throttle is inactive
|
# Log auth failures while throttle is inactive
|
||||||
logger.log_sec('Authentication Failure: %s (IP: %s).' % (username, ip))
|
logger.log_sec('Authentication Failure: %s (IP: %s).' % (username, ip))
|
||||||
|
|
||||||
# Update throttle
|
# Update throttle
|
||||||
if ip: LOGIN_THROTTLE.update(ip, 'Too many authentication failures.')
|
if ip: LOGIN_THROTTLE.update(ip, 'Too many authentication failures.')
|
||||||
|
|
||||||
# Try to call post-failure hook
|
# Try to call post-failure hook
|
||||||
session = kwargs.get('session', None)
|
session = kwargs.get('session', None)
|
||||||
if session:
|
if session:
|
||||||
|
|
@ -486,49 +486,49 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
account.at_failed_login(session)
|
account.at_failed_login(session)
|
||||||
|
|
||||||
return None, errors
|
return None, errors
|
||||||
|
|
||||||
# Account successfully authenticated
|
# Account successfully authenticated
|
||||||
logger.log_sec('Authentication Success: %s (IP: %s).' % (account, ip))
|
logger.log_sec('Authentication Success: %s (IP: %s).' % (account, ip))
|
||||||
return account, errors
|
return account, errors
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def normalize_username(cls, username):
|
def normalize_username(cls, username):
|
||||||
"""
|
"""
|
||||||
Django: Applies NFKC Unicode normalization to usernames so that visually
|
Django: Applies NFKC Unicode normalization to usernames so that visually
|
||||||
identical characters with different Unicode code points are considered
|
identical characters with different Unicode code points are considered
|
||||||
identical.
|
identical.
|
||||||
|
|
||||||
(This deals with the Turkish "i" problem and similar
|
(This deals with the Turkish "i" problem and similar
|
||||||
annoyances. Only relevant if you go out of your way to allow Unicode
|
annoyances. Only relevant if you go out of your way to allow Unicode
|
||||||
usernames though-- Evennia accepts ASCII by default.)
|
usernames though-- Evennia accepts ASCII by default.)
|
||||||
|
|
||||||
In this case we're simply piggybacking on this feature to apply
|
In this case we're simply piggybacking on this feature to apply
|
||||||
additional normalization per Evennia's standards.
|
additional normalization per Evennia's standards.
|
||||||
"""
|
"""
|
||||||
username = super(DefaultAccount, cls).normalize_username(username)
|
username = super(DefaultAccount, cls).normalize_username(username)
|
||||||
|
|
||||||
# strip excessive spaces in accountname
|
# strip excessive spaces in accountname
|
||||||
username = re.sub(r"\s+", " ", username).strip()
|
username = re.sub(r"\s+", " ", username).strip()
|
||||||
|
|
||||||
return username
|
return username
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_username(cls, username):
|
def validate_username(cls, username):
|
||||||
"""
|
"""
|
||||||
Checks the given username against the username validator associated with
|
Checks the given username against the username validator associated with
|
||||||
Account objects, and also checks the database to make sure it is unique.
|
Account objects, and also checks the database to make sure it is unique.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
username (str): Username to validate
|
username (str): Username to validate
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
valid (bool): Whether or not the password passed validation
|
valid (bool): Whether or not the password passed validation
|
||||||
errors (list): Error messages of any failures
|
errors (list): Error messages of any failures
|
||||||
|
|
||||||
"""
|
"""
|
||||||
valid = []
|
valid = []
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
# Make sure we're at least using the default validator
|
# Make sure we're at least using the default validator
|
||||||
validators = cls.get_username_validators()
|
validators = cls.get_username_validators()
|
||||||
if not validators:
|
if not validators:
|
||||||
|
|
@ -541,14 +541,14 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
valid.append(False)
|
valid.append(False)
|
||||||
errors.extend(e.messages)
|
errors.extend(e.messages)
|
||||||
|
|
||||||
# Disqualify if any check failed
|
# Disqualify if any check failed
|
||||||
if False in valid:
|
if False in valid:
|
||||||
valid = False
|
valid = False
|
||||||
else: valid = True
|
else: valid = True
|
||||||
|
|
||||||
return valid, errors
|
return valid, errors
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_password(cls, password, account=None):
|
def validate_password(cls, password, account=None):
|
||||||
"""
|
"""
|
||||||
|
|
@ -608,48 +608,48 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
super(DefaultAccount, self).set_password(password)
|
super(DefaultAccount, self).set_password(password)
|
||||||
logger.log_sec("Password successfully changed for %s." % self)
|
logger.log_sec("Password successfully changed for %s." % self)
|
||||||
self.at_password_change()
|
self.at_password_change()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, *args, **kwargs):
|
def create(cls, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Creates an Account (or Account/Character pair for MULTISESSION_MODE<2)
|
Creates an Account (or Account/Character pair for MULTISESSION_MODE<2)
|
||||||
with default (or overridden) permissions and having joined them to the
|
with default (or overridden) permissions and having joined them to the
|
||||||
appropriate default channels.
|
appropriate default channels.
|
||||||
|
|
||||||
Kwargs:
|
Kwargs:
|
||||||
username (str): Username of Account owner
|
username (str): Username of Account owner
|
||||||
password (str): Password of Account owner
|
password (str): Password of Account owner
|
||||||
email (str, optional): Email address of Account owner
|
email (str, optional): Email address of Account owner
|
||||||
ip (str, optional): IP address of requesting connection
|
ip (str, optional): IP address of requesting connection
|
||||||
guest (bool, optional): Whether or not this is to be a Guest account
|
guest (bool, optional): Whether or not this is to be a Guest account
|
||||||
|
|
||||||
permissions (str, optional): Default permissions for the Account
|
permissions (str, optional): Default permissions for the Account
|
||||||
typeclass (str, optional): Typeclass to use for new Account
|
typeclass (str, optional): Typeclass to use for new Account
|
||||||
character_typeclass (str, optional): Typeclass to use for new char
|
character_typeclass (str, optional): Typeclass to use for new char
|
||||||
when applicable.
|
when applicable.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
account (Account): Account if successfully created; None if not
|
account (Account): Account if successfully created; None if not
|
||||||
errors (list): List of error messages in string form
|
errors (list): List of error messages in string form
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
account = None
|
account = None
|
||||||
errors = []
|
errors = []
|
||||||
|
|
||||||
username = kwargs.get('username')
|
username = kwargs.get('username')
|
||||||
password = kwargs.get('password')
|
password = kwargs.get('password')
|
||||||
email = kwargs.get('email', '').strip()
|
email = kwargs.get('email', '').strip()
|
||||||
guest = kwargs.get('guest', False)
|
guest = kwargs.get('guest', False)
|
||||||
|
|
||||||
permissions = kwargs.get('permissions', settings.PERMISSION_ACCOUNT_DEFAULT)
|
permissions = kwargs.get('permissions', settings.PERMISSION_ACCOUNT_DEFAULT)
|
||||||
typeclass = kwargs.get('typeclass', settings.BASE_ACCOUNT_TYPECLASS)
|
typeclass = kwargs.get('typeclass', settings.BASE_ACCOUNT_TYPECLASS)
|
||||||
|
|
||||||
ip = kwargs.get('ip', '')
|
ip = kwargs.get('ip', '')
|
||||||
if ip and CREATION_THROTTLE.check(ip):
|
if ip and CREATION_THROTTLE.check(ip):
|
||||||
errors.append("You are creating too many accounts. Please log into an existing account.")
|
errors.append("You are creating too many accounts. Please log into an existing account.")
|
||||||
return None, errors
|
return None, errors
|
||||||
|
|
||||||
# Normalize username
|
# Normalize username
|
||||||
username = cls.normalize_username(username)
|
username = cls.normalize_username(username)
|
||||||
|
|
||||||
|
|
@ -678,50 +678,50 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
"\nIf you feel this ban is in error, please email an admin.|x"
|
"\nIf you feel this ban is in error, please email an admin.|x"
|
||||||
errors.append(string)
|
errors.append(string)
|
||||||
return None, errors
|
return None, errors
|
||||||
|
|
||||||
# everything's ok. Create the new account account.
|
# everything's ok. Create the new account account.
|
||||||
try:
|
try:
|
||||||
try:
|
try:
|
||||||
account = create.create_account(username, email, password, permissions=permissions, typeclass=typeclass)
|
account = create.create_account(username, email, password, permissions=permissions, typeclass=typeclass)
|
||||||
logger.log_sec('Account Created: %s (IP: %s).' % (account, ip))
|
logger.log_sec('Account Created: %s (IP: %s).' % (account, ip))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append("There was an error creating the Account. If this problem persists, contact an admin.")
|
errors.append("There was an error creating the Account. If this problem persists, contact an admin.")
|
||||||
logger.log_trace()
|
logger.log_trace()
|
||||||
return None, errors
|
return None, errors
|
||||||
|
|
||||||
# This needs to be set so the engine knows this account is
|
# This needs to be set so the engine knows this account is
|
||||||
# logging in for the first time. (so it knows to call the right
|
# logging in for the first time. (so it knows to call the right
|
||||||
# hooks during login later)
|
# hooks during login later)
|
||||||
account.db.FIRST_LOGIN = True
|
account.db.FIRST_LOGIN = True
|
||||||
|
|
||||||
# Record IP address of creation, if available
|
# Record IP address of creation, if available
|
||||||
if ip: account.db.creator_ip = ip
|
if ip: account.db.creator_ip = ip
|
||||||
|
|
||||||
# join the new account to the public channel
|
# join the new account to the public channel
|
||||||
pchannel = ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"])
|
pchannel = ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"])
|
||||||
if not pchannel or not pchannel.connect(account):
|
if not pchannel or not pchannel.connect(account):
|
||||||
string = "New account '%s' could not connect to public channel!" % account.key
|
string = "New account '%s' could not connect to public channel!" % account.key
|
||||||
errors.append(string)
|
errors.append(string)
|
||||||
logger.log_err(string)
|
logger.log_err(string)
|
||||||
|
|
||||||
if account and settings.MULTISESSION_MODE < 2:
|
if account and settings.MULTISESSION_MODE < 2:
|
||||||
# Load the appropriate Character class
|
# Load the appropriate Character class
|
||||||
character_typeclass = kwargs.get('character_typeclass', settings.BASE_CHARACTER_TYPECLASS)
|
character_typeclass = kwargs.get('character_typeclass', settings.BASE_CHARACTER_TYPECLASS)
|
||||||
character_home = kwargs.get('home')
|
character_home = kwargs.get('home')
|
||||||
Character = class_from_module(character_typeclass)
|
Character = class_from_module(character_typeclass)
|
||||||
|
|
||||||
# Create the character
|
# Create the character
|
||||||
character, errs = Character.create(
|
character, errs = Character.create(
|
||||||
account.key, account, ip=ip, typeclass=character_typeclass,
|
account.key, account, ip=ip, typeclass=character_typeclass,
|
||||||
permissions=permissions, home=character_home
|
permissions=permissions, home=character_home
|
||||||
)
|
)
|
||||||
errors.extend(errs)
|
errors.extend(errs)
|
||||||
|
|
||||||
if character:
|
if character:
|
||||||
# Update playable character list
|
# Update playable character list
|
||||||
account.db._playable_characters.append(character)
|
account.db._playable_characters.append(character)
|
||||||
|
|
||||||
# We need to set this to have @ic auto-connect to this character
|
# We need to set this to have @ic auto-connect to this character
|
||||||
account.db._last_puppet = character
|
account.db._last_puppet = character
|
||||||
|
|
||||||
|
|
@ -731,7 +731,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
# we won't see any errors at all.
|
# we won't see any errors at all.
|
||||||
errors.append("An error occurred. Please e-mail an admin if the problem persists.")
|
errors.append("An error occurred. Please e-mail an admin if the problem persists.")
|
||||||
logger.log_trace()
|
logger.log_trace()
|
||||||
|
|
||||||
# Update the throttle to indicate a new account was created from this IP
|
# Update the throttle to indicate a new account was created from this IP
|
||||||
if ip and not guest: CREATION_THROTTLE.update(ip, 'Too many accounts being created.')
|
if ip and not guest: CREATION_THROTTLE.update(ip, 'Too many accounts being created.')
|
||||||
return account, errors
|
return account, errors
|
||||||
|
|
@ -1384,7 +1384,7 @@ class DefaultGuest(DefaultAccount):
|
||||||
This class is used for guest logins. Unlike Accounts, Guests and
|
This class is used for guest logins. Unlike Accounts, Guests and
|
||||||
their characters are deleted after disconnection.
|
their characters are deleted after disconnection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, **kwargs):
|
def create(cls, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1392,31 +1392,31 @@ class DefaultGuest(DefaultAccount):
|
||||||
if one is available for use.
|
if one is available for use.
|
||||||
"""
|
"""
|
||||||
return cls.authenticate(**kwargs)
|
return cls.authenticate(**kwargs)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def authenticate(cls, **kwargs):
|
def authenticate(cls, **kwargs):
|
||||||
"""
|
"""
|
||||||
Gets or creates a Guest account object.
|
Gets or creates a Guest account object.
|
||||||
|
|
||||||
Kwargs:
|
Kwargs:
|
||||||
ip (str, optional): IP address of requestor; used for ban checking,
|
ip (str, optional): IP address of requestor; used for ban checking,
|
||||||
throttling and logging
|
throttling and logging
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
account (Object): Guest account object, if available
|
account (Object): Guest account object, if available
|
||||||
errors (list): List of error messages accrued during this request.
|
errors (list): List of error messages accrued during this request.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
errors = []
|
errors = []
|
||||||
account = None
|
account = None
|
||||||
username = None
|
username = None
|
||||||
ip = kwargs.get('ip', '').strip()
|
ip = kwargs.get('ip', '').strip()
|
||||||
|
|
||||||
# check if guests are enabled.
|
# check if guests are enabled.
|
||||||
if not settings.GUEST_ENABLED:
|
if not settings.GUEST_ENABLED:
|
||||||
errors.append('Guest accounts are not enabled on this server.')
|
errors.append('Guest accounts are not enabled on this server.')
|
||||||
return None, errors
|
return None, errors
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Find an available guest name.
|
# Find an available guest name.
|
||||||
for name in settings.GUEST_LIST:
|
for name in settings.GUEST_LIST:
|
||||||
|
|
@ -1433,20 +1433,20 @@ class DefaultGuest(DefaultAccount):
|
||||||
home = settings.GUEST_HOME
|
home = settings.GUEST_HOME
|
||||||
permissions = settings.PERMISSION_GUEST_DEFAULT
|
permissions = settings.PERMISSION_GUEST_DEFAULT
|
||||||
typeclass = settings.BASE_GUEST_TYPECLASS
|
typeclass = settings.BASE_GUEST_TYPECLASS
|
||||||
|
|
||||||
# Call parent class creator
|
# Call parent class creator
|
||||||
account, errs = super(DefaultGuest, cls).create(
|
account, errs = super(DefaultGuest, cls).create(
|
||||||
guest=True,
|
guest=True,
|
||||||
username=username,
|
username=username,
|
||||||
password=password,
|
password=password,
|
||||||
permissions=permissions,
|
permissions=permissions,
|
||||||
typeclass=typeclass,
|
typeclass=typeclass,
|
||||||
home=home,
|
home=home,
|
||||||
ip=ip,
|
ip=ip,
|
||||||
)
|
)
|
||||||
errors.extend(errs)
|
errors.extend(errs)
|
||||||
return account, errors
|
return account, errors
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# We are in the middle between logged in and -not, so we have
|
# We are in the middle between logged in and -not, so we have
|
||||||
# to handle tracebacks ourselves at this point. If we don't,
|
# to handle tracebacks ourselves at this point. If we don't,
|
||||||
|
|
@ -1454,7 +1454,7 @@ class DefaultGuest(DefaultAccount):
|
||||||
errors.append("An error occurred. Please e-mail an admin if the problem persists.")
|
errors.append("An error occurred. Please e-mail an admin if the problem persists.")
|
||||||
logger.log_trace()
|
logger.log_trace()
|
||||||
return None, errors
|
return None, errors
|
||||||
|
|
||||||
return account, errors
|
return account, errors
|
||||||
|
|
||||||
def at_post_login(self, session=None, **kwargs):
|
def at_post_login(self, session=None, **kwargs):
|
||||||
|
|
|
||||||
|
|
@ -7,10 +7,8 @@ from unittest import TestCase
|
||||||
from django.test import override_settings
|
from django.test import override_settings
|
||||||
from evennia.accounts.accounts import AccountSessionHandler
|
from evennia.accounts.accounts import AccountSessionHandler
|
||||||
from evennia.accounts.accounts import DefaultAccount, DefaultGuest
|
from evennia.accounts.accounts import DefaultAccount, DefaultGuest
|
||||||
from evennia.server.session import Session
|
|
||||||
from evennia.utils.test_resources import EvenniaTest
|
from evennia.utils.test_resources import EvenniaTest
|
||||||
from evennia.utils import create
|
from evennia.utils import create
|
||||||
from evennia.utils.test_resources import EvenniaTest
|
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
|
|
@ -61,78 +59,78 @@ class TestAccountSessionHandler(TestCase):
|
||||||
def test_count(self):
|
def test_count(self):
|
||||||
"Check count method"
|
"Check count method"
|
||||||
self.assertEqual(self.handler.count(), len(self.handler.get()))
|
self.assertEqual(self.handler.count(), len(self.handler.get()))
|
||||||
|
|
||||||
class TestDefaultGuest(EvenniaTest):
|
class TestDefaultGuest(EvenniaTest):
|
||||||
"Check DefaultGuest class"
|
"Check DefaultGuest class"
|
||||||
|
|
||||||
ip = '212.216.134.22'
|
ip = '212.216.134.22'
|
||||||
|
|
||||||
def test_authenticate(self):
|
def test_authenticate(self):
|
||||||
# Guest account should not be permitted
|
# Guest account should not be permitted
|
||||||
account, errors = DefaultGuest.authenticate(ip=self.ip)
|
account, errors = DefaultGuest.authenticate(ip=self.ip)
|
||||||
self.assertFalse(account, 'Guest account was created despite being disabled.')
|
self.assertFalse(account, 'Guest account was created despite being disabled.')
|
||||||
|
|
||||||
settings.GUEST_ENABLED = True
|
settings.GUEST_ENABLED = True
|
||||||
settings.GUEST_LIST = ['bruce_wayne']
|
settings.GUEST_LIST = ['bruce_wayne']
|
||||||
|
|
||||||
# Create a guest account
|
# Create a guest account
|
||||||
account, errors = DefaultGuest.authenticate(ip=self.ip)
|
account, errors = DefaultGuest.authenticate(ip=self.ip)
|
||||||
self.assertTrue(account, 'Guest account should have been created.')
|
self.assertTrue(account, 'Guest account should have been created.')
|
||||||
|
|
||||||
# Create a second guest account
|
# Create a second guest account
|
||||||
account, errors = DefaultGuest.authenticate(ip=self.ip)
|
account, errors = DefaultGuest.authenticate(ip=self.ip)
|
||||||
self.assertFalse(account, 'Two guest accounts were created with a single entry on the guest list!')
|
self.assertFalse(account, 'Two guest accounts were created with a single entry on the guest list!')
|
||||||
|
|
||||||
settings.GUEST_ENABLED = False
|
settings.GUEST_ENABLED = False
|
||||||
|
|
||||||
class TestDefaultAccountAuth(EvenniaTest):
|
class TestDefaultAccountAuth(EvenniaTest):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
super(TestDefaultAccountAuth, self).setUp()
|
super(TestDefaultAccountAuth, self).setUp()
|
||||||
|
|
||||||
self.password = "testpassword"
|
self.password = "testpassword"
|
||||||
self.account.delete()
|
self.account.delete()
|
||||||
self.account = create.create_account("TestAccount%s" % randint(100000, 999999), email="test@test.com", password=self.password, typeclass=DefaultAccount)
|
self.account = create.create_account("TestAccount%s" % randint(100000, 999999), email="test@test.com", password=self.password, typeclass=DefaultAccount)
|
||||||
|
|
||||||
def test_authentication(self):
|
def test_authentication(self):
|
||||||
"Confirm Account authentication method is authenticating/denying users."
|
"Confirm Account authentication method is authenticating/denying users."
|
||||||
# Valid credentials
|
# Valid credentials
|
||||||
obj, errors = DefaultAccount.authenticate(self.account.name, self.password)
|
obj, errors = DefaultAccount.authenticate(self.account.name, self.password)
|
||||||
self.assertTrue(obj, 'Account did not authenticate given valid credentials.')
|
self.assertTrue(obj, 'Account did not authenticate given valid credentials.')
|
||||||
|
|
||||||
# Invalid credentials
|
# Invalid credentials
|
||||||
obj, errors = DefaultAccount.authenticate(self.account.name, 'xyzzy')
|
obj, errors = DefaultAccount.authenticate(self.account.name, 'xyzzy')
|
||||||
self.assertFalse(obj, 'Account authenticated using invalid credentials.')
|
self.assertFalse(obj, 'Account authenticated using invalid credentials.')
|
||||||
|
|
||||||
def test_create(self):
|
def test_create(self):
|
||||||
"Confirm Account creation is working as expected."
|
"Confirm Account creation is working as expected."
|
||||||
# Create a normal account
|
# Create a normal account
|
||||||
account, errors = DefaultAccount.create(username='ziggy', password='stardust11')
|
account, errors = DefaultAccount.create(username='ziggy', password='stardust11')
|
||||||
self.assertTrue(account, 'New account should have been created.')
|
self.assertTrue(account, 'New account should have been created.')
|
||||||
|
|
||||||
# Try creating a duplicate account
|
# Try creating a duplicate account
|
||||||
account2, errors = DefaultAccount.create(username='Ziggy', password='starman11')
|
account2, errors = DefaultAccount.create(username='Ziggy', password='starman11')
|
||||||
self.assertFalse(account2, 'Duplicate account name should not have been allowed.')
|
self.assertFalse(account2, 'Duplicate account name should not have been allowed.')
|
||||||
account.delete()
|
account.delete()
|
||||||
|
|
||||||
def test_throttle(self):
|
def test_throttle(self):
|
||||||
"Confirm throttle activates on too many failures."
|
"Confirm throttle activates on too many failures."
|
||||||
for x in xrange(20):
|
for x in xrange(20):
|
||||||
obj, errors = DefaultAccount.authenticate(self.account.name, 'xyzzy', ip='12.24.36.48')
|
obj, errors = DefaultAccount.authenticate(self.account.name, 'xyzzy', ip='12.24.36.48')
|
||||||
self.assertFalse(obj, 'Authentication was provided a bogus password; this should NOT have returned an account!')
|
self.assertFalse(obj, 'Authentication was provided a bogus password; this should NOT have returned an account!')
|
||||||
|
|
||||||
self.assertTrue('too many login failures' in errors[-1].lower(), 'Failed logins should have been throttled.')
|
self.assertTrue('too many login failures' in errors[-1].lower(), 'Failed logins should have been throttled.')
|
||||||
|
|
||||||
def test_username_validation(self):
|
def test_username_validation(self):
|
||||||
"Check username validators deny relevant usernames"
|
"Check username validators deny relevant usernames"
|
||||||
# Should not accept Unicode by default, lest users pick names like this
|
# Should not accept Unicode by default, lest users pick names like this
|
||||||
result, error = DefaultAccount.validate_username('¯\_(ツ)_/¯')
|
result, error = DefaultAccount.validate_username('¯\_(ツ)_/¯')
|
||||||
self.assertFalse(result, "Validator allowed kanji in username.")
|
self.assertFalse(result, "Validator allowed kanji in username.")
|
||||||
|
|
||||||
# Should not allow duplicate username
|
# Should not allow duplicate username
|
||||||
result, error = DefaultAccount.validate_username(self.account.name)
|
result, error = DefaultAccount.validate_username(self.account.name)
|
||||||
self.assertFalse(result, "Duplicate username should not have passed validation.")
|
self.assertFalse(result, "Duplicate username should not have passed validation.")
|
||||||
|
|
||||||
# Should not allow username too short
|
# Should not allow username too short
|
||||||
result, error = DefaultAccount.validate_username('xx')
|
result, error = DefaultAccount.validate_username('xx')
|
||||||
self.assertFalse(result, "2-character username passed validation.")
|
self.assertFalse(result, "2-character username passed validation.")
|
||||||
|
|
@ -277,17 +275,17 @@ class TestDefaultAccount(TestCase):
|
||||||
|
|
||||||
|
|
||||||
class TestAccountPuppetDeletion(EvenniaTest):
|
class TestAccountPuppetDeletion(EvenniaTest):
|
||||||
|
|
||||||
@override_settings(MULTISESSION_MODE=2)
|
@override_settings(MULTISESSION_MODE=2)
|
||||||
def test_puppet_deletion(self):
|
def test_puppet_deletion(self):
|
||||||
# Check for existing chars
|
# Check for existing chars
|
||||||
self.assertFalse(self.account.db._playable_characters, 'Account should not have any chars by default.')
|
self.assertFalse(self.account.db._playable_characters, 'Account should not have any chars by default.')
|
||||||
|
|
||||||
# Add char1 to account's playable characters
|
# Add char1 to account's playable characters
|
||||||
self.account.db._playable_characters.append(self.char1)
|
self.account.db._playable_characters.append(self.char1)
|
||||||
self.assertTrue(self.account.db._playable_characters, 'Char was not added to account.')
|
self.assertTrue(self.account.db._playable_characters, 'Char was not added to account.')
|
||||||
|
|
||||||
# See what happens when we delete char1.
|
# See what happens when we delete char1.
|
||||||
self.char1.delete()
|
self.char1.delete()
|
||||||
# Playable char list should be empty.
|
# Playable char list should be empty.
|
||||||
self.assertFalse(self.account.db._playable_characters, 'Playable character list is not empty! %s' % self.account.db._playable_characters)
|
self.assertFalse(self.account.db._playable_characters, 'Playable character list is not empty! %s' % self.account.db._playable_characters)
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,11 @@
|
||||||
Commands that are available from the connect screen.
|
Commands that are available from the connect screen.
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
import time
|
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import authenticate
|
|
||||||
from evennia.accounts.models import AccountDB
|
|
||||||
from evennia.objects.models import ObjectDB
|
|
||||||
from evennia.comms.models import ChannelDB
|
from evennia.comms.models import ChannelDB
|
||||||
from evennia.server.models import ServerConfig
|
|
||||||
from evennia.server.sessionhandler import SESSIONS
|
from evennia.server.sessionhandler import SESSIONS
|
||||||
from evennia.server.throttle import Throttle
|
|
||||||
|
|
||||||
from evennia.utils import class_from_module, create, logger, utils, gametime
|
from evennia.utils import class_from_module, create, logger, utils, gametime
|
||||||
from evennia.commands.cmdhandler import CMD_LOGINSTART
|
from evennia.commands.cmdhandler import CMD_LOGINSTART
|
||||||
|
|
@ -26,6 +20,7 @@ __all__ = ("CmdUnconnectedConnect", "CmdUnconnectedCreate",
|
||||||
MULTISESSION_MODE = settings.MULTISESSION_MODE
|
MULTISESSION_MODE = settings.MULTISESSION_MODE
|
||||||
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
|
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
|
||||||
|
|
||||||
|
|
||||||
def create_guest_account(session):
|
def create_guest_account(session):
|
||||||
"""
|
"""
|
||||||
Creates a guest account/character for this session, if one is available.
|
Creates a guest account/character for this session, if one is available.
|
||||||
|
|
@ -40,10 +35,10 @@ def create_guest_account(session):
|
||||||
"""
|
"""
|
||||||
enabled = settings.GUEST_ENABLED
|
enabled = settings.GUEST_ENABLED
|
||||||
address = session.address
|
address = session.address
|
||||||
|
|
||||||
# Get account class
|
# Get account class
|
||||||
Guest = class_from_module(settings.BASE_GUEST_TYPECLASS)
|
Guest = class_from_module(settings.BASE_GUEST_TYPECLASS)
|
||||||
|
|
||||||
# Get an available guest account
|
# Get an available guest account
|
||||||
# authenticate() handles its own throttling
|
# authenticate() handles its own throttling
|
||||||
account, errors = Guest.authenticate(ip=address)
|
account, errors = Guest.authenticate(ip=address)
|
||||||
|
|
@ -53,6 +48,7 @@ def create_guest_account(session):
|
||||||
session.msg("|R%s|n" % '\n'.join(errors))
|
session.msg("|R%s|n" % '\n'.join(errors))
|
||||||
return enabled, None
|
return enabled, None
|
||||||
|
|
||||||
|
|
||||||
def create_normal_account(session, name, password):
|
def create_normal_account(session, name, password):
|
||||||
"""
|
"""
|
||||||
Creates an account with the given name and password.
|
Creates an account with the given name and password.
|
||||||
|
|
@ -67,9 +63,9 @@ def create_normal_account(session, name, password):
|
||||||
"""
|
"""
|
||||||
# Get account class
|
# Get account class
|
||||||
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
|
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
|
||||||
|
|
||||||
address = session.address
|
address = session.address
|
||||||
|
|
||||||
# Match account name and check password
|
# Match account name and check password
|
||||||
# authenticate() handles all its own throttling
|
# authenticate() handles all its own throttling
|
||||||
account, errors = Account.authenticate(username=name, password=password, ip=address, session=session)
|
account, errors = Account.authenticate(username=name, password=password, ip=address, session=session)
|
||||||
|
|
@ -108,19 +104,19 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
session = self.caller
|
session = self.caller
|
||||||
address = session.address
|
address = session.address
|
||||||
|
|
||||||
args = self.args
|
args = self.args
|
||||||
# extract double quote parts
|
# extract double quote parts
|
||||||
parts = [part.strip() for part in re.split(r"\"", args) if part.strip()]
|
parts = [part.strip() for part in re.split(r"\"", args) if part.strip()]
|
||||||
if len(parts) == 1:
|
if len(parts) == 1:
|
||||||
# this was (hopefully) due to no double quotes being found, or a guest login
|
# this was (hopefully) due to no double quotes being found, or a guest login
|
||||||
parts = parts[0].split(None, 1)
|
parts = parts[0].split(None, 1)
|
||||||
|
|
||||||
# Guest login
|
# Guest login
|
||||||
if len(parts) == 1 and parts[0].lower() == "guest":
|
if len(parts) == 1 and parts[0].lower() == "guest":
|
||||||
# Get Guest typeclass
|
# Get Guest typeclass
|
||||||
Guest = class_from_module(settings.BASE_GUEST_TYPECLASS)
|
Guest = class_from_module(settings.BASE_GUEST_TYPECLASS)
|
||||||
|
|
||||||
account, errors = Guest.authenticate(ip=address)
|
account, errors = Guest.authenticate(ip=address)
|
||||||
if account:
|
if account:
|
||||||
session.sessionhandler.login(session, account)
|
session.sessionhandler.login(session, account)
|
||||||
|
|
@ -128,14 +124,14 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS):
|
||||||
else:
|
else:
|
||||||
session.msg("|R%s|n" % '\n'.join(errors))
|
session.msg("|R%s|n" % '\n'.join(errors))
|
||||||
return
|
return
|
||||||
|
|
||||||
if len(parts) != 2:
|
if len(parts) != 2:
|
||||||
session.msg("\n\r Usage (without <>): connect <name> <password>")
|
session.msg("\n\r Usage (without <>): connect <name> <password>")
|
||||||
return
|
return
|
||||||
|
|
||||||
# Get account class
|
# Get account class
|
||||||
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
|
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
|
||||||
|
|
||||||
name, password = parts
|
name, password = parts
|
||||||
account, errors = Account.authenticate(username=name, password=password, ip=address, session=session)
|
account, errors = Account.authenticate(username=name, password=password, ip=address, session=session)
|
||||||
if account:
|
if account:
|
||||||
|
|
@ -168,7 +164,7 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
|
||||||
args = self.args.strip()
|
args = self.args.strip()
|
||||||
|
|
||||||
address = session.address
|
address = session.address
|
||||||
|
|
||||||
# Get account class
|
# Get account class
|
||||||
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
|
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
|
||||||
|
|
||||||
|
|
@ -182,7 +178,7 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
|
||||||
"\nIf <name> or <password> contains spaces, enclose it in double quotes."
|
"\nIf <name> or <password> contains spaces, enclose it in double quotes."
|
||||||
session.msg(string)
|
session.msg(string)
|
||||||
return
|
return
|
||||||
|
|
||||||
username, password = parts
|
username, password = parts
|
||||||
|
|
||||||
# everything's ok. Create the new account account.
|
# everything's ok. Create the new account account.
|
||||||
|
|
|
||||||
|
|
@ -195,7 +195,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
# lockstring of newly created objects, for easy overloading.
|
# lockstring of newly created objects, for easy overloading.
|
||||||
# Will be formatted with the appropriate attributes.
|
# Will be formatted with the appropriate attributes.
|
||||||
lockstring = "control:id({account_id}) or perm(Admin);delete:id({account_id}) or perm(Admin)"
|
lockstring = "control:id({account_id}) or perm(Admin);delete:id({account_id}) or perm(Admin)"
|
||||||
|
|
||||||
objects = ObjectManager()
|
objects = ObjectManager()
|
||||||
|
|
||||||
# on-object properties
|
# on-object properties
|
||||||
|
|
@ -863,66 +863,66 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
string = "This place should not exist ... contact an admin."
|
string = "This place should not exist ... contact an admin."
|
||||||
obj.msg(_(string))
|
obj.msg(_(string))
|
||||||
obj.move_to(home)
|
obj.move_to(home)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, key, account=None, **kwargs):
|
def create(cls, key, account=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Creates a basic object with default parameters, unless otherwise
|
Creates a basic object with default parameters, unless otherwise
|
||||||
specified or extended.
|
specified or extended.
|
||||||
|
|
||||||
Provides a friendlier interface to the utils.create_object() function.
|
Provides a friendlier interface to the utils.create_object() function.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key (str): Name of the new object.
|
key (str): Name of the new object.
|
||||||
account (Account): Account to attribute this object to.
|
account (Account): Account to attribute this object to.
|
||||||
|
|
||||||
Kwargs:
|
Kwargs:
|
||||||
description (str): Brief description for this object.
|
description (str): Brief description for this object.
|
||||||
ip (str): IP address of creator (for object auditing).
|
ip (str): IP address of creator (for object auditing).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
object (Object): A newly created object of the given typeclass.
|
object (Object): A newly created object of the given typeclass.
|
||||||
errors (list): A list of errors in string form, if any.
|
errors (list): A list of errors in string form, if any.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
errors = []
|
errors = []
|
||||||
obj = None
|
obj = None
|
||||||
|
|
||||||
# Get IP address of creator, if available
|
# Get IP address of creator, if available
|
||||||
ip = kwargs.pop('ip', '')
|
ip = kwargs.pop('ip', '')
|
||||||
|
|
||||||
# If no typeclass supplied, use this class
|
# If no typeclass supplied, use this class
|
||||||
kwargs['typeclass'] = kwargs.pop('typeclass', cls)
|
kwargs['typeclass'] = kwargs.pop('typeclass', cls)
|
||||||
|
|
||||||
# Set the supplied key as the name of the intended object
|
# Set the supplied key as the name of the intended object
|
||||||
kwargs['key'] = key
|
kwargs['key'] = key
|
||||||
|
|
||||||
# Get a supplied description, if any
|
# Get a supplied description, if any
|
||||||
description = kwargs.pop('description', '')
|
description = kwargs.pop('description', '')
|
||||||
|
|
||||||
# Create a sane lockstring if one wasn't supplied
|
# Create a sane lockstring if one wasn't supplied
|
||||||
lockstring = kwargs.get('locks')
|
lockstring = kwargs.get('locks')
|
||||||
if account and not lockstring:
|
if account and not lockstring:
|
||||||
lockstring = cls.lockstring.format(account_id=account.id)
|
lockstring = cls.lockstring.format(account_id=account.id)
|
||||||
kwargs['locks'] = lockstring
|
kwargs['locks'] = lockstring
|
||||||
|
|
||||||
# Create object
|
# Create object
|
||||||
try:
|
try:
|
||||||
obj = create.create_object(**kwargs)
|
obj = create.create_object(**kwargs)
|
||||||
|
|
||||||
# Record creator id and creation IP
|
# Record creator id and creation IP
|
||||||
if ip: obj.db.creator_ip = ip
|
if ip: obj.db.creator_ip = ip
|
||||||
if account: obj.db.creator_id = account.id
|
if account: obj.db.creator_id = account.id
|
||||||
|
|
||||||
# Set description if there is none, or update it if provided
|
# Set description if there is none, or update it if provided
|
||||||
if description or not obj.db.desc:
|
if description or not obj.db.desc:
|
||||||
desc = description if description else "You see nothing special."
|
desc = description if description else "You see nothing special."
|
||||||
obj.db.desc = desc
|
obj.db.desc = desc
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append("An error occurred while creating this '%s' object." % key)
|
errors.append("An error occurred while creating this '%s' object." % key)
|
||||||
logger.log_err(e)
|
logger.log_err(e)
|
||||||
|
|
||||||
return obj, errors
|
return obj, errors
|
||||||
|
|
||||||
def copy(self, new_key=None):
|
def copy(self, new_key=None):
|
||||||
|
|
@ -1895,81 +1895,81 @@ class DefaultCharacter(DefaultObject):
|
||||||
# lockstring of newly created rooms, for easy overloading.
|
# lockstring of newly created rooms, for easy overloading.
|
||||||
# Will be formatted with the appropriate attributes.
|
# Will be formatted with the appropriate attributes.
|
||||||
lockstring = "puppet:id({character_id}) or pid({account_id}) or perm(Developer) or pperm(Developer)"
|
lockstring = "puppet:id({character_id}) or pid({account_id}) or perm(Developer) or pperm(Developer)"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, key, account, **kwargs):
|
def create(cls, key, account, **kwargs):
|
||||||
"""
|
"""
|
||||||
Creates a basic Character with default parameters, unless otherwise
|
Creates a basic Character with default parameters, unless otherwise
|
||||||
specified or extended.
|
specified or extended.
|
||||||
|
|
||||||
Provides a friendlier interface to the utils.create_character() function.
|
Provides a friendlier interface to the utils.create_character() function.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key (str): Name of the new Character.
|
key (str): Name of the new Character.
|
||||||
account (obj): Account to associate this Character with. Required as
|
account (obj): Account to associate this Character with. Required as
|
||||||
an argument, but one can fake it out by supplying None-- it will
|
an argument, but one can fake it out by supplying None-- it will
|
||||||
change the default lockset and skip creator attribution.
|
change the default lockset and skip creator attribution.
|
||||||
|
|
||||||
Kwargs:
|
Kwargs:
|
||||||
description (str): Brief description for this object.
|
description (str): Brief description for this object.
|
||||||
ip (str): IP address of creator (for object auditing).
|
ip (str): IP address of creator (for object auditing).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
character (Object): A newly created Character of the given typeclass.
|
character (Object): A newly created Character of the given typeclass.
|
||||||
errors (list): A list of errors in string form, if any.
|
errors (list): A list of errors in string form, if any.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
errors = []
|
errors = []
|
||||||
obj = None
|
obj = None
|
||||||
|
|
||||||
# Get IP address of creator, if available
|
# Get IP address of creator, if available
|
||||||
ip = kwargs.pop('ip', '')
|
ip = kwargs.pop('ip', '')
|
||||||
|
|
||||||
# If no typeclass supplied, use this class
|
# If no typeclass supplied, use this class
|
||||||
kwargs['typeclass'] = kwargs.pop('typeclass', cls)
|
kwargs['typeclass'] = kwargs.pop('typeclass', cls)
|
||||||
|
|
||||||
# Set the supplied key as the name of the intended object
|
# Set the supplied key as the name of the intended object
|
||||||
kwargs['key'] = key
|
kwargs['key'] = key
|
||||||
|
|
||||||
# Get home for character
|
# Get home for character
|
||||||
kwargs['home'] = ObjectDB.objects.get_id(kwargs.get('home', settings.DEFAULT_HOME))
|
kwargs['home'] = ObjectDB.objects.get_id(kwargs.get('home', settings.DEFAULT_HOME))
|
||||||
|
|
||||||
# Get permissions
|
# Get permissions
|
||||||
kwargs['permissions'] = kwargs.get('permissions', settings.PERMISSION_ACCOUNT_DEFAULT)
|
kwargs['permissions'] = kwargs.get('permissions', settings.PERMISSION_ACCOUNT_DEFAULT)
|
||||||
|
|
||||||
# Get description if provided
|
# Get description if provided
|
||||||
description = kwargs.pop('description', '')
|
description = kwargs.pop('description', '')
|
||||||
|
|
||||||
# Get locks if provided
|
# Get locks if provided
|
||||||
locks = kwargs.pop('locks', '')
|
locks = kwargs.pop('locks', '')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Create the Character
|
# Create the Character
|
||||||
obj = create.create_object(**kwargs)
|
obj = create.create_object(**kwargs)
|
||||||
|
|
||||||
# Record creator id and creation IP
|
# Record creator id and creation IP
|
||||||
if ip: obj.db.creator_ip = ip
|
if ip: obj.db.creator_ip = ip
|
||||||
if account: obj.db.creator_id = account.id
|
if account: obj.db.creator_id = account.id
|
||||||
|
|
||||||
# Add locks
|
# Add locks
|
||||||
if not locks and account:
|
if not locks and account:
|
||||||
# Allow only the character itself and the creator account to puppet this character (and Developers).
|
# Allow only the character itself and the creator account to puppet this character (and Developers).
|
||||||
locks = cls.lockstring.format(**{'character_id': obj.id, 'account_id': account.id})
|
locks = cls.lockstring.format(**{'character_id': obj.id, 'account_id': account.id})
|
||||||
elif not locks and not account:
|
elif not locks and not account:
|
||||||
locks = cls.lockstring.format(**{'character_id': obj.id, 'account_id': -1})
|
locks = cls.lockstring.format(**{'character_id': obj.id, 'account_id': -1})
|
||||||
|
|
||||||
obj.locks.add(locks)
|
obj.locks.add(locks)
|
||||||
|
|
||||||
# If no description is set, set a default description
|
# If no description is set, set a default description
|
||||||
if description or not obj.db.desc:
|
if description or not obj.db.desc:
|
||||||
obj.db.desc = description if description else "This is a character."
|
obj.db.desc = description if description else "This is a character."
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append("An error occurred while creating this '%s' object." % key)
|
errors.append("An error occurred while creating this '%s' object." % key)
|
||||||
logger.log_err(e)
|
logger.log_err(e)
|
||||||
|
|
||||||
return obj, errors
|
return obj, errors
|
||||||
|
|
||||||
def basetype_setup(self):
|
def basetype_setup(self):
|
||||||
"""
|
"""
|
||||||
Setup character-specific security.
|
Setup character-specific security.
|
||||||
|
|
@ -2097,60 +2097,60 @@ class DefaultRoom(DefaultObject):
|
||||||
"""
|
"""
|
||||||
Creates a basic Room with default parameters, unless otherwise
|
Creates a basic Room with default parameters, unless otherwise
|
||||||
specified or extended.
|
specified or extended.
|
||||||
|
|
||||||
Provides a friendlier interface to the utils.create_object() function.
|
Provides a friendlier interface to the utils.create_object() function.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key (str): Name of the new Room.
|
key (str): Name of the new Room.
|
||||||
account (obj): Account to associate this Room with.
|
account (obj): Account to associate this Room with.
|
||||||
|
|
||||||
Kwargs:
|
Kwargs:
|
||||||
description (str): Brief description for this object.
|
description (str): Brief description for this object.
|
||||||
ip (str): IP address of creator (for object auditing).
|
ip (str): IP address of creator (for object auditing).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
room (Object): A newly created Room of the given typeclass.
|
room (Object): A newly created Room of the given typeclass.
|
||||||
errors (list): A list of errors in string form, if any.
|
errors (list): A list of errors in string form, if any.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
errors = []
|
errors = []
|
||||||
obj = None
|
obj = None
|
||||||
|
|
||||||
# Get IP address of creator, if available
|
# Get IP address of creator, if available
|
||||||
ip = kwargs.pop('ip', '')
|
ip = kwargs.pop('ip', '')
|
||||||
|
|
||||||
# If no typeclass supplied, use this class
|
# If no typeclass supplied, use this class
|
||||||
kwargs['typeclass'] = kwargs.pop('typeclass', cls)
|
kwargs['typeclass'] = kwargs.pop('typeclass', cls)
|
||||||
|
|
||||||
# Set the supplied key as the name of the intended object
|
# Set the supplied key as the name of the intended object
|
||||||
kwargs['key'] = key
|
kwargs['key'] = key
|
||||||
|
|
||||||
# Get who to send errors to
|
# Get who to send errors to
|
||||||
kwargs['report_to'] = kwargs.pop('report_to', account)
|
kwargs['report_to'] = kwargs.pop('report_to', account)
|
||||||
|
|
||||||
# Get description, if provided
|
# Get description, if provided
|
||||||
description = kwargs.pop('description', '')
|
description = kwargs.pop('description', '')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Create the Room
|
# Create the Room
|
||||||
obj = create.create_object(**kwargs)
|
obj = create.create_object(**kwargs)
|
||||||
|
|
||||||
# Set appropriate locks
|
# Set appropriate locks
|
||||||
lockstring = kwargs.get('locks', cls.lockstring.format(id=account.id))
|
lockstring = kwargs.get('locks', cls.lockstring.format(id=account.id))
|
||||||
obj.locks.add(lockstring)
|
obj.locks.add(lockstring)
|
||||||
|
|
||||||
# Record creator id and creation IP
|
# Record creator id and creation IP
|
||||||
if ip: obj.db.creator_ip = ip
|
if ip: obj.db.creator_ip = ip
|
||||||
if account: obj.db.creator_id = account.id
|
if account: obj.db.creator_id = account.id
|
||||||
|
|
||||||
# If no description is set, set a default description
|
# If no description is set, set a default description
|
||||||
if description or not obj.db.desc:
|
if description or not obj.db.desc:
|
||||||
obj.db.desc = description if description else "This is a room."
|
obj.db.desc = description if description else "This is a room."
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append("An error occurred while creating this '%s' object." % key)
|
errors.append("An error occurred while creating this '%s' object." % key)
|
||||||
logger.log_err(e)
|
logger.log_err(e)
|
||||||
|
|
||||||
return obj, errors
|
return obj, errors
|
||||||
|
|
||||||
def basetype_setup(self):
|
def basetype_setup(self):
|
||||||
|
|
@ -2230,13 +2230,13 @@ class DefaultExit(DefaultObject):
|
||||||
|
|
||||||
exit_command = ExitCommand
|
exit_command = ExitCommand
|
||||||
priority = 101
|
priority = 101
|
||||||
|
|
||||||
# lockstring of newly created exits, for easy overloading.
|
# lockstring of newly created exits, for easy overloading.
|
||||||
# Will be formatted with the {id} of the creating object.
|
# Will be formatted with the {id} of the creating object.
|
||||||
lockstring = "control:id({id}) or perm(Admin); " \
|
lockstring = "control:id({id}) or perm(Admin); " \
|
||||||
"delete:id({id}) or perm(Admin); " \
|
"delete:id({id}) or perm(Admin); " \
|
||||||
"edit:id({id}) or perm(Admin)"
|
"edit:id({id}) or perm(Admin)"
|
||||||
|
|
||||||
# Helper classes and methods to implement the Exit. These need not
|
# Helper classes and methods to implement the Exit. These need not
|
||||||
# be overloaded unless one want to change the foundation for how
|
# be overloaded unless one want to change the foundation for how
|
||||||
# Exits work. See the end of the class for hook methods to overload.
|
# Exits work. See the end of the class for hook methods to overload.
|
||||||
|
|
@ -2274,72 +2274,72 @@ class DefaultExit(DefaultObject):
|
||||||
return exit_cmdset
|
return exit_cmdset
|
||||||
|
|
||||||
# Command hooks
|
# Command hooks
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, key, account, source, dest, **kwargs):
|
def create(cls, key, account, source, dest, **kwargs):
|
||||||
"""
|
"""
|
||||||
Creates a basic Exit with default parameters, unless otherwise
|
Creates a basic Exit with default parameters, unless otherwise
|
||||||
specified or extended.
|
specified or extended.
|
||||||
|
|
||||||
Provides a friendlier interface to the utils.create_object() function.
|
Provides a friendlier interface to the utils.create_object() function.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key (str): Name of the new Exit, as it should appear from the
|
key (str): Name of the new Exit, as it should appear from the
|
||||||
source room.
|
source room.
|
||||||
account (obj): Account to associate this Exit with.
|
account (obj): Account to associate this Exit with.
|
||||||
source (Room): The room to create this exit in.
|
source (Room): The room to create this exit in.
|
||||||
dest (Room): The room to which this exit should go.
|
dest (Room): The room to which this exit should go.
|
||||||
|
|
||||||
Kwargs:
|
Kwargs:
|
||||||
description (str): Brief description for this object.
|
description (str): Brief description for this object.
|
||||||
ip (str): IP address of creator (for object auditing).
|
ip (str): IP address of creator (for object auditing).
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
exit (Object): A newly created Room of the given typeclass.
|
exit (Object): A newly created Room of the given typeclass.
|
||||||
errors (list): A list of errors in string form, if any.
|
errors (list): A list of errors in string form, if any.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
errors = []
|
errors = []
|
||||||
obj = None
|
obj = None
|
||||||
|
|
||||||
# Get IP address of creator, if available
|
# Get IP address of creator, if available
|
||||||
ip = kwargs.pop('ip', '')
|
ip = kwargs.pop('ip', '')
|
||||||
|
|
||||||
# If no typeclass supplied, use this class
|
# If no typeclass supplied, use this class
|
||||||
kwargs['typeclass'] = kwargs.pop('typeclass', cls)
|
kwargs['typeclass'] = kwargs.pop('typeclass', cls)
|
||||||
|
|
||||||
# Set the supplied key as the name of the intended object
|
# Set the supplied key as the name of the intended object
|
||||||
kwargs['key'] = key
|
kwargs['key'] = key
|
||||||
|
|
||||||
# Get who to send errors to
|
# Get who to send errors to
|
||||||
kwargs['report_to'] = kwargs.pop('report_to', account)
|
kwargs['report_to'] = kwargs.pop('report_to', account)
|
||||||
|
|
||||||
# Set to/from rooms
|
# Set to/from rooms
|
||||||
kwargs['location'] = source
|
kwargs['location'] = source
|
||||||
kwargs['destination'] = dest
|
kwargs['destination'] = dest
|
||||||
|
|
||||||
description = kwargs.pop('description', '')
|
description = kwargs.pop('description', '')
|
||||||
|
|
||||||
try:
|
try:
|
||||||
# Create the Exit
|
# Create the Exit
|
||||||
obj = create.create_object(**kwargs)
|
obj = create.create_object(**kwargs)
|
||||||
|
|
||||||
# Set appropriate locks
|
# Set appropriate locks
|
||||||
lockstring = kwargs.get('locks', cls.lockstring.format(id=account.id))
|
lockstring = kwargs.get('locks', cls.lockstring.format(id=account.id))
|
||||||
obj.locks.add(lockstring)
|
obj.locks.add(lockstring)
|
||||||
|
|
||||||
# Record creator id and creation IP
|
# Record creator id and creation IP
|
||||||
if ip: obj.db.creator_ip = ip
|
if ip: obj.db.creator_ip = ip
|
||||||
if account: obj.db.creator_id = account.id
|
if account: obj.db.creator_id = account.id
|
||||||
|
|
||||||
# If no description is set, set a default description
|
# If no description is set, set a default description
|
||||||
if description or not obj.db.desc:
|
if description or not obj.db.desc:
|
||||||
obj.db.desc = description if description else "This is an exit."
|
obj.db.desc = description if description else "This is an exit."
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append("An error occurred while creating this '%s' object." % key)
|
errors.append("An error occurred while creating this '%s' object." % key)
|
||||||
logger.log_err(e)
|
logger.log_err(e)
|
||||||
|
|
||||||
return obj, errors
|
return obj, errors
|
||||||
|
|
||||||
def basetype_setup(self):
|
def basetype_setup(self):
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
from evennia.utils.test_resources import EvenniaTest
|
from evennia.utils.test_resources import EvenniaTest
|
||||||
from evennia import DefaultObject, DefaultCharacter, DefaultRoom, DefaultExit
|
from evennia import DefaultObject, DefaultCharacter, DefaultRoom, DefaultExit
|
||||||
|
|
||||||
|
|
||||||
class DefaultObjectTest(EvenniaTest):
|
class DefaultObjectTest(EvenniaTest):
|
||||||
|
|
||||||
ip = '212.216.139.14'
|
ip = '212.216.139.14'
|
||||||
|
|
||||||
def test_object_create(self):
|
def test_object_create(self):
|
||||||
description = 'A home for a grouch.'
|
description = 'A home for a grouch.'
|
||||||
obj, errors = DefaultObject.create('trashcan', self.account, description=description, ip=self.ip)
|
obj, errors = DefaultObject.create('trashcan', self.account, description=description, ip=self.ip)
|
||||||
|
|
@ -12,7 +13,7 @@ class DefaultObjectTest(EvenniaTest):
|
||||||
self.assertFalse(errors, errors)
|
self.assertFalse(errors, errors)
|
||||||
self.assertEqual(description, obj.db.desc)
|
self.assertEqual(description, obj.db.desc)
|
||||||
self.assertEqual(obj.db.creator_ip, self.ip)
|
self.assertEqual(obj.db.creator_ip, self.ip)
|
||||||
|
|
||||||
def test_character_create(self):
|
def test_character_create(self):
|
||||||
description = 'A furry green monster, reeking of garbage.'
|
description = 'A furry green monster, reeking of garbage.'
|
||||||
obj, errors = DefaultCharacter.create('oscar', self.account, description=description, ip=self.ip)
|
obj, errors = DefaultCharacter.create('oscar', self.account, description=description, ip=self.ip)
|
||||||
|
|
@ -20,7 +21,7 @@ class DefaultObjectTest(EvenniaTest):
|
||||||
self.assertFalse(errors, errors)
|
self.assertFalse(errors, errors)
|
||||||
self.assertEqual(description, obj.db.desc)
|
self.assertEqual(description, obj.db.desc)
|
||||||
self.assertEqual(obj.db.creator_ip, self.ip)
|
self.assertEqual(obj.db.creator_ip, self.ip)
|
||||||
|
|
||||||
def test_room_create(self):
|
def test_room_create(self):
|
||||||
description = 'A dimly-lit alley behind the local Chinese restaurant.'
|
description = 'A dimly-lit alley behind the local Chinese restaurant.'
|
||||||
obj, errors = DefaultRoom.create('alley', self.account, description=description, ip=self.ip)
|
obj, errors = DefaultRoom.create('alley', self.account, description=description, ip=self.ip)
|
||||||
|
|
@ -28,7 +29,7 @@ class DefaultObjectTest(EvenniaTest):
|
||||||
self.assertFalse(errors, errors)
|
self.assertFalse(errors, errors)
|
||||||
self.assertEqual(description, obj.db.desc)
|
self.assertEqual(description, obj.db.desc)
|
||||||
self.assertEqual(obj.db.creator_ip, self.ip)
|
self.assertEqual(obj.db.creator_ip, self.ip)
|
||||||
|
|
||||||
def test_exit_create(self):
|
def test_exit_create(self):
|
||||||
description = 'The steaming depths of the dumpster, ripe with refuse in various states of decomposition.'
|
description = 'The steaming depths of the dumpster, ripe with refuse in various states of decomposition.'
|
||||||
obj, errors = DefaultExit.create('in', self.account, self.room1, self.room2, description=description, ip=self.ip)
|
obj, errors = DefaultExit.create('in', self.account, self.room1, self.room2, description=description, ip=self.ip)
|
||||||
|
|
@ -43,4 +44,4 @@ class DefaultObjectTest(EvenniaTest):
|
||||||
self.assertTrue('admin' in self.char1.web_get_admin_url())
|
self.assertTrue('admin' in self.char1.web_get_admin_url())
|
||||||
|
|
||||||
self.assertTrue(self.room1.get_absolute_url())
|
self.assertTrue(self.room1.get_absolute_url())
|
||||||
self.assertTrue('admin' in self.room1.web_get_admin_url())
|
self.assertTrue('admin' in self.room1.web_get_admin_url())
|
||||||
|
|
|
||||||
|
|
@ -323,31 +323,31 @@ class DefaultScript(ScriptBase):
|
||||||
or describe a state that changes under certain conditions.
|
or describe a state that changes under certain conditions.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create(cls, key, **kwargs):
|
def create(cls, key, **kwargs):
|
||||||
"""
|
"""
|
||||||
Provides a passthrough interface to the utils.create_script() function.
|
Provides a passthrough interface to the utils.create_script() function.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
key (str): Name of the new object.
|
key (str): Name of the new object.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
object (Object): A newly created object of the given typeclass.
|
object (Object): A newly created object of the given typeclass.
|
||||||
errors (list): A list of errors in string form, if any.
|
errors (list): A list of errors in string form, if any.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
errors = []
|
errors = []
|
||||||
obj = None
|
obj = None
|
||||||
|
|
||||||
kwargs['key'] = key
|
kwargs['key'] = key
|
||||||
|
|
||||||
try:
|
try:
|
||||||
obj = create.create_script(**kwargs)
|
obj = create.create_script(**kwargs)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
errors.append("The script '%s' encountered errors and could not be created." % key)
|
errors.append("The script '%s' encountered errors and could not be created." % key)
|
||||||
logger.log_err(e)
|
logger.log_err(e)
|
||||||
|
|
||||||
return obj, errors
|
return obj, errors
|
||||||
|
|
||||||
def at_script_creation(self):
|
def at_script_creation(self):
|
||||||
|
|
|
||||||
|
|
@ -2,25 +2,26 @@ from collections import defaultdict, deque
|
||||||
from evennia.utils import logger
|
from evennia.utils import logger
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
|
||||||
class Throttle(object):
|
class Throttle(object):
|
||||||
"""
|
"""
|
||||||
Keeps a running count of failed actions per IP address.
|
Keeps a running count of failed actions per IP address.
|
||||||
|
|
||||||
Available methods indicate whether or not the number of failures exceeds a
|
Available methods indicate whether or not the number of failures exceeds a
|
||||||
particular threshold.
|
particular threshold.
|
||||||
|
|
||||||
This version of the throttle is usable by both the terminal server as well
|
This version of the throttle is usable by both the terminal server as well
|
||||||
as the web server, imposes limits on memory consumption by using deques
|
as the web server, imposes limits on memory consumption by using deques
|
||||||
with length limits instead of open-ended lists, and removes sparse keys when
|
with length limits instead of open-ended lists, and removes sparse keys when
|
||||||
no recent failures have been recorded.
|
no recent failures have been recorded.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
error_msg = 'Too many failed attempts; you must wait a few minutes before trying again.'
|
error_msg = 'Too many failed attempts; you must wait a few minutes before trying again.'
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
"""
|
"""
|
||||||
Allows setting of throttle parameters.
|
Allows setting of throttle parameters.
|
||||||
|
|
||||||
Kwargs:
|
Kwargs:
|
||||||
limit (int): Max number of failures before imposing limiter
|
limit (int): Max number of failures before imposing limiter
|
||||||
timeout (int): number of timeout seconds after
|
timeout (int): number of timeout seconds after
|
||||||
|
|
@ -32,67 +33,67 @@ class Throttle(object):
|
||||||
self.storage = defaultdict(deque)
|
self.storage = defaultdict(deque)
|
||||||
self.cache_size = self.limit = kwargs.get('limit', 5)
|
self.cache_size = self.limit = kwargs.get('limit', 5)
|
||||||
self.timeout = kwargs.get('timeout', 5 * 60)
|
self.timeout = kwargs.get('timeout', 5 * 60)
|
||||||
|
|
||||||
def get(self, ip=None):
|
def get(self, ip=None):
|
||||||
"""
|
"""
|
||||||
Convenience function that returns the storage table, or part of.
|
Convenience function that returns the storage table, or part of.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ip (str, optional): IP address of requestor
|
ip (str, optional): IP address of requestor
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
storage (dict): When no IP is provided, returns a dict of all
|
storage (dict): When no IP is provided, returns a dict of all
|
||||||
current IPs being tracked and the timestamps of their recent
|
current IPs being tracked and the timestamps of their recent
|
||||||
failures.
|
failures.
|
||||||
timestamps (deque): When an IP is provided, returns a deque of
|
timestamps (deque): When an IP is provided, returns a deque of
|
||||||
timestamps of recent failures only for that IP.
|
timestamps of recent failures only for that IP.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if ip: return self.storage.get(ip, deque(maxlen=self.cache_size))
|
if ip: return self.storage.get(ip, deque(maxlen=self.cache_size))
|
||||||
else: return self.storage
|
else: return self.storage
|
||||||
|
|
||||||
def update(self, ip, failmsg='Exceeded threshold.'):
|
def update(self, ip, failmsg='Exceeded threshold.'):
|
||||||
"""
|
"""
|
||||||
Store the time of the latest failure.
|
Store the time of the latest failure.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ip (str): IP address of requestor
|
ip (str): IP address of requestor
|
||||||
failmsg (str, optional): Message to display in logs upon activation
|
failmsg (str, optional): Message to display in logs upon activation
|
||||||
of throttle.
|
of throttle.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Get current status
|
# Get current status
|
||||||
previously_throttled = self.check(ip)
|
previously_throttled = self.check(ip)
|
||||||
|
|
||||||
# Enforce length limits
|
# Enforce length limits
|
||||||
if not self.storage[ip].maxlen:
|
if not self.storage[ip].maxlen:
|
||||||
self.storage[ip] = deque(maxlen=self.cache_size)
|
self.storage[ip] = deque(maxlen=self.cache_size)
|
||||||
|
|
||||||
self.storage[ip].append(time.time())
|
self.storage[ip].append(time.time())
|
||||||
|
|
||||||
# See if this update caused a change in status
|
# See if this update caused a change in status
|
||||||
currently_throttled = self.check(ip)
|
currently_throttled = self.check(ip)
|
||||||
|
|
||||||
# If this makes it engage, log a single activation event
|
# If this makes it engage, log a single activation event
|
||||||
if (not previously_throttled and currently_throttled):
|
if (not previously_throttled and currently_throttled):
|
||||||
logger.log_sec('Throttle Activated: %s (IP: %s, %i hits in %i seconds.)' % (failmsg, ip, self.limit, self.timeout))
|
logger.log_sec('Throttle Activated: %s (IP: %s, %i hits in %i seconds.)' % (failmsg, ip, self.limit, self.timeout))
|
||||||
|
|
||||||
def check(self, ip):
|
def check(self, ip):
|
||||||
"""
|
"""
|
||||||
This will check the session's address against the
|
This will check the session's address against the
|
||||||
storage dictionary to check they haven't spammed too many
|
storage dictionary to check they haven't spammed too many
|
||||||
fails recently.
|
fails recently.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
ip (str): IP address of requestor
|
ip (str): IP address of requestor
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
throttled (bool): True if throttling is active,
|
throttled (bool): True if throttling is active,
|
||||||
False otherwise.
|
False otherwise.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
now = time.time()
|
now = time.time()
|
||||||
ip = str(ip)
|
ip = str(ip)
|
||||||
|
|
@ -110,5 +111,3 @@ class Throttle(object):
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -4,31 +4,32 @@ from django.utils.translation import gettext as _
|
||||||
from evennia.accounts.models import AccountDB
|
from evennia.accounts.models import AccountDB
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
class EvenniaUsernameAvailabilityValidator:
|
class EvenniaUsernameAvailabilityValidator:
|
||||||
"""
|
"""
|
||||||
Checks to make sure a given username is not taken or otherwise reserved.
|
Checks to make sure a given username is not taken or otherwise reserved.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __call__(self, username):
|
def __call__(self, username):
|
||||||
"""
|
"""
|
||||||
Validates a username to make sure it is not in use or reserved.
|
Validates a username to make sure it is not in use or reserved.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
username (str): Username to validate
|
username (str): Username to validate
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None (None): None if password successfully validated,
|
None (None): None if password successfully validated,
|
||||||
raises ValidationError otherwise.
|
raises ValidationError otherwise.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Check guest list
|
# Check guest list
|
||||||
if (settings.GUEST_LIST and username.lower() in (guest.lower() for guest in settings.GUEST_LIST)):
|
if (settings.GUEST_LIST and username.lower() in (guest.lower() for guest in settings.GUEST_LIST)):
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
_('Sorry, that username is reserved.'),
|
_('Sorry, that username is reserved.'),
|
||||||
code='evennia_username_reserved',
|
code='evennia_username_reserved',
|
||||||
)
|
)
|
||||||
|
|
||||||
# Check database
|
# Check database
|
||||||
exists = AccountDB.objects.filter(username__iexact=username).exists()
|
exists = AccountDB.objects.filter(username__iexact=username).exists()
|
||||||
if exists:
|
if exists:
|
||||||
|
|
@ -37,33 +38,36 @@ class EvenniaUsernameAvailabilityValidator:
|
||||||
code='evennia_username_taken',
|
code='evennia_username_taken',
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
class EvenniaPasswordValidator:
|
class EvenniaPasswordValidator:
|
||||||
|
|
||||||
def __init__(self, regex=r"^[\w. @+\-',]+$", policy="Password should contain a mix of letters, spaces, digits and @/./+/-/_/'/, only."):
|
def __init__(self, regex=r"^[\w. @+\-',]+$",
|
||||||
|
policy="Password should contain a mix of letters, "
|
||||||
|
"spaces, digits and @/./+/-/_/'/, only."):
|
||||||
"""
|
"""
|
||||||
Constructs a standard Django password validator.
|
Constructs a standard Django password validator.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
regex (str): Regex pattern of valid characters to allow.
|
regex (str): Regex pattern of valid characters to allow.
|
||||||
policy (str): Brief explanation of what the defined regex permits.
|
policy (str): Brief explanation of what the defined regex permits.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.regex = regex
|
self.regex = regex
|
||||||
self.policy = policy
|
self.policy = policy
|
||||||
|
|
||||||
def validate(self, password, user=None):
|
def validate(self, password, user=None):
|
||||||
"""
|
"""
|
||||||
Validates a password string to make sure it meets predefined Evennia
|
Validates a password string to make sure it meets predefined Evennia
|
||||||
acceptable character policy.
|
acceptable character policy.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
password (str): Password to validate
|
password (str): Password to validate
|
||||||
user (None): Unused argument but required by Django
|
user (None): Unused argument but required by Django
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None (None): None if password successfully validated,
|
None (None): None if password successfully validated,
|
||||||
raises ValidationError otherwise.
|
raises ValidationError otherwise.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Check complexity
|
# Check complexity
|
||||||
if not re.findall(self.regex, password):
|
if not re.findall(self.regex, password):
|
||||||
|
|
@ -76,11 +80,12 @@ class EvenniaPasswordValidator:
|
||||||
"""
|
"""
|
||||||
Returns a user-facing explanation of the password policy defined
|
Returns a user-facing explanation of the password policy defined
|
||||||
by this validator.
|
by this validator.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
text (str): Explanation of password policy.
|
text (str): Explanation of password policy.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return _(
|
return _(
|
||||||
"%s From a terminal client, you can also use a phrase of multiple words if you enclose the password in double quotes." % self.policy
|
"%s From a terminal client, you can also use a phrase of multiple words if "
|
||||||
)
|
"you enclose the password in double quotes." % self.policy
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -222,7 +222,8 @@ COMMAND_RATE_WARNING = "You entered commands too fast. Wait a moment and try aga
|
||||||
# 0 or less.
|
# 0 or less.
|
||||||
MAX_CHAR_LIMIT = 6000
|
MAX_CHAR_LIMIT = 6000
|
||||||
# The warning to echo back to users if they enter a very large string
|
# The warning to echo back to users if they enter a very large string
|
||||||
MAX_CHAR_LIMIT_WARNING = "You entered a string that was too long. Please break it up into multiple parts."
|
MAX_CHAR_LIMIT_WARNING = ("You entered a string that was too long. "
|
||||||
|
"Please break it up into multiple parts.")
|
||||||
# If this is true, errors and tracebacks from the engine will be
|
# If this is true, errors and tracebacks from the engine will be
|
||||||
# echoed as text in-game as well as to the log. This can speed up
|
# echoed as text in-game as well as to the log. This can speed up
|
||||||
# debugging. OBS: Showing full tracebacks to regular users could be a
|
# debugging. OBS: Showing full tracebacks to regular users could be a
|
||||||
|
|
@ -410,12 +411,14 @@ CMDSET_CHARACTER = "commands.default_cmdsets.CharacterCmdSet"
|
||||||
CMDSET_ACCOUNT = "commands.default_cmdsets.AccountCmdSet"
|
CMDSET_ACCOUNT = "commands.default_cmdsets.AccountCmdSet"
|
||||||
# Location to search for cmdsets if full path not given
|
# Location to search for cmdsets if full path not given
|
||||||
CMDSET_PATHS = ["commands", "evennia", "contribs"]
|
CMDSET_PATHS = ["commands", "evennia", "contribs"]
|
||||||
# Fallbacks for cmdset paths that fail to load. Note that if you change the path for your default cmdsets,
|
# Fallbacks for cmdset paths that fail to load. Note that if you change the path for your
|
||||||
# you will also need to copy CMDSET_FALLBACKS after your change in your settings file for it to detect the change.
|
# default cmdsets, you will also need to copy CMDSET_FALLBACKS after your change in your
|
||||||
CMDSET_FALLBACKS = {CMDSET_CHARACTER: 'evennia.commands.default.cmdset_character.CharacterCmdSet',
|
# settings file for it to detect the change.
|
||||||
CMDSET_ACCOUNT: 'evennia.commands.default.cmdset_account.AccountCmdSet',
|
CMDSET_FALLBACKS = {
|
||||||
CMDSET_SESSION: 'evennia.commands.default.cmdset_session.SessionCmdSet',
|
CMDSET_CHARACTER: 'evennia.commands.default.cmdset_character.CharacterCmdSet',
|
||||||
CMDSET_UNLOGGEDIN: 'evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet'}
|
CMDSET_ACCOUNT: 'evennia.commands.default.cmdset_account.AccountCmdSet',
|
||||||
|
CMDSET_SESSION: 'evennia.commands.default.cmdset_session.SessionCmdSet',
|
||||||
|
CMDSET_UNLOGGEDIN: 'evennia.commands.default.cmdset_unloggedin.UnloggedinCmdSet'}
|
||||||
# Parent class for all default commands. Changing this class will
|
# Parent class for all default commands. Changing this class will
|
||||||
# modify all default commands, so do so carefully.
|
# modify all default commands, so do so carefully.
|
||||||
COMMAND_DEFAULT_CLASS = "evennia.commands.default.muxcommand.MuxCommand"
|
COMMAND_DEFAULT_CLASS = "evennia.commands.default.muxcommand.MuxCommand"
|
||||||
|
|
@ -810,7 +813,7 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
|
{'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator'},
|
||||||
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
|
{'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator'},
|
||||||
{'NAME': 'evennia.server.validators.EvenniaPasswordValidator'}]
|
{'NAME': 'evennia.server.validators.EvenniaPasswordValidator'}]
|
||||||
|
|
||||||
# Username validation plugins
|
# Username validation plugins
|
||||||
AUTH_USERNAME_VALIDATORS = [
|
AUTH_USERNAME_VALIDATORS = [
|
||||||
{'NAME': 'django.contrib.auth.validators.ASCIIUsernameValidator'},
|
{'NAME': 'django.contrib.auth.validators.ASCIIUsernameValidator'},
|
||||||
|
|
@ -830,7 +833,7 @@ TEST_RUNNER = 'evennia.server.tests.EvenniaTestSuiteRunner'
|
||||||
# Django extesions are useful third-party tools that are not
|
# Django extesions are useful third-party tools that are not
|
||||||
# always included in the default django distro.
|
# always included in the default django distro.
|
||||||
try:
|
try:
|
||||||
import django_extensions
|
import django_extensions # noqa
|
||||||
INSTALLED_APPS = INSTALLED_APPS + ('django_extensions',)
|
INSTALLED_APPS = INSTALLED_APPS + ('django_extensions',)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
# Django extensions are not installed in all distros.
|
# Django extensions are not installed in all distros.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue