Merge branch 'develop' into modernize
This commit is contained in:
commit
2d27a54da9
46 changed files with 1810 additions and 431 deletions
45
CHANGELOG.md
45
CHANGELOG.md
|
|
@ -1,7 +1,51 @@
|
||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## Evennia 0.9 (2018-2019)
|
||||||
|
|
||||||
|
### Commands
|
||||||
|
|
||||||
|
- Removed default `@delaccount` command, incorporating as `@account/delete` instead. Added confirmation
|
||||||
|
question.
|
||||||
|
- Add new `@force` command to have another object perform a command.
|
||||||
|
- Add the Portal uptime to the `@time` command.
|
||||||
|
- 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
|
||||||
|
|
||||||
|
- Added more unit tests.
|
||||||
|
|
||||||
|
|
||||||
## Evennia 0.8 (2018)
|
## Evennia 0.8 (2018)
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
|
||||||
|
- Up requirements to Django 1.11.x, Twisted 18 and pillow 5.2.0
|
||||||
|
- Add `inflect` dependency for automatic pluralization of object names.
|
||||||
|
|
||||||
### Server/Portal
|
### Server/Portal
|
||||||
|
|
||||||
- Removed `evennia_runner`, completely refactor `evennia_launcher.py` (the 'evennia' program)
|
- Removed `evennia_runner`, completely refactor `evennia_launcher.py` (the 'evennia' program)
|
||||||
|
|
@ -85,7 +129,6 @@
|
||||||
|
|
||||||
### General
|
### General
|
||||||
|
|
||||||
- Up requirements to Django 1.11.x, Twisted 18 and pillow 5.2.0
|
|
||||||
- Start structuring the `CHANGELOG` to list features in more detail.
|
- Start structuring the `CHANGELOG` to list features in more detail.
|
||||||
- Docker image `evennia/evennia:develop` is now auto-built, tracking the develop branch.
|
- Docker image `evennia/evennia:develop` is now auto-built, tracking the develop branch.
|
||||||
- Inflection and grouping of multiple objects in default room (an box, three boxes)
|
- Inflection and grouping of multiple objects in default room (an box, three boxes)
|
||||||
|
|
|
||||||
12
Dockerfile
12
Dockerfile
|
|
@ -7,7 +7,7 @@
|
||||||
# Usage:
|
# Usage:
|
||||||
# cd to a folder where you want your game data to be (or where it already is).
|
# cd to a folder where you want your game data to be (or where it already is).
|
||||||
#
|
#
|
||||||
# docker run -it -p 4000:4000 -p 4001:4001 -p 4005:4005 -v $PWD:/usr/src/game evennia/evennia
|
# docker run -it --rm -p 4000:4000 -p 4001:4001 -p 4005:4005 -v $PWD:/usr/src/game evennia/evennia
|
||||||
#
|
#
|
||||||
# (If your OS does not support $PWD, replace it with the full path to your current
|
# (If your OS does not support $PWD, replace it with the full path to your current
|
||||||
# folder).
|
# folder).
|
||||||
|
|
@ -15,6 +15,14 @@
|
||||||
# You will end up in a shell where the `evennia` command is available. From here you
|
# You will end up in a shell where the `evennia` command is available. From here you
|
||||||
# can install and run the game normally. Use Ctrl-D to exit the evennia docker container.
|
# can install and run the game normally. Use Ctrl-D to exit the evennia docker container.
|
||||||
#
|
#
|
||||||
|
# You can also start evennia directly by passing arguments to the folder:
|
||||||
|
#
|
||||||
|
# docker run -it --rm -p 4000:4000 -p 4001:4001 -p 4005:4005 -v $PWD:/usr/src/game evennia/evennia evennia start -l
|
||||||
|
#
|
||||||
|
# This will start Evennia running as the core process of the container. Note that you *must* use -l
|
||||||
|
# or one of the foreground modes (like evennia ipstart) since otherwise the container will immediately
|
||||||
|
# die since no foreground process keeps it up.
|
||||||
|
#
|
||||||
# The evennia/evennia base image is found on DockerHub and can also be used
|
# The evennia/evennia base image is found on DockerHub and can also be used
|
||||||
# as a base for creating your own custom containerized Evennia game. For more
|
# as a base for creating your own custom containerized Evennia game. For more
|
||||||
# info, see https://github.com/evennia/evennia/wiki/Running%20Evennia%20in%20Docker .
|
# info, see https://github.com/evennia/evennia/wiki/Running%20Evennia%20in%20Docker .
|
||||||
|
|
@ -58,7 +66,7 @@ WORKDIR /usr/src/game
|
||||||
ENV PS1 "evennia|docker \w $ "
|
ENV PS1 "evennia|docker \w $ "
|
||||||
|
|
||||||
# startup a shell when we start the container
|
# startup a shell when we start the container
|
||||||
ENTRYPOINT bash -c "source /usr/src/evennia/bin/unix/evennia-docker-start.sh"
|
ENTRYPOINT ["/usr/src/evennia/bin/unix/evennia-docker-start.sh"]
|
||||||
|
|
||||||
# expose the telnet, webserver and websocket client ports
|
# expose the telnet, webserver and websocket client ports
|
||||||
EXPOSE 4000 4001 4005
|
EXPOSE 4000 4001 4005
|
||||||
|
|
|
||||||
16
bin/unix/evennia-docker-start.sh
Normal file → Executable file
16
bin/unix/evennia-docker-start.sh
Normal file → Executable file
|
|
@ -1,10 +1,18 @@
|
||||||
#! /bin/bash
|
#! /bin/sh
|
||||||
|
|
||||||
# called by the Dockerfile to start the server in docker mode
|
# called by the Dockerfile to start the server in docker mode
|
||||||
|
|
||||||
# remove leftover .pid files (such as from when dropping the container)
|
# remove leftover .pid files (such as from when dropping the container)
|
||||||
rm /usr/src/game/server/*.pid >& /dev/null || true
|
rm /usr/src/game/server/*.pid >& /dev/null || true
|
||||||
|
|
||||||
# start evennia server; log to server.log but also output to stdout so it can
|
PS1="evennia|docker \w $ "
|
||||||
# be viewed with docker-compose logs
|
|
||||||
exec 3>&1; evennia start -l
|
cmd="$@"
|
||||||
|
output="Docker starting with argument '$cmd' ..."
|
||||||
|
if test -z $cmd; then
|
||||||
|
cmd="bash"
|
||||||
|
output="No argument given, starting shell ..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo $output
|
||||||
|
exec 3>&1; $cmd
|
||||||
|
|
|
||||||
|
|
@ -10,19 +10,22 @@ character object, so you should customize that
|
||||||
instead for most things).
|
instead for most things).
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import re
|
||||||
import time
|
import time
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from django.contrib.auth import password_validation
|
from django.contrib.auth import authenticate, password_validation
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ImproperlyConfigured, ValidationError
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.module_loading import import_string
|
||||||
from evennia.typeclasses.models import TypeclassBase
|
from evennia.typeclasses.models import TypeclassBase
|
||||||
from evennia.accounts.manager import AccountManager
|
from evennia.accounts.manager import AccountManager
|
||||||
from evennia.accounts.models import AccountDB
|
from evennia.accounts.models import AccountDB
|
||||||
from evennia.objects.models import ObjectDB
|
from evennia.objects.models import ObjectDB
|
||||||
from evennia.comms.models import ChannelDB
|
from evennia.comms.models import ChannelDB
|
||||||
from evennia.commands import cmdhandler
|
from evennia.commands import cmdhandler
|
||||||
from evennia.utils import logger
|
from evennia.server.models import ServerConfig
|
||||||
|
from evennia.server.throttle import Throttle
|
||||||
|
from evennia.utils import class_from_module, create, logger
|
||||||
from evennia.utils.utils import (lazy_property, to_str,
|
from evennia.utils.utils import (lazy_property, to_str,
|
||||||
make_iter, to_unicode, is_iter,
|
make_iter, to_unicode, is_iter,
|
||||||
variable_from_module)
|
variable_from_module)
|
||||||
|
|
@ -32,6 +35,7 @@ from evennia.commands.cmdsethandler import CmdSetHandler
|
||||||
|
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from future.utils import with_metaclass
|
from future.utils import with_metaclass
|
||||||
|
from random import getrandbits
|
||||||
|
|
||||||
__all__ = ("DefaultAccount",)
|
__all__ = ("DefaultAccount",)
|
||||||
|
|
||||||
|
|
@ -43,6 +47,9 @@ _MAX_NR_CHARACTERS = settings.MAX_NR_CHARACTERS
|
||||||
_CMDSET_ACCOUNT = settings.CMDSET_ACCOUNT
|
_CMDSET_ACCOUNT = settings.CMDSET_ACCOUNT
|
||||||
_CONNECT_CHANNEL = None
|
_CONNECT_CHANNEL = None
|
||||||
|
|
||||||
|
# Create throttles for too many account-creations and login attempts
|
||||||
|
CREATION_THROTTLE = Throttle(limit=2, timeout=10 * 60)
|
||||||
|
LOGIN_THROTTLE = Throttle(limit=5, timeout=5 * 60)
|
||||||
|
|
||||||
class AccountSessionHandler(object):
|
class AccountSessionHandler(object):
|
||||||
"""
|
"""
|
||||||
|
|
@ -372,6 +379,189 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
puppet = property(__get_single_puppet)
|
puppet = property(__get_single_puppet)
|
||||||
|
|
||||||
# utility methods
|
# utility methods
|
||||||
|
@classmethod
|
||||||
|
def is_banned(cls, **kwargs):
|
||||||
|
"""
|
||||||
|
Checks if a given username or IP is banned.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
ip (str, optional): IP address.
|
||||||
|
username (str, optional): Username.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
is_banned (bool): Whether either is banned or not.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
ip = kwargs.get('ip', '').strip()
|
||||||
|
username = kwargs.get('username', '').lower().strip()
|
||||||
|
|
||||||
|
# Check IP and/or name bans
|
||||||
|
bans = ServerConfig.objects.conf("server_bans")
|
||||||
|
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])):
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_username_validators(cls, validator_config=getattr(settings, 'AUTH_USERNAME_VALIDATORS', [])):
|
||||||
|
"""
|
||||||
|
Retrieves and instantiates validators for usernames.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
validator_config (list): List of dicts comprising the battery of
|
||||||
|
validators to apply to a username.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
validators (list): List of instantiated Validator objects.
|
||||||
|
"""
|
||||||
|
|
||||||
|
objs = []
|
||||||
|
for validator in validator_config:
|
||||||
|
try:
|
||||||
|
klass = import_string(validator['NAME'])
|
||||||
|
except ImportError:
|
||||||
|
msg = "The module in NAME could not be imported: %s. Check your AUTH_USERNAME_VALIDATORS setting."
|
||||||
|
raise ImproperlyConfigured(msg % validator['NAME'])
|
||||||
|
objs.append(klass(**validator.get('OPTIONS', {})))
|
||||||
|
return objs
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def authenticate(cls, username, password, ip='', **kwargs):
|
||||||
|
"""
|
||||||
|
Checks the given username/password against the database to see if the
|
||||||
|
credentials are valid.
|
||||||
|
|
||||||
|
Note that this simply checks credentials and returns a valid reference
|
||||||
|
to the user-- it does not log them in!
|
||||||
|
|
||||||
|
To finish the job:
|
||||||
|
After calling this from a Command, associate the account with a Session:
|
||||||
|
- session.sessionhandler.login(session, account)
|
||||||
|
|
||||||
|
...or after calling this from a View, associate it with an HttpRequest:
|
||||||
|
- django.contrib.auth.login(account, request)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username (str): Username of account
|
||||||
|
password (str): Password of account
|
||||||
|
ip (str, optional): IP address of client
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
session (Session, optional): Session requesting authentication
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
account (DefaultAccount, None): Account whose credentials were
|
||||||
|
provided if not banned.
|
||||||
|
errors (list): Error messages of any failures.
|
||||||
|
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
if ip: ip = str(ip)
|
||||||
|
|
||||||
|
# See if authentication is currently being throttled
|
||||||
|
if ip and LOGIN_THROTTLE.check(ip):
|
||||||
|
errors.append('Too many login failures; please try again in a few minutes.')
|
||||||
|
|
||||||
|
# With throttle active, do not log continued hits-- it is a
|
||||||
|
# waste of storage and can be abused to make your logs harder to
|
||||||
|
# read and/or fill up your disk.
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
# Check IP and/or name bans
|
||||||
|
banned = cls.is_banned(username=username, ip=ip)
|
||||||
|
if banned:
|
||||||
|
# this is a banned IP or name!
|
||||||
|
errors.append("|rYou have been banned and cannot continue from here." \
|
||||||
|
"\nIf you feel this ban is in error, please email an admin.|x")
|
||||||
|
logger.log_sec('Authentication Denied (Banned): %s (IP: %s).' % (username, ip))
|
||||||
|
LOGIN_THROTTLE.update(ip, 'Too many sightings of banned artifact.')
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
# Authenticate and get Account object
|
||||||
|
account = authenticate(username=username, password=password)
|
||||||
|
if not account:
|
||||||
|
# User-facing message
|
||||||
|
errors.append('Username and/or password is incorrect.')
|
||||||
|
|
||||||
|
# Log auth failures while throttle is inactive
|
||||||
|
logger.log_sec('Authentication Failure: %s (IP: %s).' % (username, ip))
|
||||||
|
|
||||||
|
# Update throttle
|
||||||
|
if ip: LOGIN_THROTTLE.update(ip, 'Too many authentication failures.')
|
||||||
|
|
||||||
|
# Try to call post-failure hook
|
||||||
|
session = kwargs.get('session', None)
|
||||||
|
if session:
|
||||||
|
account = AccountDB.objects.get_account_from_name(username)
|
||||||
|
if account:
|
||||||
|
account.at_failed_login(session)
|
||||||
|
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
# Account successfully authenticated
|
||||||
|
logger.log_sec('Authentication Success: %s (IP: %s).' % (account, ip))
|
||||||
|
return account, errors
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def normalize_username(cls, username):
|
||||||
|
"""
|
||||||
|
Django: Applies NFKC Unicode normalization to usernames so that visually
|
||||||
|
identical characters with different Unicode code points are considered
|
||||||
|
identical.
|
||||||
|
|
||||||
|
(This deals with the Turkish "i" problem and similar
|
||||||
|
annoyances. Only relevant if you go out of your way to allow Unicode
|
||||||
|
usernames though-- Evennia accepts ASCII by default.)
|
||||||
|
|
||||||
|
In this case we're simply piggybacking on this feature to apply
|
||||||
|
additional normalization per Evennia's standards.
|
||||||
|
"""
|
||||||
|
username = super(DefaultAccount, cls).normalize_username(username)
|
||||||
|
|
||||||
|
# strip excessive spaces in accountname
|
||||||
|
username = re.sub(r"\s+", " ", username).strip()
|
||||||
|
|
||||||
|
return username
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def validate_username(cls, username):
|
||||||
|
"""
|
||||||
|
Checks the given username against the username validator associated with
|
||||||
|
Account objects, and also checks the database to make sure it is unique.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username (str): Username to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
valid (bool): Whether or not the password passed validation
|
||||||
|
errors (list): Error messages of any failures
|
||||||
|
|
||||||
|
"""
|
||||||
|
valid = []
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
# Make sure we're at least using the default validator
|
||||||
|
validators = cls.get_username_validators()
|
||||||
|
if not validators:
|
||||||
|
validators = [cls.username_validator]
|
||||||
|
|
||||||
|
# Try username against all enabled validators
|
||||||
|
for validator in validators:
|
||||||
|
try:
|
||||||
|
valid.append(not validator(username))
|
||||||
|
except ValidationError as e:
|
||||||
|
valid.append(False)
|
||||||
|
errors.extend(e.messages)
|
||||||
|
|
||||||
|
# Disqualify if any check failed
|
||||||
|
if False in valid:
|
||||||
|
valid = False
|
||||||
|
else: valid = True
|
||||||
|
|
||||||
|
return valid, errors
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_password(cls, password, account=None):
|
def validate_password(cls, password, account=None):
|
||||||
"""
|
"""
|
||||||
|
|
@ -429,9 +619,136 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
if error: raise error
|
if error: raise error
|
||||||
|
|
||||||
super(DefaultAccount, self).set_password(password)
|
super(DefaultAccount, self).set_password(password)
|
||||||
logger.log_info("Password succesfully changed for %s." % self)
|
logger.log_sec("Password successfully changed for %s." % self)
|
||||||
self.at_password_change()
|
self.at_password_change()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Creates an Account (or Account/Character pair for MULTISESSION_MODE<2)
|
||||||
|
with default (or overridden) permissions and having joined them to the
|
||||||
|
appropriate default channels.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
username (str): Username of Account owner
|
||||||
|
password (str): Password of Account owner
|
||||||
|
email (str, optional): Email address of Account owner
|
||||||
|
ip (str, optional): IP address of requesting connection
|
||||||
|
guest (bool, optional): Whether or not this is to be a Guest account
|
||||||
|
|
||||||
|
permissions (str, optional): Default permissions for the Account
|
||||||
|
typeclass (str, optional): Typeclass to use for new Account
|
||||||
|
character_typeclass (str, optional): Typeclass to use for new char
|
||||||
|
when applicable.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
account (Account): Account if successfully created; None if not
|
||||||
|
errors (list): List of error messages in string form
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
account = None
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
username = kwargs.get('username')
|
||||||
|
password = kwargs.get('password')
|
||||||
|
email = kwargs.get('email', '').strip()
|
||||||
|
guest = kwargs.get('guest', False)
|
||||||
|
|
||||||
|
permissions = kwargs.get('permissions', settings.PERMISSION_ACCOUNT_DEFAULT)
|
||||||
|
typeclass = kwargs.get('typeclass', settings.BASE_ACCOUNT_TYPECLASS)
|
||||||
|
|
||||||
|
ip = kwargs.get('ip', '')
|
||||||
|
if ip and CREATION_THROTTLE.check(ip):
|
||||||
|
errors.append("You are creating too many accounts. Please log into an existing account.")
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
# Normalize username
|
||||||
|
username = cls.normalize_username(username)
|
||||||
|
|
||||||
|
# Validate username
|
||||||
|
if not guest:
|
||||||
|
valid, errs = cls.validate_username(username)
|
||||||
|
if not valid:
|
||||||
|
# this echoes the restrictions made by django's auth
|
||||||
|
# module (except not allowing spaces, for convenience of
|
||||||
|
# logging in).
|
||||||
|
errors.extend(errs)
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
# Validate password
|
||||||
|
# Have to create a dummy Account object to check username similarity
|
||||||
|
valid, errs = cls.validate_password(password, account=cls(username=username))
|
||||||
|
if not valid:
|
||||||
|
errors.extend(errs)
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
# Check IP and/or name bans
|
||||||
|
banned = cls.is_banned(username=username, ip=ip)
|
||||||
|
if banned:
|
||||||
|
# this is a banned IP or name!
|
||||||
|
string = "|rYou have been banned and cannot continue from here." \
|
||||||
|
"\nIf you feel this ban is in error, please email an admin.|x"
|
||||||
|
errors.append(string)
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
# everything's ok. Create the new account account.
|
||||||
|
try:
|
||||||
|
try:
|
||||||
|
account = create.create_account(username, email, password, permissions=permissions, typeclass=typeclass)
|
||||||
|
logger.log_sec('Account Created: %s (IP: %s).' % (account, ip))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
errors.append("There was an error creating the Account. If this problem persists, contact an admin.")
|
||||||
|
logger.log_trace()
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
# 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
|
||||||
|
# hooks during login later)
|
||||||
|
account.db.FIRST_LOGIN = True
|
||||||
|
|
||||||
|
# Record IP address of creation, if available
|
||||||
|
if ip: account.db.creator_ip = ip
|
||||||
|
|
||||||
|
# join the new account to the public channel
|
||||||
|
pchannel = ChannelDB.objects.get_channel(settings.DEFAULT_CHANNELS[0]["key"])
|
||||||
|
if not pchannel or not pchannel.connect(account):
|
||||||
|
string = "New account '%s' could not connect to public channel!" % account.key
|
||||||
|
errors.append(string)
|
||||||
|
logger.log_err(string)
|
||||||
|
|
||||||
|
if account and settings.MULTISESSION_MODE < 2:
|
||||||
|
# Load the appropriate Character class
|
||||||
|
character_typeclass = kwargs.get('character_typeclass', settings.BASE_CHARACTER_TYPECLASS)
|
||||||
|
character_home = kwargs.get('home')
|
||||||
|
Character = class_from_module(character_typeclass)
|
||||||
|
|
||||||
|
# Create the character
|
||||||
|
character, errs = Character.create(
|
||||||
|
account.key, account, ip=ip, typeclass=character_typeclass,
|
||||||
|
permissions=permissions, home=character_home
|
||||||
|
)
|
||||||
|
errors.extend(errs)
|
||||||
|
|
||||||
|
if character:
|
||||||
|
# Update playable character list
|
||||||
|
account.db._playable_characters.append(character)
|
||||||
|
|
||||||
|
# We need to set this to have @ic auto-connect to this character
|
||||||
|
account.db._last_puppet = character
|
||||||
|
|
||||||
|
except Exception:
|
||||||
|
# We are in the middle between logged in and -not, so we have
|
||||||
|
# to handle tracebacks ourselves at this point. If we don't,
|
||||||
|
# we won't see any errors at all.
|
||||||
|
errors.append("An error occurred. Please e-mail an admin if the problem persists.")
|
||||||
|
logger.log_trace()
|
||||||
|
|
||||||
|
# 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.')
|
||||||
|
return account, errors
|
||||||
|
|
||||||
def delete(self, *args, **kwargs):
|
def delete(self, *args, **kwargs):
|
||||||
"""
|
"""
|
||||||
Deletes the account permanently.
|
Deletes the account permanently.
|
||||||
|
|
@ -1014,7 +1331,10 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
|
|
||||||
if target and not is_iter(target):
|
if target and not is_iter(target):
|
||||||
# single target - just show it
|
# single target - just show it
|
||||||
return target.return_appearance(self)
|
if hasattr(target, "return_appearance"):
|
||||||
|
return target.return_appearance(self)
|
||||||
|
else:
|
||||||
|
return "{} has no in-game appearance.".format(target)
|
||||||
else:
|
else:
|
||||||
# list of targets - make list to disconnect from db
|
# list of targets - make list to disconnect from db
|
||||||
characters = list(tar for tar in target if tar) if target else []
|
characters = list(tar for tar in target if tar) if target else []
|
||||||
|
|
@ -1078,6 +1398,78 @@ class DefaultGuest(DefaultAccount):
|
||||||
their characters are deleted after disconnection.
|
their characters are deleted after disconnection.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, **kwargs):
|
||||||
|
"""
|
||||||
|
Forwards request to cls.authenticate(); returns a DefaultGuest object
|
||||||
|
if one is available for use.
|
||||||
|
"""
|
||||||
|
return cls.authenticate(**kwargs)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def authenticate(cls, **kwargs):
|
||||||
|
"""
|
||||||
|
Gets or creates a Guest account object.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
ip (str, optional): IP address of requestor; used for ban checking,
|
||||||
|
throttling and logging
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
account (Object): Guest account object, if available
|
||||||
|
errors (list): List of error messages accrued during this request.
|
||||||
|
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
account = None
|
||||||
|
username = None
|
||||||
|
ip = kwargs.get('ip', '').strip()
|
||||||
|
|
||||||
|
# check if guests are enabled.
|
||||||
|
if not settings.GUEST_ENABLED:
|
||||||
|
errors.append('Guest accounts are not enabled on this server.')
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Find an available guest name.
|
||||||
|
for name in settings.GUEST_LIST:
|
||||||
|
if not AccountDB.objects.filter(username__iexact=name).count():
|
||||||
|
username = name
|
||||||
|
break
|
||||||
|
if not username:
|
||||||
|
errors.append("All guest accounts are in use. Please try again later.")
|
||||||
|
if ip: LOGIN_THROTTLE.update(ip, 'Too many requests for Guest access.')
|
||||||
|
return None, errors
|
||||||
|
else:
|
||||||
|
# build a new account with the found guest username
|
||||||
|
password = "%016x" % getrandbits(64)
|
||||||
|
home = settings.GUEST_HOME
|
||||||
|
permissions = settings.PERMISSION_GUEST_DEFAULT
|
||||||
|
typeclass = settings.BASE_GUEST_TYPECLASS
|
||||||
|
|
||||||
|
# Call parent class creator
|
||||||
|
account, errs = super(DefaultGuest, cls).create(
|
||||||
|
guest=True,
|
||||||
|
username=username,
|
||||||
|
password=password,
|
||||||
|
permissions=permissions,
|
||||||
|
typeclass=typeclass,
|
||||||
|
home=home,
|
||||||
|
ip=ip,
|
||||||
|
)
|
||||||
|
errors.extend(errs)
|
||||||
|
return account, errors
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
# We are in the middle between logged in and -not, so we have
|
||||||
|
# to handle tracebacks ourselves at this point. If we don't,
|
||||||
|
# we won't see any errors at all.
|
||||||
|
errors.append("An error occurred. Please e-mail an admin if the problem persists.")
|
||||||
|
logger.log_trace()
|
||||||
|
return None, errors
|
||||||
|
|
||||||
|
return account, errors
|
||||||
|
|
||||||
def at_post_login(self, session=None, **kwargs):
|
def at_post_login(self, session=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
In theory, guests only have one character regardless of which
|
In theory, guests only have one character regardless of which
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,13 @@
|
||||||
from mock import Mock
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
from mock import Mock, MagicMock
|
||||||
from random import randint
|
from random import randint
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
|
||||||
|
from django.test import override_settings
|
||||||
from evennia.accounts.accounts import AccountSessionHandler
|
from evennia.accounts.accounts import AccountSessionHandler
|
||||||
from evennia.accounts.accounts import DefaultAccount
|
from evennia.accounts.accounts import DefaultAccount, DefaultGuest
|
||||||
from evennia.server.session import Session
|
from evennia.utils.test_resources import EvenniaTest
|
||||||
from evennia.utils import create
|
from evennia.utils import create
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
@ -14,9 +17,15 @@ class TestAccountSessionHandler(TestCase):
|
||||||
"Check AccountSessionHandler class"
|
"Check AccountSessionHandler class"
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.account = create.create_account("TestAccount%s" % randint(0, 999999), email="test@test.com", password="testpassword", typeclass=DefaultAccount)
|
self.account = create.create_account(
|
||||||
|
"TestAccount%s" % randint(0, 999999), email="test@test.com",
|
||||||
|
password="testpassword", typeclass=DefaultAccount)
|
||||||
self.handler = AccountSessionHandler(self.account)
|
self.handler = AccountSessionHandler(self.account)
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
if hasattr(self, 'account'):
|
||||||
|
self.account.delete()
|
||||||
|
|
||||||
def test_get(self):
|
def test_get(self):
|
||||||
"Check get method"
|
"Check get method"
|
||||||
self.assertEqual(self.handler.get(), [])
|
self.assertEqual(self.handler.get(), [])
|
||||||
|
|
@ -24,24 +33,24 @@ class TestAccountSessionHandler(TestCase):
|
||||||
|
|
||||||
import evennia.server.sessionhandler
|
import evennia.server.sessionhandler
|
||||||
|
|
||||||
s1 = Session()
|
s1 = MagicMock()
|
||||||
s1.logged_in = True
|
s1.logged_in = True
|
||||||
s1.uid = self.account.uid
|
s1.uid = self.account.uid
|
||||||
evennia.server.sessionhandler.SESSIONS[s1.uid] = s1
|
evennia.server.sessionhandler.SESSIONS[s1.uid] = s1
|
||||||
|
|
||||||
s2 = Session()
|
s2 = MagicMock()
|
||||||
s2.logged_in = True
|
s2.logged_in = True
|
||||||
s2.uid = self.account.uid + 1
|
s2.uid = self.account.uid + 1
|
||||||
evennia.server.sessionhandler.SESSIONS[s2.uid] = s2
|
evennia.server.sessionhandler.SESSIONS[s2.uid] = s2
|
||||||
|
|
||||||
s3 = Session()
|
s3 = MagicMock()
|
||||||
s3.logged_in = False
|
s3.logged_in = False
|
||||||
s3.uid = self.account.uid + 2
|
s3.uid = self.account.uid + 2
|
||||||
evennia.server.sessionhandler.SESSIONS[s3.uid] = s3
|
evennia.server.sessionhandler.SESSIONS[s3.uid] = s3
|
||||||
|
|
||||||
self.assertEqual(self.handler.get(), [s1])
|
self.assertEqual([s.uid for s in self.handler.get()], [s1.uid])
|
||||||
self.assertEqual(self.handler.get(self.account.uid), [s1])
|
self.assertEqual([s.uid for s in [self.handler.get(self.account.uid)]], [s1.uid])
|
||||||
self.assertEqual(self.handler.get(self.account.uid + 1), [])
|
self.assertEqual([s.uid for s in self.handler.get(self.account.uid + 1)], [])
|
||||||
|
|
||||||
def test_all(self):
|
def test_all(self):
|
||||||
"Check all method"
|
"Check all method"
|
||||||
|
|
@ -51,40 +60,115 @@ class TestAccountSessionHandler(TestCase):
|
||||||
"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):
|
||||||
|
"Check DefaultGuest class"
|
||||||
|
|
||||||
class TestDefaultAccount(TestCase):
|
ip = '212.216.134.22'
|
||||||
"Check DefaultAccount class"
|
|
||||||
|
def test_authenticate(self):
|
||||||
|
# Guest account should not be permitted
|
||||||
|
account, errors = DefaultGuest.authenticate(ip=self.ip)
|
||||||
|
self.assertFalse(account, 'Guest account was created despite being disabled.')
|
||||||
|
|
||||||
|
settings.GUEST_ENABLED = True
|
||||||
|
settings.GUEST_LIST = ['bruce_wayne']
|
||||||
|
|
||||||
|
# Create a guest account
|
||||||
|
account, errors = DefaultGuest.authenticate(ip=self.ip)
|
||||||
|
self.assertTrue(account, 'Guest account should have been created.')
|
||||||
|
|
||||||
|
# Create a second guest account
|
||||||
|
account, errors = DefaultGuest.authenticate(ip=self.ip)
|
||||||
|
self.assertFalse(account, 'Two guest accounts were created with a single entry on the guest list!')
|
||||||
|
|
||||||
|
settings.GUEST_ENABLED = False
|
||||||
|
|
||||||
|
class TestDefaultAccountAuth(EvenniaTest):
|
||||||
|
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.s1 = Session()
|
super(TestDefaultAccountAuth, self).setUp()
|
||||||
self.s1.puppet = None
|
|
||||||
self.s1.sessid = 0
|
self.password = "testpassword"
|
||||||
|
self.account.delete()
|
||||||
|
self.account = create.create_account("TestAccount%s" % randint(100000, 999999), email="test@test.com", password=self.password, typeclass=DefaultAccount)
|
||||||
|
|
||||||
|
def test_authentication(self):
|
||||||
|
"Confirm Account authentication method is authenticating/denying users."
|
||||||
|
# Valid credentials
|
||||||
|
obj, errors = DefaultAccount.authenticate(self.account.name, self.password)
|
||||||
|
self.assertTrue(obj, 'Account did not authenticate given valid credentials.')
|
||||||
|
|
||||||
|
# Invalid credentials
|
||||||
|
obj, errors = DefaultAccount.authenticate(self.account.name, 'xyzzy')
|
||||||
|
self.assertFalse(obj, 'Account authenticated using invalid credentials.')
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
"Confirm Account creation is working as expected."
|
||||||
|
# Create a normal account
|
||||||
|
account, errors = DefaultAccount.create(username='ziggy', password='stardust11')
|
||||||
|
self.assertTrue(account, 'New account should have been created.')
|
||||||
|
|
||||||
|
# Try creating a duplicate account
|
||||||
|
account2, errors = DefaultAccount.create(username='Ziggy', password='starman11')
|
||||||
|
self.assertFalse(account2, 'Duplicate account name should not have been allowed.')
|
||||||
|
account.delete()
|
||||||
|
|
||||||
|
def test_throttle(self):
|
||||||
|
"Confirm throttle activates on too many failures."
|
||||||
|
for x in xrange(20):
|
||||||
|
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.assertTrue('too many login failures' in errors[-1].lower(), 'Failed logins should have been throttled.')
|
||||||
|
|
||||||
|
def test_username_validation(self):
|
||||||
|
"Check username validators deny relevant usernames"
|
||||||
|
# Should not accept Unicode by default, lest users pick names like this
|
||||||
|
result, error = DefaultAccount.validate_username('¯\_(ツ)_/¯')
|
||||||
|
self.assertFalse(result, "Validator allowed kanji in username.")
|
||||||
|
|
||||||
|
# Should not allow duplicate username
|
||||||
|
result, error = DefaultAccount.validate_username(self.account.name)
|
||||||
|
self.assertFalse(result, "Duplicate username should not have passed validation.")
|
||||||
|
|
||||||
|
# Should not allow username too short
|
||||||
|
result, error = DefaultAccount.validate_username('xx')
|
||||||
|
self.assertFalse(result, "2-character username passed validation.")
|
||||||
|
|
||||||
def test_password_validation(self):
|
def test_password_validation(self):
|
||||||
"Check password validators deny bad passwords"
|
"Check password validators deny bad passwords"
|
||||||
|
|
||||||
self.account = create.create_account("TestAccount%s" % randint(0, 9),
|
account = create.create_account("TestAccount%s" % randint(100000, 999999),
|
||||||
email="test@test.com", password="testpassword", typeclass=DefaultAccount)
|
email="test@test.com", password="testpassword", typeclass=DefaultAccount)
|
||||||
for bad in ('', '123', 'password', 'TestAccount', '#', 'xyzzy'):
|
for bad in ('', '123', 'password', 'TestAccount', '#', 'xyzzy'):
|
||||||
self.assertFalse(self.account.validate_password(bad, account=self.account)[0])
|
self.assertFalse(account.validate_password(bad, account=self.account)[0])
|
||||||
|
|
||||||
"Check validators allow sufficiently complex passwords"
|
"Check validators allow sufficiently complex passwords"
|
||||||
for better in ('Mxyzptlk', "j0hn, i'M 0n1y d4nc1nG"):
|
for better in ('Mxyzptlk', "j0hn, i'M 0n1y d4nc1nG"):
|
||||||
self.assertTrue(self.account.validate_password(better, account=self.account)[0])
|
self.assertTrue(account.validate_password(better, account=self.account)[0])
|
||||||
self.account.delete()
|
account.delete()
|
||||||
|
|
||||||
def test_password_change(self):
|
def test_password_change(self):
|
||||||
"Check password setting and validation is working as expected"
|
"Check password setting and validation is working as expected"
|
||||||
self.account = create.create_account("TestAccount%s" % randint(0, 9),
|
account = create.create_account("TestAccount%s" % randint(100000, 999999),
|
||||||
email="test@test.com", password="testpassword", typeclass=DefaultAccount)
|
email="test@test.com", password="testpassword", typeclass=DefaultAccount)
|
||||||
|
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
# Try setting some bad passwords
|
# Try setting some bad passwords
|
||||||
for bad in ('', '#', 'TestAccount', 'password'):
|
for bad in ('', '#', 'TestAccount', 'password'):
|
||||||
self.assertRaises(ValidationError, self.account.set_password, bad)
|
self.assertRaises(ValidationError, account.set_password, bad)
|
||||||
|
|
||||||
# Try setting a better password (test for False; returns None on success)
|
# Try setting a better password (test for False; returns None on success)
|
||||||
self.assertFalse(self.account.set_password('Mxyzptlk'))
|
self.assertFalse(account.set_password('Mxyzptlk'))
|
||||||
|
account.delete()
|
||||||
|
|
||||||
|
class TestDefaultAccount(TestCase):
|
||||||
|
"Check DefaultAccount class"
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.s1 = MagicMock()
|
||||||
|
self.s1.puppet = None
|
||||||
|
self.s1.sessid = 0
|
||||||
|
|
||||||
def test_puppet_object_no_object(self):
|
def test_puppet_object_no_object(self):
|
||||||
"Check puppet_object method called with no object param"
|
"Check puppet_object method called with no object param"
|
||||||
|
|
@ -109,7 +193,9 @@ class TestDefaultAccount(TestCase):
|
||||||
|
|
||||||
import evennia.server.sessionhandler
|
import evennia.server.sessionhandler
|
||||||
|
|
||||||
account = create.create_account("TestAccount%s" % randint(0, 999999), email="test@test.com", password="testpassword", typeclass=DefaultAccount)
|
account = create.create_account(
|
||||||
|
"TestAccount%s" % randint(0, 999999), email="test@test.com",
|
||||||
|
password="testpassword", typeclass=DefaultAccount)
|
||||||
self.s1.uid = account.uid
|
self.s1.uid = account.uid
|
||||||
evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1
|
evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1
|
||||||
|
|
||||||
|
|
@ -131,10 +217,7 @@ class TestDefaultAccount(TestCase):
|
||||||
self.s1.uid = account.uid
|
self.s1.uid = account.uid
|
||||||
evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1
|
evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1
|
||||||
|
|
||||||
self.s1.puppet = None
|
self.s1.data_out = MagicMock()
|
||||||
self.s1.logged_in = True
|
|
||||||
self.s1.data_out = Mock(return_value=None)
|
|
||||||
|
|
||||||
obj = Mock()
|
obj = Mock()
|
||||||
obj.access = Mock(return_value=False)
|
obj.access = Mock(return_value=False)
|
||||||
|
|
||||||
|
|
@ -143,6 +226,7 @@ class TestDefaultAccount(TestCase):
|
||||||
self.assertTrue(self.s1.data_out.call_args[1]['text'].startswith("You don't have permission to puppet"))
|
self.assertTrue(self.s1.data_out.call_args[1]['text'].startswith("You don't have permission to puppet"))
|
||||||
self.assertIsNone(obj.at_post_puppet.call_args)
|
self.assertIsNone(obj.at_post_puppet.call_args)
|
||||||
|
|
||||||
|
@override_settings(MULTISESSION_MODE=0)
|
||||||
def test_puppet_object_joining_other_session(self):
|
def test_puppet_object_joining_other_session(self):
|
||||||
"Check puppet_object method called, joining other session"
|
"Check puppet_object method called, joining other session"
|
||||||
|
|
||||||
|
|
@ -154,15 +238,16 @@ class TestDefaultAccount(TestCase):
|
||||||
|
|
||||||
self.s1.puppet = None
|
self.s1.puppet = None
|
||||||
self.s1.logged_in = True
|
self.s1.logged_in = True
|
||||||
self.s1.data_out = Mock(return_value=None)
|
self.s1.data_out = MagicMock()
|
||||||
|
|
||||||
obj = Mock()
|
obj = Mock()
|
||||||
obj.access = Mock(return_value=True)
|
obj.access = Mock(return_value=True)
|
||||||
obj.account = account
|
obj.account = account
|
||||||
|
obj.sessions.all = MagicMock(return_value=[self.s1])
|
||||||
|
|
||||||
account.puppet_object(self.s1, obj)
|
account.puppet_object(self.s1, obj)
|
||||||
# works because django.conf.settings.MULTISESSION_MODE is not in (1, 3)
|
# works because django.conf.settings.MULTISESSION_MODE is not in (1, 3)
|
||||||
self.assertTrue(self.s1.data_out.call_args[1]['text'].endswith("from another of your sessions."))
|
self.assertTrue(self.s1.data_out.call_args[1]['text'].endswith("from another of your sessions.|n"))
|
||||||
self.assertTrue(obj.at_post_puppet.call_args[1] == {})
|
self.assertTrue(obj.at_post_puppet.call_args[1] == {})
|
||||||
|
|
||||||
def test_puppet_object_already_puppeted(self):
|
def test_puppet_object_already_puppeted(self):
|
||||||
|
|
@ -171,6 +256,7 @@ class TestDefaultAccount(TestCase):
|
||||||
import evennia.server.sessionhandler
|
import evennia.server.sessionhandler
|
||||||
|
|
||||||
account = create.create_account("TestAccount%s" % randint(0, 999999), email="test@test.com", password="testpassword", typeclass=DefaultAccount)
|
account = create.create_account("TestAccount%s" % randint(0, 999999), email="test@test.com", password="testpassword", typeclass=DefaultAccount)
|
||||||
|
self.account = account
|
||||||
self.s1.uid = account.uid
|
self.s1.uid = account.uid
|
||||||
evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1
|
evennia.server.sessionhandler.SESSIONS[self.s1.uid] = self.s1
|
||||||
|
|
||||||
|
|
@ -186,3 +272,20 @@ class TestDefaultAccount(TestCase):
|
||||||
account.puppet_object(self.s1, obj)
|
account.puppet_object(self.s1, obj)
|
||||||
self.assertTrue(self.s1.data_out.call_args[1]['text'].endswith("is already puppeted by another Account."))
|
self.assertTrue(self.s1.data_out.call_args[1]['text'].endswith("is already puppeted by another Account."))
|
||||||
self.assertIsNone(obj.at_post_puppet.call_args)
|
self.assertIsNone(obj.at_post_puppet.call_args)
|
||||||
|
|
||||||
|
|
||||||
|
class TestAccountPuppetDeletion(EvenniaTest):
|
||||||
|
|
||||||
|
@override_settings(MULTISESSION_MODE=2)
|
||||||
|
def test_puppet_deletion(self):
|
||||||
|
# Check for existing chars
|
||||||
|
self.assertFalse(self.account.db._playable_characters, 'Account should not have any chars by default.')
|
||||||
|
|
||||||
|
# Add char1 to account's playable characters
|
||||||
|
self.account.db._playable_characters.append(self.char1)
|
||||||
|
self.assertTrue(self.account.db._playable_characters, 'Char was not added to account.')
|
||||||
|
|
||||||
|
# See what happens when we delete char1.
|
||||||
|
self.char1.delete()
|
||||||
|
# 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)
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ from builtins import range
|
||||||
import time
|
import time
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from evennia.server.sessionhandler import SESSIONS
|
from evennia.server.sessionhandler import SESSIONS
|
||||||
from evennia.utils import utils, create, search, evtable
|
from evennia.utils import utils, create, logger, search, evtable
|
||||||
|
|
||||||
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||||
|
|
||||||
|
|
@ -171,6 +171,7 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
|
||||||
new_character.db.desc = "This is a character."
|
new_character.db.desc = "This is a character."
|
||||||
self.msg("Created new character %s. Use |w@ic %s|n to enter the game as this character."
|
self.msg("Created new character %s. Use |w@ic %s|n to enter the game as this character."
|
||||||
% (new_character.key, new_character.key))
|
% (new_character.key, new_character.key))
|
||||||
|
logger.log_sec('Character Created: %s (Caller: %s, IP: %s).' % (new_character, account, self.session.address))
|
||||||
|
|
||||||
|
|
||||||
class CmdCharDelete(COMMAND_DEFAULT_CLASS):
|
class CmdCharDelete(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
@ -214,6 +215,7 @@ class CmdCharDelete(COMMAND_DEFAULT_CLASS):
|
||||||
caller.db._playable_characters = [pc for pc in caller.db._playable_characters if pc != delobj]
|
caller.db._playable_characters = [pc for pc in caller.db._playable_characters if pc != delobj]
|
||||||
delobj.delete()
|
delobj.delete()
|
||||||
self.msg("Character '%s' was permanently deleted." % key)
|
self.msg("Character '%s' was permanently deleted." % key)
|
||||||
|
logger.log_sec('Character Deleted: %s (Caller: %s, IP: %s).' % (key, account, self.session.address))
|
||||||
else:
|
else:
|
||||||
self.msg("Deletion was aborted.")
|
self.msg("Deletion was aborted.")
|
||||||
del caller.ndb._char_to_delete
|
del caller.ndb._char_to_delete
|
||||||
|
|
@ -279,8 +281,10 @@ class CmdIC(COMMAND_DEFAULT_CLASS):
|
||||||
try:
|
try:
|
||||||
account.puppet_object(session, new_character)
|
account.puppet_object(session, new_character)
|
||||||
account.db._last_puppet = new_character
|
account.db._last_puppet = new_character
|
||||||
|
logger.log_sec('Puppet Success: (Caller: %s, Target: %s, IP: %s).' % (account, new_character, self.session.address))
|
||||||
except RuntimeError as exc:
|
except RuntimeError as exc:
|
||||||
self.msg("|rYou cannot become |C%s|n: %s" % (new_character.name, exc))
|
self.msg("|rYou cannot become |C%s|n: %s" % (new_character.name, exc))
|
||||||
|
logger.log_sec('Puppet Failed: %s (Caller: %s, Target: %s, IP: %s).' % (exc, account, new_character, self.session.address))
|
||||||
|
|
||||||
|
|
||||||
# note that this is inheriting from MuxAccountLookCommand,
|
# note that this is inheriting from MuxAccountLookCommand,
|
||||||
|
|
@ -641,6 +645,7 @@ class CmdPassword(COMMAND_DEFAULT_CLASS):
|
||||||
account.set_password(newpass)
|
account.set_password(newpass)
|
||||||
account.save()
|
account.save()
|
||||||
self.msg("Password changed.")
|
self.msg("Password changed.")
|
||||||
|
logger.log_sec('Password Changed: %s (Caller: %s, IP: %s).' % (account, account, self.session.address))
|
||||||
|
|
||||||
|
|
||||||
class CmdQuit(COMMAND_DEFAULT_CLASS):
|
class CmdQuit(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
|
||||||
|
|
@ -9,15 +9,15 @@ import re
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from evennia.server.sessionhandler import SESSIONS
|
from evennia.server.sessionhandler import SESSIONS
|
||||||
from evennia.server.models import ServerConfig
|
from evennia.server.models import ServerConfig
|
||||||
from evennia.utils import evtable, search, class_from_module
|
from evennia.utils import evtable, logger, search, class_from_module
|
||||||
|
|
||||||
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||||
|
|
||||||
PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY]
|
PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY]
|
||||||
|
|
||||||
# limit members for API inclusion
|
# limit members for API inclusion
|
||||||
__all__ = ("CmdBoot", "CmdBan", "CmdUnban", "CmdDelAccount",
|
__all__ = ("CmdBoot", "CmdBan", "CmdUnban",
|
||||||
"CmdEmit", "CmdNewPassword", "CmdPerm", "CmdWall")
|
"CmdEmit", "CmdNewPassword", "CmdPerm", "CmdWall", "CmdForce")
|
||||||
|
|
||||||
|
|
||||||
class CmdBoot(COMMAND_DEFAULT_CLASS):
|
class CmdBoot(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
@ -96,6 +96,9 @@ class CmdBoot(COMMAND_DEFAULT_CLASS):
|
||||||
session.msg(feedback)
|
session.msg(feedback)
|
||||||
session.account.disconnect_session_from_account(session)
|
session.account.disconnect_session_from_account(session)
|
||||||
|
|
||||||
|
if pobj and boot_list:
|
||||||
|
logger.log_sec('Booted: %s (Reason: %s, Caller: %s, IP: %s).' % (pobj, reason, caller, self.session.address))
|
||||||
|
|
||||||
|
|
||||||
# regex matching IP addresses with wildcards, eg. 233.122.4.*
|
# regex matching IP addresses with wildcards, eg. 233.122.4.*
|
||||||
IPREGEX = re.compile(r"[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}")
|
IPREGEX = re.compile(r"[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}")
|
||||||
|
|
@ -130,7 +133,7 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
|
||||||
reason to be able to later remember why the ban was put in place.
|
reason to be able to later remember why the ban was put in place.
|
||||||
|
|
||||||
It is often preferable to ban an account from the server than to
|
It is often preferable to ban an account from the server than to
|
||||||
delete an account with @delaccount. If banned by name, that account
|
delete an account with @accounts/delete. If banned by name, that account
|
||||||
account can no longer be logged into.
|
account can no longer be logged into.
|
||||||
|
|
||||||
IP (Internet Protocol) address banning allows blocking all access
|
IP (Internet Protocol) address banning allows blocking all access
|
||||||
|
|
@ -203,6 +206,7 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
|
||||||
banlist.append(bantup)
|
banlist.append(bantup)
|
||||||
ServerConfig.objects.conf('server_bans', banlist)
|
ServerConfig.objects.conf('server_bans', banlist)
|
||||||
self.caller.msg("%s-Ban |w%s|n was added." % (typ, ban))
|
self.caller.msg("%s-Ban |w%s|n was added." % (typ, ban))
|
||||||
|
logger.log_sec('Banned %s: %s (Caller: %s, IP: %s).' % (typ, ban.strip(), self.caller, self.session.address))
|
||||||
|
|
||||||
|
|
||||||
class CmdUnban(COMMAND_DEFAULT_CLASS):
|
class CmdUnban(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
@ -246,79 +250,10 @@ class CmdUnban(COMMAND_DEFAULT_CLASS):
|
||||||
ban = banlist[num - 1]
|
ban = banlist[num - 1]
|
||||||
del banlist[num - 1]
|
del banlist[num - 1]
|
||||||
ServerConfig.objects.conf('server_bans', banlist)
|
ServerConfig.objects.conf('server_bans', banlist)
|
||||||
|
value = " ".join([s for s in ban[:2]])
|
||||||
self.caller.msg("Cleared ban %s: %s" %
|
self.caller.msg("Cleared ban %s: %s" %
|
||||||
(num, " ".join([s for s in ban[:2]])))
|
(num, value))
|
||||||
|
logger.log_sec('Unbanned: %s (Caller: %s, IP: %s).' % (value.strip(), self.caller, self.session.address))
|
||||||
|
|
||||||
class CmdDelAccount(COMMAND_DEFAULT_CLASS):
|
|
||||||
"""
|
|
||||||
delete an account from the server
|
|
||||||
|
|
||||||
Usage:
|
|
||||||
@delaccount[/switch] <name> [: reason]
|
|
||||||
|
|
||||||
Switch:
|
|
||||||
delobj - also delete the account's currently
|
|
||||||
assigned in-game object.
|
|
||||||
|
|
||||||
Completely deletes a user from the server database,
|
|
||||||
making their nick and e-mail again available.
|
|
||||||
"""
|
|
||||||
|
|
||||||
key = "@delaccount"
|
|
||||||
switch_options = ("delobj",)
|
|
||||||
locks = "cmd:perm(delaccount) or perm(Developer)"
|
|
||||||
help_category = "Admin"
|
|
||||||
|
|
||||||
def func(self):
|
|
||||||
"""Implements the command."""
|
|
||||||
|
|
||||||
caller = self.caller
|
|
||||||
args = self.args
|
|
||||||
|
|
||||||
if hasattr(caller, 'account'):
|
|
||||||
caller = caller.account
|
|
||||||
|
|
||||||
if not args:
|
|
||||||
self.msg("Usage: @delaccount <account/user name or #id> [: reason]")
|
|
||||||
return
|
|
||||||
|
|
||||||
reason = ""
|
|
||||||
if ':' in args:
|
|
||||||
args, reason = [arg.strip() for arg in args.split(':', 1)]
|
|
||||||
|
|
||||||
# We use account_search since we want to be sure to find also accounts
|
|
||||||
# that lack characters.
|
|
||||||
accounts = search.account_search(args)
|
|
||||||
|
|
||||||
if not accounts:
|
|
||||||
self.msg('Could not find an account by that name.')
|
|
||||||
return
|
|
||||||
|
|
||||||
if len(accounts) > 1:
|
|
||||||
string = "There were multiple matches:\n"
|
|
||||||
string += "\n".join(" %s %s" % (account.id, account.key) for account in accounts)
|
|
||||||
self.msg(string)
|
|
||||||
return
|
|
||||||
|
|
||||||
# one single match
|
|
||||||
|
|
||||||
account = accounts.first()
|
|
||||||
|
|
||||||
if not account.access(caller, 'delete'):
|
|
||||||
string = "You don't have the permissions to delete that account."
|
|
||||||
self.msg(string)
|
|
||||||
return
|
|
||||||
|
|
||||||
uname = account.username
|
|
||||||
# boot the account then delete
|
|
||||||
self.msg("Informing and disconnecting account ...")
|
|
||||||
string = "\nYour account '%s' is being *permanently* deleted.\n" % uname
|
|
||||||
if reason:
|
|
||||||
string += " Reason given:\n '%s'" % reason
|
|
||||||
account.msg(string)
|
|
||||||
account.delete()
|
|
||||||
self.msg("Account %s was successfully deleted." % uname)
|
|
||||||
|
|
||||||
|
|
||||||
class CmdEmit(COMMAND_DEFAULT_CLASS):
|
class CmdEmit(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
@ -445,6 +380,7 @@ class CmdNewPassword(COMMAND_DEFAULT_CLASS):
|
||||||
if account.character != caller:
|
if account.character != caller:
|
||||||
account.msg("%s has changed your password to '%s'." % (caller.name,
|
account.msg("%s has changed your password to '%s'." % (caller.name,
|
||||||
newpass))
|
newpass))
|
||||||
|
logger.log_sec('Password Changed: %s (Caller: %s, IP: %s).' % (account, caller, self.session.address))
|
||||||
|
|
||||||
|
|
||||||
class CmdPerm(COMMAND_DEFAULT_CLASS):
|
class CmdPerm(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
@ -526,6 +462,7 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
|
||||||
else:
|
else:
|
||||||
caller_result.append("\nPermission %s removed from %s (if they existed)." % (perm, obj.name))
|
caller_result.append("\nPermission %s removed from %s (if they existed)." % (perm, obj.name))
|
||||||
target_result.append("\n%s revokes the permission(s) %s from you." % (caller.name, perm))
|
target_result.append("\n%s revokes the permission(s) %s from you." % (caller.name, perm))
|
||||||
|
logger.log_sec('Permissions Deleted: %s, %s (Caller: %s, IP: %s).' % (perm, obj, caller, self.session.address))
|
||||||
else:
|
else:
|
||||||
# add a new permission
|
# add a new permission
|
||||||
permissions = obj.permissions.all()
|
permissions = obj.permissions.all()
|
||||||
|
|
@ -547,6 +484,8 @@ class CmdPerm(COMMAND_DEFAULT_CLASS):
|
||||||
caller_result.append("\nPermission '%s' given to %s (%s)." % (perm, obj.name, plystring))
|
caller_result.append("\nPermission '%s' given to %s (%s)." % (perm, obj.name, plystring))
|
||||||
target_result.append("\n%s gives you (%s, %s) the permission '%s'."
|
target_result.append("\n%s gives you (%s, %s) the permission '%s'."
|
||||||
% (caller.name, obj.name, plystring, perm))
|
% (caller.name, obj.name, plystring, perm))
|
||||||
|
logger.log_sec('Permissions Added: %s, %s (Caller: %s, IP: %s).' % (obj, perm, caller, self.session.address))
|
||||||
|
|
||||||
caller.msg("".join(caller_result).strip())
|
caller.msg("".join(caller_result).strip())
|
||||||
if target_result:
|
if target_result:
|
||||||
obj.msg("".join(target_result).strip())
|
obj.msg("".join(target_result).strip())
|
||||||
|
|
@ -574,3 +513,33 @@ class CmdWall(COMMAND_DEFAULT_CLASS):
|
||||||
message = "%s shouts \"%s\"" % (self.caller.name, self.args)
|
message = "%s shouts \"%s\"" % (self.caller.name, self.args)
|
||||||
self.msg("Announcing to all connected sessions ...")
|
self.msg("Announcing to all connected sessions ...")
|
||||||
SESSIONS.announce_all(message)
|
SESSIONS.announce_all(message)
|
||||||
|
|
||||||
|
|
||||||
|
class CmdForce(COMMAND_DEFAULT_CLASS):
|
||||||
|
"""
|
||||||
|
forces an object to execute a command
|
||||||
|
|
||||||
|
Usage:
|
||||||
|
@force <object>=<command string>
|
||||||
|
|
||||||
|
Example:
|
||||||
|
@force bob=get stick
|
||||||
|
"""
|
||||||
|
key = "@force"
|
||||||
|
locks = "cmd:perm(spawn) or perm(Builder)"
|
||||||
|
help_category = "Building"
|
||||||
|
perm_used = "edit"
|
||||||
|
|
||||||
|
def func(self):
|
||||||
|
"""Implements the force command"""
|
||||||
|
if not self.lhs or not self.rhs:
|
||||||
|
self.caller.msg("You must provide a target and a command string to execute.")
|
||||||
|
return
|
||||||
|
targ = self.caller.search(self.lhs)
|
||||||
|
if not targ:
|
||||||
|
return
|
||||||
|
if not targ.access(self.caller, self.perm_used):
|
||||||
|
self.caller.msg("You don't have permission to force them to execute commands.")
|
||||||
|
return
|
||||||
|
targ.execute_cmd(self.rhs)
|
||||||
|
self.caller.msg("You have forced %s to: %s" % (targ, self.rhs))
|
||||||
|
|
|
||||||
|
|
@ -10,7 +10,7 @@ from evennia.objects.models import ObjectDB
|
||||||
from evennia.locks.lockhandler import LockException
|
from evennia.locks.lockhandler import LockException
|
||||||
from evennia.commands.cmdhandler import get_and_merge_cmdsets
|
from evennia.commands.cmdhandler import get_and_merge_cmdsets
|
||||||
from evennia.utils import create, utils, search
|
from evennia.utils import create, utils, search
|
||||||
from evennia.utils.utils import inherits_from, class_from_module, get_all_typeclasses
|
from evennia.utils.utils import inherits_from, class_from_module, get_all_typeclasses, variable_from_module
|
||||||
from evennia.utils.eveditor import EvEditor
|
from evennia.utils.eveditor import EvEditor
|
||||||
from evennia.utils.evmore import EvMore
|
from evennia.utils.evmore import EvMore
|
||||||
from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus
|
from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus
|
||||||
|
|
@ -612,12 +612,12 @@ class CmdDesc(COMMAND_DEFAULT_CLASS):
|
||||||
self.edit_handler()
|
self.edit_handler()
|
||||||
return
|
return
|
||||||
|
|
||||||
if self.rhs:
|
if '=' in self.args:
|
||||||
# We have an =
|
# We have an =
|
||||||
obj = caller.search(self.lhs)
|
obj = caller.search(self.lhs)
|
||||||
if not obj:
|
if not obj:
|
||||||
return
|
return
|
||||||
desc = self.rhs
|
desc = self.rhs or ''
|
||||||
else:
|
else:
|
||||||
obj = caller.location or self.msg("|rYou can't describe oblivion.|n")
|
obj = caller.location or self.msg("|rYou can't describe oblivion.|n")
|
||||||
if not obj:
|
if not obj:
|
||||||
|
|
@ -737,12 +737,11 @@ class CmdDestroy(COMMAND_DEFAULT_CLASS):
|
||||||
confirm += ", ".join(["#{}".format(obj.id) for obj in objs])
|
confirm += ", ".join(["#{}".format(obj.id) for obj in objs])
|
||||||
confirm += " [yes]/no?" if self.default_confirm == 'yes' else " yes/[no]"
|
confirm += " [yes]/no?" if self.default_confirm == 'yes' else " yes/[no]"
|
||||||
answer = ""
|
answer = ""
|
||||||
while answer.strip().lower() not in ("y", "yes", "n", "no"):
|
answer = yield(confirm)
|
||||||
answer = yield(confirm)
|
answer = self.default_confirm if answer == '' else answer
|
||||||
answer = self.default_confirm if answer == '' else answer
|
|
||||||
|
|
||||||
if answer.strip().lower() in ("n", "no"):
|
if answer.strip().lower() in ("n", "no"):
|
||||||
caller.msg("Cancelled: no object was destroyed.")
|
caller.msg("Canceled: no object was destroyed.")
|
||||||
delete = False
|
delete = False
|
||||||
|
|
||||||
if delete:
|
if delete:
|
||||||
|
|
@ -1023,10 +1022,17 @@ class CmdLink(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
object_name = self.lhs
|
object_name = self.lhs
|
||||||
|
|
||||||
# get object
|
# try to search locally first
|
||||||
obj = caller.search(object_name, global_search=True)
|
results = caller.search(object_name, quiet=True)
|
||||||
if not obj:
|
if len(results) > 1: # local results was a multimatch. Inform them to be more specific
|
||||||
return
|
_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
|
||||||
|
return _AT_SEARCH_RESULT(results, caller, query=object_name)
|
||||||
|
elif len(results) == 1: # A unique local match
|
||||||
|
obj = results[0]
|
||||||
|
else: # No matches. Search globally
|
||||||
|
obj = caller.search(object_name, global_search=True)
|
||||||
|
if not obj:
|
||||||
|
return
|
||||||
|
|
||||||
if self.rhs:
|
if self.rhs:
|
||||||
# this means a target name was given
|
# this means a target name was given
|
||||||
|
|
@ -2856,7 +2862,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
key = "@spawn"
|
key = "@spawn"
|
||||||
aliases = ["olc"]
|
aliases = ["olc"]
|
||||||
switch_options = ("noloc", "search", "list", "show", "save", "delete", "menu", "olc", "update")
|
switch_options = ("noloc", "search", "list", "show", "examine", "save", "delete", "menu", "olc", "update", "edit")
|
||||||
locks = "cmd:perm(spawn) or perm(Builder)"
|
locks = "cmd:perm(spawn) or perm(Builder)"
|
||||||
help_category = "Building"
|
help_category = "Building"
|
||||||
|
|
||||||
|
|
@ -2889,7 +2895,8 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||||
"use the 'exec' prototype key.")
|
"use the 'exec' prototype key.")
|
||||||
return None
|
return None
|
||||||
try:
|
try:
|
||||||
protlib.validate_prototype(prototype)
|
# we homogenize first, to be more lenient
|
||||||
|
protlib.validate_prototype(protlib.homogenize_prototype(prototype))
|
||||||
except RuntimeError as err:
|
except RuntimeError as err:
|
||||||
self.caller.msg(str(err))
|
self.caller.msg(str(err))
|
||||||
return
|
return
|
||||||
|
|
@ -2906,12 +2913,13 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
caller = self.caller
|
caller = self.caller
|
||||||
|
|
||||||
if self.cmdstring == "olc" or 'menu' in self.switches or 'olc' in self.switches:
|
if self.cmdstring == "olc" or 'menu' in self.switches \
|
||||||
|
or 'olc' in self.switches or 'edit' in self.switches:
|
||||||
# OLC menu mode
|
# OLC menu mode
|
||||||
prototype = None
|
prototype = None
|
||||||
if self.lhs:
|
if self.lhs:
|
||||||
key = self.lhs
|
key = self.lhs
|
||||||
prototype = spawner.search_prototype(key=key, return_meta=True)
|
prototype = protlib.search_prototype(key=key)
|
||||||
if len(prototype) > 1:
|
if len(prototype) > 1:
|
||||||
caller.msg("More than one match for {}:\n{}".format(
|
caller.msg("More than one match for {}:\n{}".format(
|
||||||
key, "\n".join(proto.get('prototype_key', '') for proto in prototype)))
|
key, "\n".join(proto.get('prototype_key', '') for proto in prototype)))
|
||||||
|
|
@ -2919,6 +2927,10 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||||
elif prototype:
|
elif prototype:
|
||||||
# one match
|
# one match
|
||||||
prototype = prototype[0]
|
prototype = prototype[0]
|
||||||
|
else:
|
||||||
|
# no match
|
||||||
|
caller.msg("No prototype '{}' was found.".format(key))
|
||||||
|
return
|
||||||
olc_menus.start_olc(caller, session=self.session, prototype=prototype)
|
olc_menus.start_olc(caller, session=self.session, prototype=prototype)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,6 @@ class AccountCmdSet(CmdSet):
|
||||||
self.add(system.CmdPy())
|
self.add(system.CmdPy())
|
||||||
|
|
||||||
# Admin commands
|
# Admin commands
|
||||||
self.add(admin.CmdDelAccount())
|
|
||||||
self.add(admin.CmdNewPassword())
|
self.add(admin.CmdNewPassword())
|
||||||
|
|
||||||
# Comm commands
|
# Comm commands
|
||||||
|
|
|
||||||
|
|
@ -57,6 +57,7 @@ class CharacterCmdSet(CmdSet):
|
||||||
self.add(admin.CmdEmit())
|
self.add(admin.CmdEmit())
|
||||||
self.add(admin.CmdPerm())
|
self.add(admin.CmdPerm())
|
||||||
self.add(admin.CmdWall())
|
self.add(admin.CmdWall())
|
||||||
|
self.add(admin.CmdForce())
|
||||||
|
|
||||||
# Building and world manipulation
|
# Building and world manipulation
|
||||||
self.add(building.CmdTeleport())
|
self.add(building.CmdTeleport())
|
||||||
|
|
|
||||||
|
|
@ -7,6 +7,8 @@ make sure to homogenize self.caller to always be the account object
|
||||||
for easy handling.
|
for easy handling.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
from past.builtins import cmp
|
from past.builtins import cmp
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from evennia.comms.models import ChannelDB, Msg
|
from evennia.comms.models import ChannelDB, Msg
|
||||||
|
|
@ -14,7 +16,7 @@ from evennia.accounts.models import AccountDB
|
||||||
from evennia.accounts import bots
|
from evennia.accounts import bots
|
||||||
from evennia.comms.channelhandler import CHANNELHANDLER
|
from evennia.comms.channelhandler import CHANNELHANDLER
|
||||||
from evennia.locks.lockhandler import LockException
|
from evennia.locks.lockhandler import LockException
|
||||||
from evennia.utils import create, utils, evtable
|
from evennia.utils import create, logger, utils, evtable
|
||||||
from evennia.utils.utils import make_iter, class_from_module
|
from evennia.utils.utils import make_iter, class_from_module
|
||||||
|
|
||||||
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||||
|
|
@ -368,6 +370,7 @@ class CmdCdestroy(COMMAND_DEFAULT_CLASS):
|
||||||
channel.delete()
|
channel.delete()
|
||||||
CHANNELHANDLER.update()
|
CHANNELHANDLER.update()
|
||||||
self.msg("Channel '%s' was destroyed." % channel_key)
|
self.msg("Channel '%s' was destroyed." % channel_key)
|
||||||
|
logger.log_sec('Channel Deleted: %s (Caller: %s, IP: %s).' % (channel_key, caller, self.session.address))
|
||||||
|
|
||||||
|
|
||||||
class CmdCBoot(COMMAND_DEFAULT_CLASS):
|
class CmdCBoot(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
@ -433,6 +436,8 @@ class CmdCBoot(COMMAND_DEFAULT_CLASS):
|
||||||
# disconnect account
|
# disconnect account
|
||||||
channel.disconnect(account)
|
channel.disconnect(account)
|
||||||
CHANNELHANDLER.update()
|
CHANNELHANDLER.update()
|
||||||
|
logger.log_sec('Channel Boot: %s (Channel: %s, Reason: %s, Caller: %s, IP: %s).' % (
|
||||||
|
account, channel, reason, self.caller, self.session.address))
|
||||||
|
|
||||||
|
|
||||||
class CmdCemit(COMMAND_DEFAULT_CLASS):
|
class CmdCemit(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
@ -918,8 +923,9 @@ class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
|
||||||
self.msg("Account '%s' already exists and is not a bot." % botname)
|
self.msg("Account '%s' already exists and is not a bot." % botname)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
|
password = hashlib.md5(str(time.time())).hexdigest()[:11]
|
||||||
try:
|
try:
|
||||||
bot = create.create_account(botname, None, None, typeclass=botclass)
|
bot = create.create_account(botname, None, password, typeclass=botclass)
|
||||||
except Exception as err:
|
except Exception as err:
|
||||||
self.msg("|rError, could not create the bot:|n '%s'." % err)
|
self.msg("|rError, could not create the bot:|n '%s'." % err)
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@ from evennia.server.sessionhandler import SESSIONS
|
||||||
from evennia.scripts.models import ScriptDB
|
from evennia.scripts.models import ScriptDB
|
||||||
from evennia.objects.models import ObjectDB
|
from evennia.objects.models import ObjectDB
|
||||||
from evennia.accounts.models import AccountDB
|
from evennia.accounts.models import AccountDB
|
||||||
from evennia.utils import logger, utils, gametime, create
|
from evennia.utils import logger, utils, gametime, create, search
|
||||||
from evennia.utils.eveditor import EvEditor
|
from evennia.utils.eveditor import EvEditor
|
||||||
from evennia.utils.evtable import EvTable
|
from evennia.utils.evtable import EvTable
|
||||||
from evennia.utils.utils import crop, class_from_module
|
from evennia.utils.utils import crop, class_from_module
|
||||||
|
|
@ -460,17 +460,22 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
||||||
class CmdAccounts(COMMAND_DEFAULT_CLASS):
|
class CmdAccounts(COMMAND_DEFAULT_CLASS):
|
||||||
"""
|
"""
|
||||||
list all registered accounts
|
Manage registered accounts
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
@accounts [nr]
|
@accounts [nr]
|
||||||
|
@accounts/delete <name or #id> [: reason]
|
||||||
|
|
||||||
Lists statistics about the Accounts registered with the game.
|
Switches:
|
||||||
|
delete - delete an account from the server
|
||||||
|
|
||||||
|
By default, lists statistics about the Accounts registered with the game.
|
||||||
It will list the <nr> amount of latest registered accounts
|
It will list the <nr> amount of latest registered accounts
|
||||||
If not given, <nr> defaults to 10.
|
If not given, <nr> defaults to 10.
|
||||||
"""
|
"""
|
||||||
key = "@accounts"
|
key = "@accounts"
|
||||||
aliases = ["@listaccounts"]
|
aliases = ["@account", "@listaccounts"]
|
||||||
|
switch_options = ("delete", )
|
||||||
locks = "cmd:perm(listaccounts) or perm(Admin)"
|
locks = "cmd:perm(listaccounts) or perm(Admin)"
|
||||||
help_category = "System"
|
help_category = "System"
|
||||||
|
|
||||||
|
|
@ -478,6 +483,56 @@ class CmdAccounts(COMMAND_DEFAULT_CLASS):
|
||||||
"""List the accounts"""
|
"""List the accounts"""
|
||||||
|
|
||||||
caller = self.caller
|
caller = self.caller
|
||||||
|
args = self.args
|
||||||
|
|
||||||
|
if "delete" in self.switches:
|
||||||
|
account = getattr(caller, "account")
|
||||||
|
if not account or not account.check_permstring("Developer"):
|
||||||
|
caller.msg("You are not allowed to delete accounts.")
|
||||||
|
return
|
||||||
|
if not args:
|
||||||
|
caller.msg("Usage: @accounts/delete <name or #id> [: reason]")
|
||||||
|
return
|
||||||
|
reason = ""
|
||||||
|
if ":" in args:
|
||||||
|
args, reason = [arg.strip() for arg in args.split(":", 1)]
|
||||||
|
# We use account_search since we want to be sure to find also accounts
|
||||||
|
# that lack characters.
|
||||||
|
accounts = search.account_search(args)
|
||||||
|
if not accounts:
|
||||||
|
self.msg("Could not find an account by that name.")
|
||||||
|
return
|
||||||
|
if len(accounts) > 1:
|
||||||
|
string = "There were multiple matches:\n"
|
||||||
|
string += "\n".join(" %s %s" % (account.id, account.key) for account in accounts)
|
||||||
|
self.msg(string)
|
||||||
|
return
|
||||||
|
account = accounts.first()
|
||||||
|
if not account.access(caller, "delete"):
|
||||||
|
self.msg("You don't have the permissions to delete that account.")
|
||||||
|
return
|
||||||
|
username = account.username
|
||||||
|
# ask for confirmation
|
||||||
|
confirm = ("It is often better to block access to an account rather than to delete it. "
|
||||||
|
"|yAre you sure you want to permanently delete "
|
||||||
|
"account '|n{}|y'|n yes/[no]?".format(username))
|
||||||
|
answer = yield(confirm)
|
||||||
|
if answer.lower() not in ('y', 'yes'):
|
||||||
|
caller.msg("Canceled deletion.")
|
||||||
|
return
|
||||||
|
|
||||||
|
# Boot the account then delete it.
|
||||||
|
self.msg("Informing and disconnecting account ...")
|
||||||
|
string = "\nYour account '%s' is being *permanently* deleted.\n" % username
|
||||||
|
if reason:
|
||||||
|
string += " Reason given:\n '%s'" % reason
|
||||||
|
account.msg(string)
|
||||||
|
logger.log_sec("Account Deleted: %s (Reason: %s, Caller: %s, IP: %s)." % (account, reason, caller, self.session.address))
|
||||||
|
account.delete()
|
||||||
|
self.msg("Account %s was successfully deleted." % username)
|
||||||
|
return
|
||||||
|
|
||||||
|
# No switches, default to displaying a list of accounts.
|
||||||
if self.args and self.args.isdigit():
|
if self.args and self.args.isdigit():
|
||||||
nlim = int(self.args)
|
nlim = int(self.args)
|
||||||
else:
|
else:
|
||||||
|
|
@ -655,6 +710,7 @@ class CmdTime(COMMAND_DEFAULT_CLASS):
|
||||||
"""Show server time data in a table."""
|
"""Show server time data in a table."""
|
||||||
table1 = EvTable("|wServer time", "", align="l", width=78)
|
table1 = EvTable("|wServer time", "", align="l", width=78)
|
||||||
table1.add_row("Current uptime", utils.time_format(gametime.uptime(), 3))
|
table1.add_row("Current uptime", utils.time_format(gametime.uptime(), 3))
|
||||||
|
table1.add_row("Portal uptime", utils.time_format(gametime.portal_uptime(), 3))
|
||||||
table1.add_row("Total runtime", utils.time_format(gametime.runtime(), 2))
|
table1.add_row("Total runtime", utils.time_format(gametime.runtime(), 2))
|
||||||
table1.add_row("First start", datetime.datetime.fromtimestamp(gametime.server_epoch()))
|
table1.add_row("First start", datetime.datetime.fromtimestamp(gametime.server_epoch()))
|
||||||
table1.add_row("Current time", datetime.datetime.now())
|
table1.add_row("Current time", datetime.datetime.now())
|
||||||
|
|
|
||||||
|
|
@ -243,6 +243,9 @@ class TestAdmin(CommandTest):
|
||||||
def test_ban(self):
|
def test_ban(self):
|
||||||
self.call(admin.CmdBan(), "Char", "Name-Ban char was added.")
|
self.call(admin.CmdBan(), "Char", "Name-Ban char was added.")
|
||||||
|
|
||||||
|
def test_force(self):
|
||||||
|
self.call(admin.CmdForce(), "Char2=say test", 'Char2(#7) says, "test"|You have forced Char2 to: say test')
|
||||||
|
|
||||||
|
|
||||||
class TestAccount(CommandTest):
|
class TestAccount(CommandTest):
|
||||||
|
|
||||||
|
|
@ -315,6 +318,24 @@ class TestBuilding(CommandTest):
|
||||||
def test_desc(self):
|
def test_desc(self):
|
||||||
self.call(building.CmdDesc(), "Obj2=TestDesc", "The description was set on Obj2(#5).")
|
self.call(building.CmdDesc(), "Obj2=TestDesc", "The description was set on Obj2(#5).")
|
||||||
|
|
||||||
|
def test_empty_desc(self):
|
||||||
|
"""
|
||||||
|
empty desc sets desc as ''
|
||||||
|
"""
|
||||||
|
o2d = self.obj2.db.desc
|
||||||
|
r1d = self.room1.db.desc
|
||||||
|
self.call(building.CmdDesc(), "Obj2=", "The description was set on Obj2(#5).")
|
||||||
|
assert self.obj2.db.desc == '' and self.obj2.db.desc != o2d
|
||||||
|
assert self.room1.db.desc == r1d
|
||||||
|
|
||||||
|
def test_desc_default_to_room(self):
|
||||||
|
"""no rhs changes room's desc"""
|
||||||
|
o2d = self.obj2.db.desc
|
||||||
|
r1d = self.room1.db.desc
|
||||||
|
self.call(building.CmdDesc(), "Obj2", "The description was set on Room(#1).")
|
||||||
|
assert self.obj2.db.desc == o2d
|
||||||
|
assert self.room1.db.desc == 'Obj2' and self.room1.db.desc != r1d
|
||||||
|
|
||||||
def test_wipe(self):
|
def test_wipe(self):
|
||||||
confirm = building.CmdDestroy.confirm
|
confirm = building.CmdDestroy.confirm
|
||||||
building.CmdDestroy.confirm = False
|
building.CmdDestroy.confirm = False
|
||||||
|
|
@ -334,6 +355,14 @@ class TestBuilding(CommandTest):
|
||||||
self.call(building.CmdOpen(), "TestExit1=Room2", "Created new Exit 'TestExit1' from Room to Room2")
|
self.call(building.CmdOpen(), "TestExit1=Room2", "Created new Exit 'TestExit1' from Room to Room2")
|
||||||
self.call(building.CmdLink(), "TestExit1=Room", "Link created TestExit1 -> Room (one way).")
|
self.call(building.CmdLink(), "TestExit1=Room", "Link created TestExit1 -> Room (one way).")
|
||||||
self.call(building.CmdUnLink(), "TestExit1", "Former exit TestExit1 no longer links anywhere.")
|
self.call(building.CmdUnLink(), "TestExit1", "Former exit TestExit1 no longer links anywhere.")
|
||||||
|
self.char1.location = self.room2
|
||||||
|
self.call(building.CmdOpen(), "TestExit2=Room", "Created new Exit 'TestExit2' from Room2 to Room.")
|
||||||
|
# ensure it matches locally first
|
||||||
|
self.call(building.CmdLink(), "TestExit=Room2", "Link created TestExit2 -> Room2 (one way).")
|
||||||
|
# ensure can still match globally when not a local name
|
||||||
|
self.call(building.CmdLink(), "TestExit1=Room2", "Note: TestExit1(#8) did not have a destination set before. "
|
||||||
|
"Make sure you linked the right thing.\n"
|
||||||
|
"Link created TestExit1 -> Room2 (one way).")
|
||||||
|
|
||||||
def test_set_home(self):
|
def test_set_home(self):
|
||||||
self.call(building.CmdSetHome(), "Obj = Room2", "Obj's home location was changed from Room")
|
self.call(building.CmdSetHome(), "Obj = Room2", "Obj's home location was changed from Room")
|
||||||
|
|
@ -460,6 +489,61 @@ class TestBuilding(CommandTest):
|
||||||
# Test listing commands
|
# Test listing commands
|
||||||
self.call(building.CmdSpawn(), "/list", "Key ")
|
self.call(building.CmdSpawn(), "/list", "Key ")
|
||||||
|
|
||||||
|
# @spawn/edit (missing prototype)
|
||||||
|
# brings up olc menu
|
||||||
|
msg = self.call(
|
||||||
|
building.CmdSpawn(),
|
||||||
|
'/edit')
|
||||||
|
assert 'Prototype wizard' in msg
|
||||||
|
|
||||||
|
# @spawn/edit with valid prototype
|
||||||
|
# brings up olc menu loaded with prototype
|
||||||
|
msg = self.call(
|
||||||
|
building.CmdSpawn(),
|
||||||
|
'/edit testball')
|
||||||
|
assert 'Prototype wizard' in msg
|
||||||
|
assert hasattr(self.char1.ndb._menutree, "olc_prototype")
|
||||||
|
assert dict == type(self.char1.ndb._menutree.olc_prototype) \
|
||||||
|
and 'prototype_key' in self.char1.ndb._menutree.olc_prototype \
|
||||||
|
and 'key' in self.char1.ndb._menutree.olc_prototype \
|
||||||
|
and 'testball' == self.char1.ndb._menutree.olc_prototype['prototype_key'] \
|
||||||
|
and 'Ball' == self.char1.ndb._menutree.olc_prototype['key']
|
||||||
|
assert 'Ball' in msg and 'testball' in msg
|
||||||
|
|
||||||
|
# @spawn/edit with valid prototype (synomym)
|
||||||
|
msg = self.call(
|
||||||
|
building.CmdSpawn(),
|
||||||
|
'/edit BALL')
|
||||||
|
assert 'Prototype wizard' in msg
|
||||||
|
assert 'Ball' in msg and 'testball' in msg
|
||||||
|
|
||||||
|
# @spawn/edit with invalid prototype
|
||||||
|
msg = self.call(
|
||||||
|
building.CmdSpawn(),
|
||||||
|
'/edit NO_EXISTS',
|
||||||
|
"No prototype 'NO_EXISTS' was found.")
|
||||||
|
|
||||||
|
# @spawn/examine (missing prototype)
|
||||||
|
# lists all prototypes that exist
|
||||||
|
msg = self.call(
|
||||||
|
building.CmdSpawn(),
|
||||||
|
'/examine')
|
||||||
|
assert 'testball' in msg and 'testprot' in msg
|
||||||
|
|
||||||
|
# @spawn/examine with valid prototype
|
||||||
|
# prints the prototype
|
||||||
|
msg = self.call(
|
||||||
|
building.CmdSpawn(),
|
||||||
|
'/examine BALL')
|
||||||
|
assert 'Ball' in msg and 'testball' in msg
|
||||||
|
|
||||||
|
# @spawn/examine with invalid prototype
|
||||||
|
# shows error
|
||||||
|
self.call(
|
||||||
|
building.CmdSpawn(),
|
||||||
|
'/examine NO_EXISTS',
|
||||||
|
"No prototype 'NO_EXISTS' was found.")
|
||||||
|
|
||||||
|
|
||||||
class TestComms(CommandTest):
|
class TestComms(CommandTest):
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,19 +2,13 @@
|
||||||
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 random import getrandbits
|
|
||||||
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.server.models import ServerConfig
|
|
||||||
from evennia.server.throttle import Throttle
|
|
||||||
from evennia.comms.models import ChannelDB
|
from evennia.comms.models import ChannelDB
|
||||||
from evennia.server.sessionhandler import SESSIONS
|
from evennia.server.sessionhandler import SESSIONS
|
||||||
|
|
||||||
from evennia.utils import 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
|
||||||
|
|
||||||
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||||
|
|
@ -26,11 +20,6 @@ __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
|
||||||
|
|
||||||
# Create throttles for too many connections, account-creations and login attempts
|
|
||||||
CONNECTION_THROTTLE = Throttle(limit=5, timeout=1 * 60)
|
|
||||||
CREATION_THROTTLE = Throttle(limit=2, timeout=10 * 60)
|
|
||||||
LOGIN_THROTTLE = Throttle(limit=5, timeout=5 * 60)
|
|
||||||
|
|
||||||
|
|
||||||
def create_guest_account(session):
|
def create_guest_account(session):
|
||||||
"""
|
"""
|
||||||
|
|
@ -44,49 +33,20 @@ def create_guest_account(session):
|
||||||
the boolean is whether guest accounts are enabled at all.
|
the boolean is whether guest accounts are enabled at all.
|
||||||
the Account which was created from an available guest name.
|
the Account which was created from an available guest name.
|
||||||
"""
|
"""
|
||||||
# check if guests are enabled.
|
enabled = settings.GUEST_ENABLED
|
||||||
if not settings.GUEST_ENABLED:
|
address = session.address
|
||||||
return False, None
|
|
||||||
|
|
||||||
# Check IP bans.
|
# Get account class
|
||||||
bans = ServerConfig.objects.conf("server_bans")
|
Guest = class_from_module(settings.BASE_GUEST_TYPECLASS)
|
||||||
if bans and any(tup[2].match(session.address) for tup in bans if tup[2]):
|
|
||||||
# this is a banned IP!
|
|
||||||
string = "|rYou have been banned and cannot continue from here." \
|
|
||||||
"\nIf you feel this ban is in error, please email an admin.|x"
|
|
||||||
session.msg(string)
|
|
||||||
session.sessionhandler.disconnect(session, "Good bye! Disconnecting.")
|
|
||||||
return True, None
|
|
||||||
|
|
||||||
try:
|
# Get an available guest account
|
||||||
# Find an available guest name.
|
# authenticate() handles its own throttling
|
||||||
accountname = None
|
account, errors = Guest.authenticate(ip=address)
|
||||||
for name in settings.GUEST_LIST:
|
if account:
|
||||||
if not AccountDB.objects.filter(username__iexact=accountname).count():
|
return enabled, account
|
||||||
accountname = name
|
else:
|
||||||
break
|
session.msg("|R%s|n" % '\n'.join(errors))
|
||||||
if not accountname:
|
return enabled, None
|
||||||
session.msg("All guest accounts are in use. Please try again later.")
|
|
||||||
return True, None
|
|
||||||
else:
|
|
||||||
# build a new account with the found guest accountname
|
|
||||||
password = "%016x" % getrandbits(64)
|
|
||||||
home = ObjectDB.objects.get_id(settings.GUEST_HOME)
|
|
||||||
permissions = settings.PERMISSION_GUEST_DEFAULT
|
|
||||||
typeclass = settings.BASE_CHARACTER_TYPECLASS
|
|
||||||
ptypeclass = settings.BASE_GUEST_TYPECLASS
|
|
||||||
new_account = _create_account(session, accountname, password, permissions, ptypeclass)
|
|
||||||
if new_account:
|
|
||||||
_create_character(session, new_account, typeclass, home, permissions)
|
|
||||||
return True, new_account
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
# We are in the middle between logged in and -not, so we have
|
|
||||||
# to handle tracebacks ourselves at this point. If we don't,
|
|
||||||
# we won't see any errors at all.
|
|
||||||
session.msg("An error occurred. Please e-mail an admin if the problem persists.")
|
|
||||||
logger.log_trace()
|
|
||||||
raise
|
|
||||||
|
|
||||||
|
|
||||||
def create_normal_account(session, name, password):
|
def create_normal_account(session, name, password):
|
||||||
|
|
@ -101,38 +61,17 @@ def create_normal_account(session, name, password):
|
||||||
Returns:
|
Returns:
|
||||||
account (Account): the account which was created from the name and password.
|
account (Account): the account which was created from the name and password.
|
||||||
"""
|
"""
|
||||||
# check for too many login errors too quick.
|
# Get account class
|
||||||
address = session.address
|
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
|
||||||
if isinstance(address, tuple):
|
|
||||||
address = address[0]
|
|
||||||
|
|
||||||
if LOGIN_THROTTLE.check(address):
|
address = session.address
|
||||||
session.msg("|RYou made too many connection attempts. Try again in a few minutes.|n")
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Match account name and check password
|
# Match account name and check password
|
||||||
account = authenticate(username=name, password=password)
|
# authenticate() handles all its own throttling
|
||||||
|
account, errors = Account.authenticate(username=name, password=password, ip=address, session=session)
|
||||||
if not account:
|
if not account:
|
||||||
# No accountname or password match
|
# No accountname or password match
|
||||||
session.msg("Incorrect login information given.")
|
session.msg("|R%s|n" % '\n'.join(errors))
|
||||||
# this just updates the throttle
|
|
||||||
LOGIN_THROTTLE.update(address)
|
|
||||||
# calls account hook for a failed login if possible.
|
|
||||||
account = AccountDB.objects.get_account_from_name(name)
|
|
||||||
if account:
|
|
||||||
account.at_failed_login(session)
|
|
||||||
return None
|
|
||||||
|
|
||||||
# Check IP and/or name bans
|
|
||||||
bans = ServerConfig.objects.conf("server_bans")
|
|
||||||
if bans and (any(tup[0] == account.name.lower() for tup in bans) or
|
|
||||||
any(tup[2].match(session.address) for tup in bans if tup[2])):
|
|
||||||
# this is a banned IP or name!
|
|
||||||
string = "|rYou have been banned and cannot continue from here." \
|
|
||||||
"\nIf you feel this ban is in error, please email an admin.|x"
|
|
||||||
session.msg(string)
|
|
||||||
session.sessionhandler.disconnect(session, "Good bye! Disconnecting.")
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return account
|
return account
|
||||||
|
|
@ -164,15 +103,7 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS):
|
||||||
there is no object yet before the account has logged in)
|
there is no object yet before the account has logged in)
|
||||||
"""
|
"""
|
||||||
session = self.caller
|
session = self.caller
|
||||||
|
|
||||||
# check for too many login errors too quick.
|
|
||||||
address = session.address
|
address = session.address
|
||||||
if isinstance(address, tuple):
|
|
||||||
address = address[0]
|
|
||||||
if CONNECTION_THROTTLE.check(address):
|
|
||||||
# timeout is 5 minutes.
|
|
||||||
session.msg("|RYou made too many connection attempts. Try again in a few minutes.|n")
|
|
||||||
return
|
|
||||||
|
|
||||||
args = self.args
|
args = self.args
|
||||||
# extract double quote parts
|
# extract double quote parts
|
||||||
|
|
@ -180,23 +111,33 @@ class CmdUnconnectedConnect(COMMAND_DEFAULT_CLASS):
|
||||||
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":
|
||||||
enabled, new_account = create_guest_account(session)
|
# Get Guest typeclass
|
||||||
if new_account:
|
Guest = class_from_module(settings.BASE_GUEST_TYPECLASS)
|
||||||
session.sessionhandler.login(session, new_account)
|
|
||||||
if enabled:
|
account, errors = Guest.authenticate(ip=address)
|
||||||
|
if account:
|
||||||
|
session.sessionhandler.login(session, account)
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
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
|
||||||
|
|
||||||
CONNECTION_THROTTLE.update(address)
|
# Get account class
|
||||||
|
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
|
||||||
|
|
||||||
name, password = parts
|
name, password = parts
|
||||||
account = create_normal_account(session, name, password)
|
account, errors = Account.authenticate(username=name, password=password, ip=address, session=session)
|
||||||
if account:
|
if account:
|
||||||
session.sessionhandler.login(session, account)
|
session.sessionhandler.login(session, account)
|
||||||
|
else:
|
||||||
|
session.msg("|R%s|n" % '\n'.join(errors))
|
||||||
|
|
||||||
|
|
||||||
class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
|
class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
@ -222,14 +163,10 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
|
||||||
session = self.caller
|
session = self.caller
|
||||||
args = self.args.strip()
|
args = self.args.strip()
|
||||||
|
|
||||||
# Rate-limit account creation.
|
|
||||||
address = session.address
|
address = session.address
|
||||||
|
|
||||||
if isinstance(address, tuple):
|
# Get account class
|
||||||
address = address[0]
|
Account = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
|
||||||
if CREATION_THROTTLE.check(address):
|
|
||||||
session.msg("|RYou are creating too many accounts. Try again in a few minutes.|n")
|
|
||||||
return
|
|
||||||
|
|
||||||
# extract double quoted parts
|
# extract double quoted 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()]
|
||||||
|
|
@ -241,77 +178,21 @@ 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
|
||||||
accountname, password = parts
|
|
||||||
|
|
||||||
# sanity checks
|
username, password = parts
|
||||||
if not re.findall(r"^[\w. @+\-']+$", accountname) or not (0 < len(accountname) <= 30):
|
|
||||||
# this echoes the restrictions made by django's auth
|
|
||||||
# module (except not allowing spaces, for convenience of
|
|
||||||
# logging in).
|
|
||||||
string = "\n\r Accountname can max be 30 characters or fewer. Letters, spaces, digits and @/./+/-/_/' only."
|
|
||||||
session.msg(string)
|
|
||||||
return
|
|
||||||
# strip excessive spaces in accountname
|
|
||||||
accountname = re.sub(r"\s+", " ", accountname).strip()
|
|
||||||
if AccountDB.objects.filter(username__iexact=accountname):
|
|
||||||
# account already exists (we also ignore capitalization here)
|
|
||||||
session.msg("Sorry, there is already an account with the name '%s'." % accountname)
|
|
||||||
return
|
|
||||||
# Reserve accountnames found in GUEST_LIST
|
|
||||||
if settings.GUEST_LIST and accountname.lower() in (guest.lower() for guest in settings.GUEST_LIST):
|
|
||||||
string = "\n\r That name is reserved. Please choose another Accountname."
|
|
||||||
session.msg(string)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Validate password
|
|
||||||
Account = utils.class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
|
|
||||||
# Have to create a dummy Account object to check username similarity
|
|
||||||
valid, error = Account.validate_password(password, account=Account(username=accountname))
|
|
||||||
if error:
|
|
||||||
errors = [e for suberror in error.messages for e in error.messages]
|
|
||||||
string = "\n".join(errors)
|
|
||||||
session.msg(string)
|
|
||||||
return
|
|
||||||
|
|
||||||
# Check IP and/or name bans
|
|
||||||
bans = ServerConfig.objects.conf("server_bans")
|
|
||||||
if bans and (any(tup[0] == accountname.lower() for tup in bans) or
|
|
||||||
|
|
||||||
any(tup[2].match(session.address) for tup in bans if tup[2])):
|
|
||||||
# this is a banned IP or name!
|
|
||||||
string = "|rYou have been banned and cannot continue from here." \
|
|
||||||
"\nIf you feel this ban is in error, please email an admin.|x"
|
|
||||||
session.msg(string)
|
|
||||||
session.sessionhandler.disconnect(session, "Good bye! Disconnecting.")
|
|
||||||
return
|
|
||||||
|
|
||||||
# everything's ok. Create the new account account.
|
# everything's ok. Create the new account account.
|
||||||
try:
|
account, errors = Account.create(username=username, password=password, ip=address, session=session)
|
||||||
permissions = settings.PERMISSION_ACCOUNT_DEFAULT
|
if account:
|
||||||
typeclass = settings.BASE_CHARACTER_TYPECLASS
|
# tell the caller everything went well.
|
||||||
new_account = _create_account(session, accountname, password, permissions)
|
string = "A new account '%s' was created. Welcome!"
|
||||||
if new_account:
|
if " " in username:
|
||||||
if MULTISESSION_MODE < 2:
|
string += "\n\nYou can now log in with the command 'connect \"%s\" <your password>'."
|
||||||
default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME)
|
else:
|
||||||
_create_character(session, new_account, typeclass, default_home, permissions)
|
string += "\n\nYou can now log with the command 'connect %s <your password>'."
|
||||||
|
session.msg(string % (username, username))
|
||||||
# Update the throttle to indicate a new account was created from this IP
|
else:
|
||||||
CREATION_THROTTLE.update(address)
|
session.msg("|R%s|n" % '\n'.join(errors))
|
||||||
|
|
||||||
# tell the caller everything went well.
|
|
||||||
string = "A new account '%s' was created. Welcome!"
|
|
||||||
if " " in accountname:
|
|
||||||
string += "\n\nYou can now log in with the command 'connect \"%s\" <your password>'."
|
|
||||||
else:
|
|
||||||
string += "\n\nYou can now log with the command 'connect %s <your password>'."
|
|
||||||
session.msg(string % (accountname, accountname))
|
|
||||||
|
|
||||||
except Exception:
|
|
||||||
# We are in the middle between logged in and -not, so we have
|
|
||||||
# to handle tracebacks ourselves at this point. If we don't,
|
|
||||||
# we won't see any errors at all.
|
|
||||||
session.msg("An error occurred. Please e-mail an admin if the problem persists.")
|
|
||||||
logger.log_trace()
|
|
||||||
|
|
||||||
|
|
||||||
class CmdUnconnectedQuit(COMMAND_DEFAULT_CLASS):
|
class CmdUnconnectedQuit(COMMAND_DEFAULT_CLASS):
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ Base typeclass for in-game Channels.
|
||||||
from evennia.typeclasses.models import TypeclassBase
|
from evennia.typeclasses.models import TypeclassBase
|
||||||
from evennia.comms.models import TempMsg, ChannelDB
|
from evennia.comms.models import TempMsg, ChannelDB
|
||||||
from evennia.comms.managers import ChannelManager
|
from evennia.comms.managers import ChannelManager
|
||||||
from evennia.utils import logger
|
from evennia.utils import create, logger
|
||||||
from evennia.utils.utils import make_iter
|
from evennia.utils.utils import make_iter
|
||||||
from future.utils import with_metaclass
|
from future.utils import with_metaclass
|
||||||
_CHANNEL_HANDLER = None
|
_CHANNEL_HANDLER = None
|
||||||
|
|
@ -220,6 +220,50 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
|
||||||
return self.locks.check(accessing_obj, access_type=access_type,
|
return self.locks.check(accessing_obj, access_type=access_type,
|
||||||
default=default, no_superuser_bypass=no_superuser_bypass)
|
default=default, no_superuser_bypass=no_superuser_bypass)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, key, account=None, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Creates a basic Channel with default parameters, unless otherwise
|
||||||
|
specified or extended.
|
||||||
|
|
||||||
|
Provides a friendlier interface to the utils.create_channel() function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): This must be unique.
|
||||||
|
account (Account): Account to attribute this object to.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
aliases (list of str): List of alternative (likely shorter) keynames.
|
||||||
|
description (str): A description of the channel, for use in listings.
|
||||||
|
locks (str): Lockstring.
|
||||||
|
keep_log (bool): Log channel throughput.
|
||||||
|
typeclass (str or class): The typeclass of the Channel (not
|
||||||
|
often used).
|
||||||
|
ip (str): IP address of creator (for object auditing).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
channel (Channel): A newly created Channel.
|
||||||
|
errors (list): A list of errors in string form, if any.
|
||||||
|
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
obj = None
|
||||||
|
ip = kwargs.pop('ip', '')
|
||||||
|
|
||||||
|
try:
|
||||||
|
kwargs['desc'] = kwargs.pop('description', '')
|
||||||
|
obj = create.create_channel(key, *args, **kwargs)
|
||||||
|
|
||||||
|
# Record creator id and creation IP
|
||||||
|
if ip: obj.db.creator_ip = ip
|
||||||
|
if account: obj.db.creator_id = account.id
|
||||||
|
|
||||||
|
except Exception as exc:
|
||||||
|
errors.append("An error occurred while creating this '%s' object." % key)
|
||||||
|
logger.log_err(exc)
|
||||||
|
|
||||||
|
return obj, errors
|
||||||
|
|
||||||
def delete(self):
|
def delete(self):
|
||||||
"""
|
"""
|
||||||
Deletes channel while also cleaning up channelhandler.
|
Deletes channel while also cleaning up channelhandler.
|
||||||
|
|
|
||||||
13
evennia/comms/tests.py
Normal file
13
evennia/comms/tests.py
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
|
||||||
|
from evennia.utils.test_resources import EvenniaTest
|
||||||
|
from evennia import DefaultChannel
|
||||||
|
|
||||||
|
class ObjectCreationTest(EvenniaTest):
|
||||||
|
|
||||||
|
def test_channel_create(self):
|
||||||
|
description = "A place to talk about coffee."
|
||||||
|
|
||||||
|
obj, errors = DefaultChannel.create('coffeetalk', description=description)
|
||||||
|
self.assertTrue(obj, errors)
|
||||||
|
self.assertFalse(errors, errors)
|
||||||
|
self.assertEqual(description, obj.db.desc)
|
||||||
|
|
@ -475,14 +475,14 @@ class CmdShiftRoot(Command):
|
||||||
root_pos["blue"] -= 1
|
root_pos["blue"] -= 1
|
||||||
self.caller.msg("The root with blue flowers gets in the way and is pushed to the left.")
|
self.caller.msg("The root with blue flowers gets in the way and is pushed to the left.")
|
||||||
else:
|
else:
|
||||||
self.caller.msg("You cannot move the root in that direction.")
|
self.caller.msg("The root hangs straight down - you can only move it left or right.")
|
||||||
elif color == "blue":
|
elif color == "blue":
|
||||||
if direction == "left":
|
if direction == "left":
|
||||||
root_pos[color] = max(-1, root_pos[color] - 1)
|
root_pos[color] = max(-1, root_pos[color] - 1)
|
||||||
self.caller.msg("You shift the root with small blue flowers to the left.")
|
self.caller.msg("You shift the root with small blue flowers to the left.")
|
||||||
if root_pos[color] != 0 and root_pos[color] == root_pos["red"]:
|
if root_pos[color] != 0 and root_pos[color] == root_pos["red"]:
|
||||||
root_pos["red"] += 1
|
root_pos["red"] += 1
|
||||||
self.caller.msg("The reddish root is to big to fit as well, so that one falls away to the left.")
|
self.caller.msg("The reddish root is too big to fit as well, so that one falls away to the left.")
|
||||||
elif direction == "right":
|
elif direction == "right":
|
||||||
root_pos[color] = min(1, root_pos[color] + 1)
|
root_pos[color] = min(1, root_pos[color] + 1)
|
||||||
self.caller.msg("You shove the root adorned with small blue flowers to the right.")
|
self.caller.msg("You shove the root adorned with small blue flowers to the right.")
|
||||||
|
|
@ -490,7 +490,7 @@ class CmdShiftRoot(Command):
|
||||||
root_pos["red"] -= 1
|
root_pos["red"] -= 1
|
||||||
self.caller.msg("The thick reddish root gets in the way and is pushed back to the left.")
|
self.caller.msg("The thick reddish root gets in the way and is pushed back to the left.")
|
||||||
else:
|
else:
|
||||||
self.caller.msg("You cannot move the root in that direction.")
|
self.caller.msg("The root hangs straight down - you can only move it left or right.")
|
||||||
|
|
||||||
# now the horizontal roots (yellow/green). They can be moved up/down
|
# now the horizontal roots (yellow/green). They can be moved up/down
|
||||||
elif color == "yellow":
|
elif color == "yellow":
|
||||||
|
|
@ -507,7 +507,7 @@ class CmdShiftRoot(Command):
|
||||||
root_pos["green"] -= 1
|
root_pos["green"] -= 1
|
||||||
self.caller.msg("The weedy green root is shifted upwards to make room.")
|
self.caller.msg("The weedy green root is shifted upwards to make room.")
|
||||||
else:
|
else:
|
||||||
self.caller.msg("You cannot move the root in that direction.")
|
self.caller.msg("The root hangs across the wall - you can only move it up or down.")
|
||||||
elif color == "green":
|
elif color == "green":
|
||||||
if direction == "up":
|
if direction == "up":
|
||||||
root_pos[color] = max(-1, root_pos[color] - 1)
|
root_pos[color] = max(-1, root_pos[color] - 1)
|
||||||
|
|
@ -522,7 +522,7 @@ class CmdShiftRoot(Command):
|
||||||
root_pos["yellow"] -= 1
|
root_pos["yellow"] -= 1
|
||||||
self.caller.msg("The root with yellow flowers gets in the way and is pushed upwards.")
|
self.caller.msg("The root with yellow flowers gets in the way and is pushed upwards.")
|
||||||
else:
|
else:
|
||||||
self.caller.msg("You cannot move the root in that direction.")
|
self.caller.msg("The root hangs across the wall - you can only move it up or down.")
|
||||||
|
|
||||||
# we have moved the root. Store new position
|
# we have moved the root. Store new position
|
||||||
self.obj.db.root_pos = root_pos
|
self.obj.db.root_pos = root_pos
|
||||||
|
|
|
||||||
|
|
@ -747,9 +747,16 @@ class CmdLookDark(Command):
|
||||||
"""
|
"""
|
||||||
caller = self.caller
|
caller = self.caller
|
||||||
|
|
||||||
if random.random() < 0.75:
|
# count how many searches we've done
|
||||||
|
nr_searches = caller.ndb.dark_searches
|
||||||
|
if nr_searches is None:
|
||||||
|
nr_searches = 0
|
||||||
|
caller.ndb.dark_searches = nr_searches
|
||||||
|
|
||||||
|
if nr_searches < 4 and random.random() < 0.90:
|
||||||
# we don't find anything
|
# we don't find anything
|
||||||
caller.msg(random.choice(DARK_MESSAGES))
|
caller.msg(random.choice(DARK_MESSAGES))
|
||||||
|
caller.ndb.dark_searches += 1
|
||||||
else:
|
else:
|
||||||
# we could have found something!
|
# we could have found something!
|
||||||
if any(obj for obj in caller.contents if utils.inherits_from(obj, LightSource)):
|
if any(obj for obj in caller.contents if utils.inherits_from(obj, LightSource)):
|
||||||
|
|
@ -791,7 +798,8 @@ class CmdDarkNoMatch(Command):
|
||||||
|
|
||||||
def func(self):
|
def func(self):
|
||||||
"""Implements the command."""
|
"""Implements the command."""
|
||||||
self.caller.msg("Until you find some light, there's not much you can do. Try feeling around.")
|
self.caller.msg("Until you find some light, there's not much you can do. "
|
||||||
|
"Try feeling around, maybe you'll find something helpful!")
|
||||||
|
|
||||||
|
|
||||||
class DarkCmdSet(CmdSet):
|
class DarkCmdSet(CmdSet):
|
||||||
|
|
@ -814,7 +822,9 @@ class DarkCmdSet(CmdSet):
|
||||||
self.add(CmdLookDark())
|
self.add(CmdLookDark())
|
||||||
self.add(CmdDarkHelp())
|
self.add(CmdDarkHelp())
|
||||||
self.add(CmdDarkNoMatch())
|
self.add(CmdDarkNoMatch())
|
||||||
self.add(default_cmds.CmdSay)
|
self.add(default_cmds.CmdSay())
|
||||||
|
self.add(default_cmds.CmdQuit())
|
||||||
|
self.add(default_cmds.CmdHome())
|
||||||
|
|
||||||
|
|
||||||
class DarkRoom(TutorialRoom):
|
class DarkRoom(TutorialRoom):
|
||||||
|
|
|
||||||
|
|
@ -21,6 +21,7 @@ from evennia.scripts.scripthandler import ScriptHandler
|
||||||
from evennia.commands import cmdset, command
|
from evennia.commands import cmdset, command
|
||||||
from evennia.commands.cmdsethandler import CmdSetHandler
|
from evennia.commands.cmdsethandler import CmdSetHandler
|
||||||
from evennia.commands import cmdhandler
|
from evennia.commands import cmdhandler
|
||||||
|
from evennia.utils import create
|
||||||
from evennia.utils import search
|
from evennia.utils import search
|
||||||
from evennia.utils import logger
|
from evennia.utils import logger
|
||||||
from evennia.utils import ansi
|
from evennia.utils import ansi
|
||||||
|
|
@ -191,6 +192,10 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
without `obj.save()` having to be called explicitly.
|
without `obj.save()` having to be called explicitly.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# lockstring of newly created objects, for easy overloading.
|
||||||
|
# Will be formatted with the appropriate attributes.
|
||||||
|
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
|
||||||
|
|
@ -214,7 +219,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
@property
|
@property
|
||||||
def is_connected(self):
|
def is_connected(self):
|
||||||
# we get an error for objects subscribed to channels without this
|
# we get an error for objects subscribed to channels without this
|
||||||
if self.account: # seems sane to pass on the account
|
if self.account: # seems sane to pass on the account
|
||||||
return self.account.is_connected
|
return self.account.is_connected
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
@ -425,7 +430,8 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
# only allow exact matching if searching the entire database
|
# only allow exact matching if searching the entire database
|
||||||
# or unique #dbrefs
|
# or unique #dbrefs
|
||||||
exact = True
|
exact = True
|
||||||
elif candidates is None:
|
else:
|
||||||
|
# TODO: write code...if candidates is None:
|
||||||
# no custom candidates given - get them automatically
|
# no custom candidates given - get them automatically
|
||||||
if location:
|
if location:
|
||||||
# location(s) were given
|
# location(s) were given
|
||||||
|
|
@ -858,6 +864,67 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
obj.msg(_(string))
|
obj.msg(_(string))
|
||||||
obj.move_to(home)
|
obj.move_to(home)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, key, account=None, **kwargs):
|
||||||
|
"""
|
||||||
|
Creates a basic object with default parameters, unless otherwise
|
||||||
|
specified or extended.
|
||||||
|
|
||||||
|
Provides a friendlier interface to the utils.create_object() function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): Name of the new object.
|
||||||
|
account (Account): Account to attribute this object to.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
description (str): Brief description for this object.
|
||||||
|
ip (str): IP address of creator (for object auditing).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
object (Object): A newly created object of the given typeclass.
|
||||||
|
errors (list): A list of errors in string form, if any.
|
||||||
|
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
obj = None
|
||||||
|
|
||||||
|
# Get IP address of creator, if available
|
||||||
|
ip = kwargs.pop('ip', '')
|
||||||
|
|
||||||
|
# If no typeclass supplied, use this class
|
||||||
|
kwargs['typeclass'] = kwargs.pop('typeclass', cls)
|
||||||
|
|
||||||
|
# Set the supplied key as the name of the intended object
|
||||||
|
kwargs['key'] = key
|
||||||
|
|
||||||
|
# Get a supplied description, if any
|
||||||
|
description = kwargs.pop('description', '')
|
||||||
|
|
||||||
|
# Create a sane lockstring if one wasn't supplied
|
||||||
|
lockstring = kwargs.get('locks')
|
||||||
|
if account and not lockstring:
|
||||||
|
lockstring = cls.lockstring.format(account_id=account.id)
|
||||||
|
kwargs['locks'] = lockstring
|
||||||
|
|
||||||
|
# Create object
|
||||||
|
try:
|
||||||
|
obj = create.create_object(**kwargs)
|
||||||
|
|
||||||
|
# Record creator id and creation IP
|
||||||
|
if ip: obj.db.creator_ip = ip
|
||||||
|
if account: obj.db.creator_id = account.id
|
||||||
|
|
||||||
|
# Set description if there is none, or update it if provided
|
||||||
|
if description or not obj.db.desc:
|
||||||
|
desc = description if description else "You see nothing special."
|
||||||
|
obj.db.desc = desc
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
errors.append("An error occurred while creating this '%s' object." % key)
|
||||||
|
logger.log_err(e)
|
||||||
|
|
||||||
|
return obj, errors
|
||||||
|
|
||||||
def copy(self, new_key=None):
|
def copy(self, new_key=None):
|
||||||
"""
|
"""
|
||||||
Makes an identical copy of this object, identical except for a
|
Makes an identical copy of this object, identical except for a
|
||||||
|
|
@ -913,8 +980,12 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
||||||
# no need to disconnect, Account just jumps to OOC mode.
|
# no need to disconnect, Account just jumps to OOC mode.
|
||||||
# sever the connection (important!)
|
# sever the connection (important!)
|
||||||
if self.account:
|
if self.account:
|
||||||
|
# Remove the object from playable characters list
|
||||||
|
if self in self.account.db._playable_characters:
|
||||||
|
self.account.db._playable_characters = [x for x in self.account.db._playable_characters if x != self]
|
||||||
for session in self.sessions.all():
|
for session in self.sessions.all():
|
||||||
self.account.unpuppet_object(session)
|
self.account.unpuppet_object(session)
|
||||||
|
|
||||||
self.account = None
|
self.account = None
|
||||||
|
|
||||||
for script in _ScriptDB.objects.get_all_scripts_on_obj(self):
|
for script in _ScriptDB.objects.get_all_scripts_on_obj(self):
|
||||||
|
|
@ -1821,6 +1892,83 @@ class DefaultCharacter(DefaultObject):
|
||||||
a character avatar controlled by an account.
|
a character avatar controlled by an account.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# lockstring of newly created rooms, for easy overloading.
|
||||||
|
# Will be formatted with the appropriate attributes.
|
||||||
|
lockstring = "puppet:id({character_id}) or pid({account_id}) or perm(Developer) or pperm(Developer)"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, key, account, **kwargs):
|
||||||
|
"""
|
||||||
|
Creates a basic Character with default parameters, unless otherwise
|
||||||
|
specified or extended.
|
||||||
|
|
||||||
|
Provides a friendlier interface to the utils.create_character() function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): Name of the new Character.
|
||||||
|
account (obj): Account to associate this Character with. Required as
|
||||||
|
an argument, but one can fake it out by supplying None-- it will
|
||||||
|
change the default lockset and skip creator attribution.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
description (str): Brief description for this object.
|
||||||
|
ip (str): IP address of creator (for object auditing).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
character (Object): A newly created Character of the given typeclass.
|
||||||
|
errors (list): A list of errors in string form, if any.
|
||||||
|
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
obj = None
|
||||||
|
|
||||||
|
# Get IP address of creator, if available
|
||||||
|
ip = kwargs.pop('ip', '')
|
||||||
|
|
||||||
|
# If no typeclass supplied, use this class
|
||||||
|
kwargs['typeclass'] = kwargs.pop('typeclass', cls)
|
||||||
|
|
||||||
|
# Set the supplied key as the name of the intended object
|
||||||
|
kwargs['key'] = key
|
||||||
|
|
||||||
|
# Get home for character
|
||||||
|
kwargs['home'] = ObjectDB.objects.get_id(kwargs.get('home', settings.DEFAULT_HOME))
|
||||||
|
|
||||||
|
# Get permissions
|
||||||
|
kwargs['permissions'] = kwargs.get('permissions', settings.PERMISSION_ACCOUNT_DEFAULT)
|
||||||
|
|
||||||
|
# Get description if provided
|
||||||
|
description = kwargs.pop('description', '')
|
||||||
|
|
||||||
|
# Get locks if provided
|
||||||
|
locks = kwargs.pop('locks', '')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create the Character
|
||||||
|
obj = create.create_object(**kwargs)
|
||||||
|
|
||||||
|
# Record creator id and creation IP
|
||||||
|
if ip: obj.db.creator_ip = ip
|
||||||
|
if account: obj.db.creator_id = account.id
|
||||||
|
|
||||||
|
# Add locks
|
||||||
|
if not locks and account:
|
||||||
|
# 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})
|
||||||
|
elif not locks and not account:
|
||||||
|
locks = cls.lockstring.format(**{'character_id': obj.id, 'account_id': -1})
|
||||||
|
|
||||||
|
obj.locks.add(locks)
|
||||||
|
|
||||||
|
# If no description is set, set a default description
|
||||||
|
if description or not obj.db.desc:
|
||||||
|
obj.db.desc = description if description else "This is a character."
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
errors.append("An error occurred while creating this '%s' object." % key)
|
||||||
|
logger.log_err(e)
|
||||||
|
|
||||||
|
return obj, errors
|
||||||
|
|
||||||
def basetype_setup(self):
|
def basetype_setup(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1938,6 +2086,72 @@ class DefaultRoom(DefaultObject):
|
||||||
This is the base room object. It's just like any Object except its
|
This is the base room object. It's just like any Object except its
|
||||||
location is always `None`.
|
location is always `None`.
|
||||||
"""
|
"""
|
||||||
|
# lockstring of newly created rooms, for easy overloading.
|
||||||
|
# Will be formatted with the {id} of the creating object.
|
||||||
|
lockstring = "control:id({id}) or perm(Admin); " \
|
||||||
|
"delete:id({id}) or perm(Admin); " \
|
||||||
|
"edit:id({id}) or perm(Admin)"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, key, account, **kwargs):
|
||||||
|
"""
|
||||||
|
Creates a basic Room with default parameters, unless otherwise
|
||||||
|
specified or extended.
|
||||||
|
|
||||||
|
Provides a friendlier interface to the utils.create_object() function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): Name of the new Room.
|
||||||
|
account (obj): Account to associate this Room with.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
description (str): Brief description for this object.
|
||||||
|
ip (str): IP address of creator (for object auditing).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
room (Object): A newly created Room of the given typeclass.
|
||||||
|
errors (list): A list of errors in string form, if any.
|
||||||
|
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
obj = None
|
||||||
|
|
||||||
|
# Get IP address of creator, if available
|
||||||
|
ip = kwargs.pop('ip', '')
|
||||||
|
|
||||||
|
# If no typeclass supplied, use this class
|
||||||
|
kwargs['typeclass'] = kwargs.pop('typeclass', cls)
|
||||||
|
|
||||||
|
# Set the supplied key as the name of the intended object
|
||||||
|
kwargs['key'] = key
|
||||||
|
|
||||||
|
# Get who to send errors to
|
||||||
|
kwargs['report_to'] = kwargs.pop('report_to', account)
|
||||||
|
|
||||||
|
# Get description, if provided
|
||||||
|
description = kwargs.pop('description', '')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create the Room
|
||||||
|
obj = create.create_object(**kwargs)
|
||||||
|
|
||||||
|
# Set appropriate locks
|
||||||
|
lockstring = kwargs.get('locks', cls.lockstring.format(id=account.id))
|
||||||
|
obj.locks.add(lockstring)
|
||||||
|
|
||||||
|
# Record creator id and creation IP
|
||||||
|
if ip: obj.db.creator_ip = ip
|
||||||
|
if account: obj.db.creator_id = account.id
|
||||||
|
|
||||||
|
# If no description is set, set a default description
|
||||||
|
if description or not obj.db.desc:
|
||||||
|
obj.db.desc = description if description else "This is a room."
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
errors.append("An error occurred while creating this '%s' object." % key)
|
||||||
|
logger.log_err(e)
|
||||||
|
|
||||||
|
return obj, errors
|
||||||
|
|
||||||
def basetype_setup(self):
|
def basetype_setup(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -2016,6 +2230,13 @@ class DefaultExit(DefaultObject):
|
||||||
|
|
||||||
exit_command = ExitCommand
|
exit_command = ExitCommand
|
||||||
priority = 101
|
priority = 101
|
||||||
|
|
||||||
|
# lockstring of newly created exits, for easy overloading.
|
||||||
|
# Will be formatted with the {id} of the creating object.
|
||||||
|
lockstring = "control:id({id}) or perm(Admin); " \
|
||||||
|
"delete: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.
|
||||||
|
|
@ -2054,6 +2275,73 @@ class DefaultExit(DefaultObject):
|
||||||
|
|
||||||
# Command hooks
|
# Command hooks
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, key, account, source, dest, **kwargs):
|
||||||
|
"""
|
||||||
|
Creates a basic Exit with default parameters, unless otherwise
|
||||||
|
specified or extended.
|
||||||
|
|
||||||
|
Provides a friendlier interface to the utils.create_object() function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): Name of the new Exit, as it should appear from the
|
||||||
|
source room.
|
||||||
|
account (obj): Account to associate this Exit with.
|
||||||
|
source (Room): The room to create this exit in.
|
||||||
|
dest (Room): The room to which this exit should go.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
description (str): Brief description for this object.
|
||||||
|
ip (str): IP address of creator (for object auditing).
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
exit (Object): A newly created Room of the given typeclass.
|
||||||
|
errors (list): A list of errors in string form, if any.
|
||||||
|
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
obj = None
|
||||||
|
|
||||||
|
# Get IP address of creator, if available
|
||||||
|
ip = kwargs.pop('ip', '')
|
||||||
|
|
||||||
|
# If no typeclass supplied, use this class
|
||||||
|
kwargs['typeclass'] = kwargs.pop('typeclass', cls)
|
||||||
|
|
||||||
|
# Set the supplied key as the name of the intended object
|
||||||
|
kwargs['key'] = key
|
||||||
|
|
||||||
|
# Get who to send errors to
|
||||||
|
kwargs['report_to'] = kwargs.pop('report_to', account)
|
||||||
|
|
||||||
|
# Set to/from rooms
|
||||||
|
kwargs['location'] = source
|
||||||
|
kwargs['destination'] = dest
|
||||||
|
|
||||||
|
description = kwargs.pop('description', '')
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Create the Exit
|
||||||
|
obj = create.create_object(**kwargs)
|
||||||
|
|
||||||
|
# Set appropriate locks
|
||||||
|
lockstring = kwargs.get('locks', cls.lockstring.format(id=account.id))
|
||||||
|
obj.locks.add(lockstring)
|
||||||
|
|
||||||
|
# Record creator id and creation IP
|
||||||
|
if ip: obj.db.creator_ip = ip
|
||||||
|
if account: obj.db.creator_id = account.id
|
||||||
|
|
||||||
|
# If no description is set, set a default description
|
||||||
|
if description or not obj.db.desc:
|
||||||
|
obj.db.desc = description if description else "This is an exit."
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
errors.append("An error occurred while creating this '%s' object." % key)
|
||||||
|
logger.log_err(e)
|
||||||
|
|
||||||
|
return obj, errors
|
||||||
|
|
||||||
def basetype_setup(self):
|
def basetype_setup(self):
|
||||||
"""
|
"""
|
||||||
Setup exit-security
|
Setup exit-security
|
||||||
|
|
|
||||||
47
evennia/objects/tests.py
Normal file
47
evennia/objects/tests.py
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
from evennia.utils.test_resources import EvenniaTest
|
||||||
|
from evennia import DefaultObject, DefaultCharacter, DefaultRoom, DefaultExit
|
||||||
|
|
||||||
|
|
||||||
|
class DefaultObjectTest(EvenniaTest):
|
||||||
|
|
||||||
|
ip = '212.216.139.14'
|
||||||
|
|
||||||
|
def test_object_create(self):
|
||||||
|
description = 'A home for a grouch.'
|
||||||
|
obj, errors = DefaultObject.create('trashcan', self.account, description=description, ip=self.ip)
|
||||||
|
self.assertTrue(obj, errors)
|
||||||
|
self.assertFalse(errors, errors)
|
||||||
|
self.assertEqual(description, obj.db.desc)
|
||||||
|
self.assertEqual(obj.db.creator_ip, self.ip)
|
||||||
|
|
||||||
|
def test_character_create(self):
|
||||||
|
description = 'A furry green monster, reeking of garbage.'
|
||||||
|
obj, errors = DefaultCharacter.create('oscar', self.account, description=description, ip=self.ip)
|
||||||
|
self.assertTrue(obj, errors)
|
||||||
|
self.assertFalse(errors, errors)
|
||||||
|
self.assertEqual(description, obj.db.desc)
|
||||||
|
self.assertEqual(obj.db.creator_ip, self.ip)
|
||||||
|
|
||||||
|
def test_room_create(self):
|
||||||
|
description = 'A dimly-lit alley behind the local Chinese restaurant.'
|
||||||
|
obj, errors = DefaultRoom.create('alley', self.account, description=description, ip=self.ip)
|
||||||
|
self.assertTrue(obj, errors)
|
||||||
|
self.assertFalse(errors, errors)
|
||||||
|
self.assertEqual(description, obj.db.desc)
|
||||||
|
self.assertEqual(obj.db.creator_ip, self.ip)
|
||||||
|
|
||||||
|
def test_exit_create(self):
|
||||||
|
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)
|
||||||
|
self.assertTrue(obj, errors)
|
||||||
|
self.assertFalse(errors, errors)
|
||||||
|
self.assertEqual(description, obj.db.desc)
|
||||||
|
self.assertEqual(obj.db.creator_ip, self.ip)
|
||||||
|
|
||||||
|
def test_urls(self):
|
||||||
|
"Make sure objects are returning URLs"
|
||||||
|
self.assertTrue(self.char1.get_absolute_url())
|
||||||
|
self.assertTrue('admin' in self.char1.web_get_admin_url())
|
||||||
|
|
||||||
|
self.assertTrue(self.room1.get_absolute_url())
|
||||||
|
self.assertTrue('admin' in self.room1.web_get_admin_url())
|
||||||
|
|
@ -562,6 +562,7 @@ def node_index(caller):
|
||||||
|
|
||||||
text = """
|
text = """
|
||||||
|c --- Prototype wizard --- |n
|
|c --- Prototype wizard --- |n
|
||||||
|
%s
|
||||||
|
|
||||||
A |cprototype|n is a 'template' for |wspawning|n an in-game entity. A field of the prototype
|
A |cprototype|n is a 'template' for |wspawning|n an in-game entity. A field of the prototype
|
||||||
can either be hard-coded, left empty or scripted using |w$protfuncs|n - for example to
|
can either be hard-coded, left empty or scripted using |w$protfuncs|n - for example to
|
||||||
|
|
@ -599,6 +600,17 @@ def node_index(caller):
|
||||||
{pfuncs}
|
{pfuncs}
|
||||||
""".format(pfuncs=_format_protfuncs())
|
""".format(pfuncs=_format_protfuncs())
|
||||||
|
|
||||||
|
# If a prototype is being edited, show its key and
|
||||||
|
# prototype_key under the title
|
||||||
|
loaded_prototype = ''
|
||||||
|
if 'prototype_key' in prototype \
|
||||||
|
or 'key' in prototype:
|
||||||
|
loaded_prototype = ' --- Editing: |y{}({})|n --- '.format(
|
||||||
|
prototype.get('key', ''),
|
||||||
|
prototype.get('prototype_key', '')
|
||||||
|
)
|
||||||
|
text = text % (loaded_prototype)
|
||||||
|
|
||||||
text = (text, helptxt)
|
text = (text, helptxt)
|
||||||
|
|
||||||
options = []
|
options = []
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,8 @@ Handling storage of prototypes, both database-based ones (DBPrototypes) and thos
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import re
|
import re
|
||||||
|
import hashlib
|
||||||
|
import time
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from evennia.scripts.scripts import DefaultScript
|
from evennia.scripts.scripts import DefaultScript
|
||||||
|
|
@ -13,7 +15,7 @@ from evennia.objects.models import ObjectDB
|
||||||
from evennia.utils.create import create_script
|
from evennia.utils.create import create_script
|
||||||
from evennia.utils.utils import (
|
from evennia.utils.utils import (
|
||||||
all_from_module, make_iter, is_iter, dbid_to_obj, callables_from_module,
|
all_from_module, make_iter, is_iter, dbid_to_obj, callables_from_module,
|
||||||
get_all_typeclasses, to_str, dbref, justify)
|
get_all_typeclasses, to_str, dbref, justify, class_from_module)
|
||||||
from evennia.locks.lockhandler import validate_lockstring, check_lockstring
|
from evennia.locks.lockhandler import validate_lockstring, check_lockstring
|
||||||
from evennia.utils import logger
|
from evennia.utils import logger
|
||||||
from evennia.utils import inlinefuncs, dbserialize
|
from evennia.utils import inlinefuncs, dbserialize
|
||||||
|
|
@ -47,8 +49,8 @@ class ValidationError(RuntimeError):
|
||||||
|
|
||||||
def homogenize_prototype(prototype, custom_keys=None):
|
def homogenize_prototype(prototype, custom_keys=None):
|
||||||
"""
|
"""
|
||||||
Homogenize the more free-form prototype (where undefined keys are non-category attributes)
|
Homogenize the more free-form prototype supported pre Evennia 0.7 into the stricter form.
|
||||||
into the stricter form using `attrs` required by the system.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
prototype (dict): Prototype.
|
prototype (dict): Prototype.
|
||||||
|
|
@ -56,18 +58,45 @@ def homogenize_prototype(prototype, custom_keys=None):
|
||||||
the default reserved keys.
|
the default reserved keys.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
homogenized (dict): Prototype where all non-identified keys grouped as attributes.
|
homogenized (dict): Prototype where all non-identified keys grouped as attributes and other
|
||||||
|
homogenizations like adding missing prototype_keys and setting a default typeclass.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
reserved = _PROTOTYPE_RESERVED_KEYS + (custom_keys or ())
|
reserved = _PROTOTYPE_RESERVED_KEYS + (custom_keys or ())
|
||||||
|
|
||||||
attrs = list(prototype.get('attrs', [])) # break reference
|
attrs = list(prototype.get('attrs', [])) # break reference
|
||||||
|
tags = make_iter(prototype.get('tags', []))
|
||||||
|
homogenized_tags = []
|
||||||
|
|
||||||
homogenized = {}
|
homogenized = {}
|
||||||
for key, val in prototype.items():
|
for key, val in prototype.items():
|
||||||
if key in reserved:
|
if key in reserved:
|
||||||
homogenized[key] = val
|
if key == 'tags':
|
||||||
|
for tag in tags:
|
||||||
|
if not is_iter(tag):
|
||||||
|
homogenized_tags.append((tag, None, None))
|
||||||
|
else:
|
||||||
|
homogenized_tags.append(tag)
|
||||||
|
else:
|
||||||
|
homogenized[key] = val
|
||||||
else:
|
else:
|
||||||
|
# unassigned keys -> attrs
|
||||||
attrs.append((key, val, None, ''))
|
attrs.append((key, val, None, ''))
|
||||||
if attrs:
|
if attrs:
|
||||||
homogenized['attrs'] = attrs
|
homogenized['attrs'] = attrs
|
||||||
|
if homogenized_tags:
|
||||||
|
homogenized['tags'] = homogenized_tags
|
||||||
|
|
||||||
|
# add required missing parts that had defaults before
|
||||||
|
|
||||||
|
if "prototype_key" not in prototype:
|
||||||
|
# assign a random hash as key
|
||||||
|
homogenized["prototype_key"] = "prototype-{}".format(
|
||||||
|
hashlib.md5(str(time.time())).hexdigest()[:7])
|
||||||
|
|
||||||
|
if "typeclass" not in prototype and "prototype_parent" not in prototype:
|
||||||
|
homogenized["typeclass"] = settings.BASE_OBJECT_TYPECLASS
|
||||||
|
|
||||||
return homogenized
|
return homogenized
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -76,9 +105,12 @@ def homogenize_prototype(prototype, custom_keys=None):
|
||||||
for mod in settings.PROTOTYPE_MODULES:
|
for mod in settings.PROTOTYPE_MODULES:
|
||||||
# to remove a default prototype, override it with an empty dict.
|
# to remove a default prototype, override it with an empty dict.
|
||||||
# internally we store as (key, desc, locks, tags, prototype_dict)
|
# internally we store as (key, desc, locks, tags, prototype_dict)
|
||||||
prots = [(prototype_key.lower(), homogenize_prototype(prot))
|
prots = []
|
||||||
for prototype_key, prot in all_from_module(mod).items()
|
for variable_name, prot in all_from_module(mod).items():
|
||||||
if prot and isinstance(prot, dict)]
|
if isinstance(prot, dict):
|
||||||
|
if "prototype_key" not in prot:
|
||||||
|
prot['prototype_key'] = variable_name.lower()
|
||||||
|
prots.append((prot['prototype_key'], homogenize_prototype(prot)))
|
||||||
# assign module path to each prototype_key for easy reference
|
# assign module path to each prototype_key for easy reference
|
||||||
_MODULE_PROTOTYPE_MODULES.update({prototype_key.lower(): mod for prototype_key, _ in prots})
|
_MODULE_PROTOTYPE_MODULES.update({prototype_key.lower(): mod for prototype_key, _ in prots})
|
||||||
# make sure the prototype contains all meta info
|
# make sure the prototype contains all meta info
|
||||||
|
|
@ -432,11 +464,13 @@ def validate_prototype(prototype, protkey=None, protparents=None,
|
||||||
_flags['warnings'].append("Prototype {} can only be used as a mixin since it lacks "
|
_flags['warnings'].append("Prototype {} can only be used as a mixin since it lacks "
|
||||||
"a typeclass or a prototype_parent.".format(protkey))
|
"a typeclass or a prototype_parent.".format(protkey))
|
||||||
|
|
||||||
if (strict and typeclass and typeclass not
|
if strict and typeclass:
|
||||||
in get_all_typeclasses("evennia.objects.models.ObjectDB")):
|
try:
|
||||||
_flags['errors'].append(
|
class_from_module(typeclass)
|
||||||
"Prototype {} is based on typeclass {}, which could not be imported!".format(
|
except ImportError as err:
|
||||||
protkey, typeclass))
|
_flags['errors'].append(
|
||||||
|
"{}: Prototype {} is based on typeclass {}, which could not be imported!".format(
|
||||||
|
err, protkey, typeclass))
|
||||||
|
|
||||||
# recursively traverese prototype_parent chain
|
# recursively traverese prototype_parent chain
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -259,11 +259,11 @@ def prototype_from_object(obj):
|
||||||
if aliases:
|
if aliases:
|
||||||
prot['aliases'] = aliases
|
prot['aliases'] = aliases
|
||||||
tags = [(tag.db_key, tag.db_category, tag.db_data)
|
tags = [(tag.db_key, tag.db_category, tag.db_data)
|
||||||
for tag in obj.tags.get(return_tagobj=True, return_list=True) if tag]
|
for tag in obj.tags.all(return_objs=True)]
|
||||||
if tags:
|
if tags:
|
||||||
prot['tags'] = tags
|
prot['tags'] = tags
|
||||||
attrs = [(attr.key, attr.value, attr.category, ';'.join(attr.locks.all()))
|
attrs = [(attr.key, attr.value, attr.category, ';'.join(attr.locks.all()))
|
||||||
for attr in obj.attributes.get(return_obj=True, return_list=True) if attr]
|
for attr in obj.attributes.all()]
|
||||||
if attrs:
|
if attrs:
|
||||||
prot['attrs'] = attrs
|
prot['attrs'] = attrs
|
||||||
|
|
||||||
|
|
@ -659,6 +659,10 @@ def spawn(*prototypes, **kwargs):
|
||||||
# get available protparents
|
# get available protparents
|
||||||
protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()}
|
protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()}
|
||||||
|
|
||||||
|
if not kwargs.get("only_validate"):
|
||||||
|
# homogenization to be more lenient about prototype format when entering the prototype manually
|
||||||
|
prototypes = [protlib.homogenize_prototype(prot) for prot in prototypes]
|
||||||
|
|
||||||
# overload module's protparents with specifically given protparents
|
# overload module's protparents with specifically given protparents
|
||||||
# we allow prototype_key to be the key of the protparent dict, to allow for module-level
|
# we allow prototype_key to be the key of the protparent dict, to allow for module-level
|
||||||
# prototype imports. We need to insert prototype_key in this case
|
# prototype imports. We need to insert prototype_key in this case
|
||||||
|
|
@ -711,8 +715,8 @@ def spawn(*prototypes, **kwargs):
|
||||||
|
|
||||||
val = prot.pop("tags", [])
|
val = prot.pop("tags", [])
|
||||||
tags = []
|
tags = []
|
||||||
for (tag, category, data) in tags:
|
for (tag, category, data) in val:
|
||||||
tags.append((init_spawn_value(val, str), category, data))
|
tags.append((init_spawn_value(tag, str), category, data))
|
||||||
|
|
||||||
prototype_key = prototype.get('prototype_key', None)
|
prototype_key = prototype.get('prototype_key', None)
|
||||||
if prototype_key:
|
if prototype_key:
|
||||||
|
|
@ -730,7 +734,7 @@ def spawn(*prototypes, **kwargs):
|
||||||
val = make_iter(prot.pop("attrs", []))
|
val = make_iter(prot.pop("attrs", []))
|
||||||
attributes = []
|
attributes = []
|
||||||
for (attrname, value, category, locks) in val:
|
for (attrname, value, category, locks) in val:
|
||||||
attributes.append((attrname, init_spawn_value(val), category, locks))
|
attributes.append((attrname, init_spawn_value(value), category, locks))
|
||||||
|
|
||||||
simple_attributes = []
|
simple_attributes = []
|
||||||
for key, value in ((key, value) for key, value in prot.items()
|
for key, value in ((key, value) for key, value in prot.items()
|
||||||
|
|
|
||||||
|
|
@ -134,6 +134,7 @@ class TestUtils(EvenniaTest):
|
||||||
'prototype_key': Something,
|
'prototype_key': Something,
|
||||||
'prototype_locks': 'spawn:all();edit:all()',
|
'prototype_locks': 'spawn:all();edit:all()',
|
||||||
'prototype_tags': [],
|
'prototype_tags': [],
|
||||||
|
'tags': [(u'footag', u'foocategory', None)],
|
||||||
'typeclass': 'evennia.objects.objects.DefaultObject'})
|
'typeclass': 'evennia.objects.objects.DefaultObject'})
|
||||||
|
|
||||||
self.assertEqual(old_prot,
|
self.assertEqual(old_prot,
|
||||||
|
|
@ -182,6 +183,7 @@ class TestUtils(EvenniaTest):
|
||||||
'typeclass': ('evennia.objects.objects.DefaultObject',
|
'typeclass': ('evennia.objects.objects.DefaultObject',
|
||||||
'evennia.objects.objects.DefaultObject', 'KEEP'),
|
'evennia.objects.objects.DefaultObject', 'KEEP'),
|
||||||
'aliases': {'foo': ('foo', None, 'REMOVE')},
|
'aliases': {'foo': ('foo', None, 'REMOVE')},
|
||||||
|
'tags': {u'footag': ((u'footag', u'foocategory', None), None, 'REMOVE')},
|
||||||
'prototype_desc': ('Built from Obj',
|
'prototype_desc': ('Built from Obj',
|
||||||
'New version of prototype', 'UPDATE'),
|
'New version of prototype', 'UPDATE'),
|
||||||
'permissions': {"Builder": (None, 'Builder', 'ADD')}
|
'permissions': {"Builder": (None, 'Builder', 'ADD')}
|
||||||
|
|
@ -200,6 +202,7 @@ class TestUtils(EvenniaTest):
|
||||||
'prototype_key': 'UPDATE',
|
'prototype_key': 'UPDATE',
|
||||||
'prototype_locks': 'KEEP',
|
'prototype_locks': 'KEEP',
|
||||||
'prototype_tags': 'KEEP',
|
'prototype_tags': 'KEEP',
|
||||||
|
'tags': 'REMOVE',
|
||||||
'typeclass': 'KEEP'}
|
'typeclass': 'KEEP'}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -384,8 +387,9 @@ class TestPrototypeStorage(EvenniaTest):
|
||||||
prot3 = protlib.create_prototype(**self.prot3)
|
prot3 = protlib.create_prototype(**self.prot3)
|
||||||
|
|
||||||
# partial match
|
# partial match
|
||||||
self.assertEqual(list(protlib.search_prototype("prot")), [prot1b, prot2, prot3])
|
with mock.patch("evennia.prototypes.prototypes._MODULE_PROTOTYPES", {}):
|
||||||
self.assertEqual(list(protlib.search_prototype(tags="foo1")), [prot1b, prot2, prot3])
|
self.assertEqual(list(protlib.search_prototype("prot")), [prot1b, prot2, prot3])
|
||||||
|
self.assertEqual(list(protlib.search_prototype(tags="foo1")), [prot1b, prot2, prot3])
|
||||||
|
|
||||||
self.assertTrue(str(unicode(protlib.list_prototypes(self.char1))))
|
self.assertTrue(str(unicode(protlib.list_prototypes(self.char1))))
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ from django.utils.translation import ugettext as _
|
||||||
from evennia.typeclasses.models import TypeclassBase
|
from evennia.typeclasses.models import TypeclassBase
|
||||||
from evennia.scripts.models import ScriptDB
|
from evennia.scripts.models import ScriptDB
|
||||||
from evennia.scripts.manager import ScriptManager
|
from evennia.scripts.manager import ScriptManager
|
||||||
from evennia.utils import logger
|
from evennia.utils import create, logger
|
||||||
from future.utils import with_metaclass
|
from future.utils import with_metaclass
|
||||||
|
|
||||||
__all__ = ["DefaultScript", "DoNothing", "Store"]
|
__all__ = ["DefaultScript", "DoNothing", "Store"]
|
||||||
|
|
@ -324,6 +324,32 @@ class DefaultScript(ScriptBase):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create(cls, key, **kwargs):
|
||||||
|
"""
|
||||||
|
Provides a passthrough interface to the utils.create_script() function.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): Name of the new object.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
object (Object): A newly created object of the given typeclass.
|
||||||
|
errors (list): A list of errors in string form, if any.
|
||||||
|
|
||||||
|
"""
|
||||||
|
errors = []
|
||||||
|
obj = None
|
||||||
|
|
||||||
|
kwargs['key'] = key
|
||||||
|
|
||||||
|
try:
|
||||||
|
obj = create.create_script(**kwargs)
|
||||||
|
except Exception as e:
|
||||||
|
errors.append("The script '%s' encountered errors and could not be created." % key)
|
||||||
|
logger.log_err(e)
|
||||||
|
|
||||||
|
return obj, errors
|
||||||
|
|
||||||
def at_script_creation(self):
|
def at_script_creation(self):
|
||||||
"""
|
"""
|
||||||
Only called once, when script is first created.
|
Only called once, when script is first created.
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,20 @@
|
||||||
# this is an optimized version only available in later Django versions
|
# this is an optimized version only available in later Django versions
|
||||||
from unittest import TestCase
|
from unittest import TestCase
|
||||||
|
from evennia import DefaultScript
|
||||||
from evennia.scripts.models import ScriptDB, ObjectDoesNotExist
|
from evennia.scripts.models import ScriptDB, ObjectDoesNotExist
|
||||||
from evennia.utils.create import create_script
|
from evennia.utils.create import create_script
|
||||||
|
from evennia.utils.test_resources import EvenniaTest
|
||||||
from evennia.scripts.scripts import DoNothing
|
from evennia.scripts.scripts import DoNothing
|
||||||
|
|
||||||
|
|
||||||
|
class TestScript(EvenniaTest):
|
||||||
|
|
||||||
|
def test_create(self):
|
||||||
|
"Check the script can be created via the convenience method."
|
||||||
|
obj, errors = DefaultScript.create('useless-machine')
|
||||||
|
self.assertTrue(obj, errors)
|
||||||
|
self.assertFalse(errors, errors)
|
||||||
|
|
||||||
class TestScriptDB(TestCase):
|
class TestScriptDB(TestCase):
|
||||||
"Check the singleton/static ScriptDB object works correctly"
|
"Check the singleton/static ScriptDB object works correctly"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -221,6 +221,7 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
|
||||||
server_restart_mode = kwargs.get("server_restart_mode", "shutdown")
|
server_restart_mode = kwargs.get("server_restart_mode", "shutdown")
|
||||||
self.factory.server.run_init_hooks(server_restart_mode)
|
self.factory.server.run_init_hooks(server_restart_mode)
|
||||||
server_sessionhandler.portal_sessions_sync(kwargs.get("sessiondata"))
|
server_sessionhandler.portal_sessions_sync(kwargs.get("sessiondata"))
|
||||||
|
server_sessionhandler.portal_start_time = kwargs.get("portal_start_time")
|
||||||
|
|
||||||
elif operation == amp.SRELOAD: # server reload
|
elif operation == amp.SRELOAD: # server reload
|
||||||
# shut down in reload mode
|
# shut down in reload mode
|
||||||
|
|
|
||||||
|
|
@ -325,7 +325,7 @@ MENU = \
|
||||||
| 7) Kill Server only (send kill signal to process) |
|
| 7) Kill Server only (send kill signal to process) |
|
||||||
| 8) Kill Portal + Server |
|
| 8) Kill Portal + Server |
|
||||||
+--- Information -----------------------------------------------+
|
+--- Information -----------------------------------------------+
|
||||||
| 9) Tail log files (quickly see errors) |
|
| 9) Tail log files (quickly see errors - Ctrl-C to exit) |
|
||||||
| 10) Status |
|
| 10) Status |
|
||||||
| 11) Port info |
|
| 11) Port info |
|
||||||
+--- Testing ---------------------------------------------------+
|
+--- Testing ---------------------------------------------------+
|
||||||
|
|
|
||||||
|
|
@ -428,7 +428,8 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
|
||||||
self.send_AdminPortal2Server(amp.DUMMYSESSION,
|
self.send_AdminPortal2Server(amp.DUMMYSESSION,
|
||||||
amp.PSYNC,
|
amp.PSYNC,
|
||||||
server_restart_mode=server_restart_mode,
|
server_restart_mode=server_restart_mode,
|
||||||
sessiondata=sessdata)
|
sessiondata=sessdata,
|
||||||
|
portal_start_time=self.factory.portal.start_time)
|
||||||
self.factory.portal.sessions.at_server_connection()
|
self.factory.portal.sessions.at_server_connection()
|
||||||
|
|
||||||
if self.factory.server_connection:
|
if self.factory.server_connection:
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ from builtins import object
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import os
|
import os
|
||||||
|
import time
|
||||||
|
|
||||||
from os.path import dirname, abspath
|
from os.path import dirname, abspath
|
||||||
from twisted.application import internet, service
|
from twisted.application import internet, service
|
||||||
|
|
@ -114,6 +115,8 @@ class Portal(object):
|
||||||
self.server_restart_mode = "shutdown"
|
self.server_restart_mode = "shutdown"
|
||||||
self.server_info_dict = {}
|
self.server_info_dict = {}
|
||||||
|
|
||||||
|
self.start_time = time.time()
|
||||||
|
|
||||||
# in non-interactive portal mode, this gets overwritten by
|
# in non-interactive portal mode, this gets overwritten by
|
||||||
# cmdline sent by the evennia launcher
|
# cmdline sent by the evennia launcher
|
||||||
self.server_twistd_cmd = self._get_backup_server_twistd_cmd()
|
self.server_twistd_cmd = self._get_backup_server_twistd_cmd()
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
||||||
|
|
||||||
from evennia.utils.utils import delay
|
from evennia.utils.utils import delay
|
||||||
# timeout the handshakes in case the client doesn't reply at all
|
# timeout the handshakes in case the client doesn't reply at all
|
||||||
delay(2, callback=self.handshake_done, timeout=True)
|
self._handshake_delay = delay(2, callback=self.handshake_done, timeout=True)
|
||||||
|
|
||||||
# TCP/IP keepalive watches for dead links
|
# TCP/IP keepalive watches for dead links
|
||||||
self.transport.setTcpKeepAlive(1)
|
self.transport.setTcpKeepAlive(1)
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,24 @@ try:
|
||||||
except ImportError:
|
except ImportError:
|
||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
|
from mock import Mock
|
||||||
import string
|
import string
|
||||||
from evennia.server.portal import irc
|
from evennia.server.portal import irc
|
||||||
|
|
||||||
|
from twisted.conch.telnet import IAC, WILL, DONT, SB, SE, NAWS, DO
|
||||||
|
from twisted.test import proto_helpers
|
||||||
|
from twisted.trial.unittest import TestCase as TwistedTestCase
|
||||||
|
|
||||||
|
from .telnet import TelnetServerFactory, TelnetProtocol
|
||||||
|
from .portal import PORTAL_SESSIONS
|
||||||
|
from .suppress_ga import SUPPRESS_GA
|
||||||
|
from .naws import DEFAULT_HEIGHT, DEFAULT_WIDTH
|
||||||
|
from .ttype import TTYPE, IS
|
||||||
|
from .mccp import MCCP
|
||||||
|
from .mssp import MSSP
|
||||||
|
from .mxp import MXP
|
||||||
|
from .telnet_oob import MSDP, MSDP_VAL, MSDP_VAR
|
||||||
|
|
||||||
|
|
||||||
class TestIRC(TestCase):
|
class TestIRC(TestCase):
|
||||||
|
|
||||||
|
|
@ -73,3 +88,64 @@ class TestIRC(TestCase):
|
||||||
s = r'|wthis|Xis|gis|Ma|C|complex|*string'
|
s = r'|wthis|Xis|gis|Ma|C|complex|*string'
|
||||||
|
|
||||||
self.assertEqual(irc.parse_irc_to_ansi(irc.parse_ansi_to_irc(s)), s)
|
self.assertEqual(irc.parse_irc_to_ansi(irc.parse_ansi_to_irc(s)), s)
|
||||||
|
|
||||||
|
|
||||||
|
class TestTelnet(TwistedTestCase):
|
||||||
|
def setUp(self):
|
||||||
|
super(TestTelnet, self).setUp()
|
||||||
|
factory = TelnetServerFactory()
|
||||||
|
factory.protocol = TelnetProtocol
|
||||||
|
factory.sessionhandler = PORTAL_SESSIONS
|
||||||
|
factory.sessionhandler.portal = Mock()
|
||||||
|
self.proto = factory.buildProtocol(("localhost", 0))
|
||||||
|
self.transport = proto_helpers.StringTransport()
|
||||||
|
self.addCleanup(factory.sessionhandler.disconnect_all)
|
||||||
|
|
||||||
|
def test_mudlet_ttype(self):
|
||||||
|
self.transport.client = ["localhost"]
|
||||||
|
self.transport.setTcpKeepAlive = Mock()
|
||||||
|
d = self.proto.makeConnection(self.transport)
|
||||||
|
# test suppress_ga
|
||||||
|
self.assertTrue(self.proto.protocol_flags["NOGOAHEAD"])
|
||||||
|
self.proto.dataReceived(IAC + DONT + SUPPRESS_GA)
|
||||||
|
self.assertFalse(self.proto.protocol_flags["NOGOAHEAD"])
|
||||||
|
self.assertEqual(self.proto.handshakes, 7)
|
||||||
|
# test naws
|
||||||
|
self.assertEqual(self.proto.protocol_flags['SCREENWIDTH'], {0: DEFAULT_WIDTH})
|
||||||
|
self.assertEqual(self.proto.protocol_flags['SCREENHEIGHT'], {0: DEFAULT_HEIGHT})
|
||||||
|
self.proto.dataReceived(IAC + WILL + NAWS)
|
||||||
|
self.proto.dataReceived([IAC, SB, NAWS, '', 'x', '', 'd', IAC, SE])
|
||||||
|
self.assertEqual(self.proto.protocol_flags['SCREENWIDTH'][0], 120)
|
||||||
|
self.assertEqual(self.proto.protocol_flags['SCREENHEIGHT'][0], 100)
|
||||||
|
self.assertEqual(self.proto.handshakes, 6)
|
||||||
|
# test ttype
|
||||||
|
self.assertTrue(self.proto.protocol_flags["FORCEDENDLINE"])
|
||||||
|
self.assertFalse(self.proto.protocol_flags["TTYPE"])
|
||||||
|
self.assertTrue(self.proto.protocol_flags["ANSI"])
|
||||||
|
self.proto.dataReceived(IAC + WILL + TTYPE)
|
||||||
|
self.proto.dataReceived([IAC, SB, TTYPE, IS, "MUDLET", IAC, SE])
|
||||||
|
self.assertTrue(self.proto.protocol_flags["XTERM256"])
|
||||||
|
self.assertEqual(self.proto.protocol_flags["CLIENTNAME"], "MUDLET")
|
||||||
|
self.proto.dataReceived([IAC, SB, TTYPE, IS, "XTERM", IAC, SE])
|
||||||
|
self.proto.dataReceived([IAC, SB, TTYPE, IS, "MTTS 137", IAC, SE])
|
||||||
|
self.assertEqual(self.proto.handshakes, 5)
|
||||||
|
# test mccp
|
||||||
|
self.proto.dataReceived(IAC + DONT + MCCP)
|
||||||
|
self.assertFalse(self.proto.protocol_flags['MCCP'])
|
||||||
|
self.assertEqual(self.proto.handshakes, 4)
|
||||||
|
# test mssp
|
||||||
|
self.proto.dataReceived(IAC + DONT + MSSP)
|
||||||
|
self.assertEqual(self.proto.handshakes, 3)
|
||||||
|
# test oob
|
||||||
|
self.proto.dataReceived(IAC + DO + MSDP)
|
||||||
|
self.proto.dataReceived([IAC, SB, MSDP, MSDP_VAR, "LIST", MSDP_VAL, "COMMANDS", IAC, SE])
|
||||||
|
self.assertTrue(self.proto.protocol_flags['OOB'])
|
||||||
|
self.assertEqual(self.proto.handshakes, 2)
|
||||||
|
# test mxp
|
||||||
|
self.proto.dataReceived(IAC + DONT + MXP)
|
||||||
|
self.assertFalse(self.proto.protocol_flags['MXP'])
|
||||||
|
self.assertEqual(self.proto.handshakes, 1)
|
||||||
|
# clean up to prevent Unclean reactor
|
||||||
|
self.proto.nop_keep_alive.stop()
|
||||||
|
self.proto._handshake_delay.cancel()
|
||||||
|
return d
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,14 @@ import time
|
||||||
# TODO!
|
# TODO!
|
||||||
#sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
|
#sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))))
|
||||||
#os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings'
|
#os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings'
|
||||||
import ev
|
import evennia
|
||||||
from evennia.utils.idmapper import base as _idmapper
|
from evennia.utils.idmapper import models as _idmapper
|
||||||
|
|
||||||
LOGFILE = "logs/memoryusage.log"
|
LOGFILE = "logs/memoryusage.log"
|
||||||
INTERVAL = 30 # log every 30 seconds
|
INTERVAL = 30 # log every 30 seconds
|
||||||
|
|
||||||
|
|
||||||
class Memplot(ev.Script):
|
class Memplot(evennia.DefaultScript):
|
||||||
"""
|
"""
|
||||||
Describes a memory plotting action.
|
Describes a memory plotting action.
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
from django.test import TestCase
|
from django.test import TestCase
|
||||||
from mock import Mock
|
from mock import Mock, patch, mock_open
|
||||||
from .dummyrunner_settings import (c_creates_button, c_creates_obj, c_digs, c_examines, c_help, c_idles, c_login,
|
from .dummyrunner_settings import (c_creates_button, c_creates_obj, c_digs, c_examines, c_help, c_idles, c_login,
|
||||||
c_login_nodig, c_logout, c_looks, c_moves, c_moves_n, c_moves_s, c_socialize)
|
c_login_nodig, c_logout, c_looks, c_moves, c_moves_n, c_moves_s, c_socialize)
|
||||||
|
import memplot
|
||||||
|
|
||||||
|
|
||||||
class TestDummyrunnerSettings(TestCase):
|
class TestDummyrunnerSettings(TestCase):
|
||||||
|
|
@ -91,3 +92,21 @@ class TestDummyrunnerSettings(TestCase):
|
||||||
|
|
||||||
def test_c_move_s(self):
|
def test_c_move_s(self):
|
||||||
self.assertEqual(c_moves_s(self.client), "south")
|
self.assertEqual(c_moves_s(self.client), "south")
|
||||||
|
|
||||||
|
|
||||||
|
class TestMemPlot(TestCase):
|
||||||
|
@patch.object(memplot, "_idmapper")
|
||||||
|
@patch.object(memplot, "os")
|
||||||
|
@patch.object(memplot, "open", new_callable=mock_open, create=True)
|
||||||
|
@patch.object(memplot, "time")
|
||||||
|
def test_memplot(self, mock_time, mocked_open, mocked_os, mocked_idmapper):
|
||||||
|
from evennia.utils.create import create_script
|
||||||
|
mocked_idmapper.cache_size.return_value = (9, 5000)
|
||||||
|
mock_time.time = Mock(return_value=6000.0)
|
||||||
|
script = create_script(memplot.Memplot)
|
||||||
|
script.db.starttime = 0.0
|
||||||
|
mocked_os.popen.read.return_value = 5000.0
|
||||||
|
script.at_repeat()
|
||||||
|
handle = mocked_open()
|
||||||
|
handle.write.assert_called_with('100.0, 0.001, 0.001, 9\n')
|
||||||
|
script.stop()
|
||||||
|
|
|
||||||
|
|
@ -407,7 +407,7 @@ class ServerSession(Session):
|
||||||
else:
|
else:
|
||||||
self.data_out(**kwargs)
|
self.data_out(**kwargs)
|
||||||
|
|
||||||
def execute_cmd(self, raw_string, **kwargs):
|
def execute_cmd(self, raw_string, session=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Do something as this object. This method is normally never
|
Do something as this object. This method is normally never
|
||||||
called directly, instead incoming command instructions are
|
called directly, instead incoming command instructions are
|
||||||
|
|
@ -417,6 +417,9 @@ class ServerSession(Session):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
raw_string (string): Raw command input
|
raw_string (string): Raw command input
|
||||||
|
session (Session): This is here to make API consistent with
|
||||||
|
Account/Object.execute_cmd. If given, data is passed to
|
||||||
|
that Session, otherwise use self.
|
||||||
Kwargs:
|
Kwargs:
|
||||||
Other keyword arguments will be added to the found command
|
Other keyword arguments will be added to the found command
|
||||||
object instace as variables before it executes. This is
|
object instace as variables before it executes. This is
|
||||||
|
|
@ -426,7 +429,7 @@ class ServerSession(Session):
|
||||||
"""
|
"""
|
||||||
# inject instruction into input stream
|
# inject instruction into input stream
|
||||||
kwargs["text"] = ((raw_string,), {})
|
kwargs["text"] = ((raw_string,), {})
|
||||||
self.sessionhandler.data_in(self, **kwargs)
|
self.sessionhandler.data_in(session or self, **kwargs)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
"""Handle session comparisons"""
|
"""Handle session comparisons"""
|
||||||
|
|
|
||||||
|
|
@ -280,6 +280,8 @@ class ServerSessionHandler(SessionHandler):
|
||||||
super(ServerSessionHandler, self).__init__(*args, **kwargs)
|
super(ServerSessionHandler, self).__init__(*args, **kwargs)
|
||||||
self.server = None # set at server initialization
|
self.server = None # set at server initialization
|
||||||
self.server_data = {"servername": _SERVERNAME}
|
self.server_data = {"servername": _SERVERNAME}
|
||||||
|
# will be set on psync
|
||||||
|
self.portal_start_time = 0.0
|
||||||
|
|
||||||
def _run_cmd_login(self, session):
|
def _run_cmd_login(self, session):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,8 @@
|
||||||
from collections import defaultdict, deque
|
from collections import defaultdict, deque
|
||||||
|
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.
|
||||||
|
|
@ -50,23 +52,35 @@ class Throttle(object):
|
||||||
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):
|
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
|
||||||
|
of throttle.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
None
|
None
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# Get current status
|
||||||
|
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
|
||||||
|
currently_throttled = self.check(ip)
|
||||||
|
|
||||||
|
# If this makes it engage, log a single activation event
|
||||||
|
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))
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -97,5 +111,3 @@ class Throttle(object):
|
||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,10 +1,49 @@
|
||||||
|
from django.conf import settings
|
||||||
from django.core.exceptions import ValidationError
|
from django.core.exceptions import ValidationError
|
||||||
from django.utils.translation import gettext as _
|
from django.utils.translation import gettext as _
|
||||||
|
from evennia.accounts.models import AccountDB
|
||||||
import re
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
class EvenniaUsernameAvailabilityValidator:
|
||||||
|
"""
|
||||||
|
Checks to make sure a given username is not taken or otherwise reserved.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __call__(self, username):
|
||||||
|
"""
|
||||||
|
Validates a username to make sure it is not in use or reserved.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
username (str): Username to validate
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
None (None): None if password successfully validated,
|
||||||
|
raises ValidationError otherwise.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Check guest list
|
||||||
|
if (settings.GUEST_LIST and username.lower() in (guest.lower() for guest in settings.GUEST_LIST)):
|
||||||
|
raise ValidationError(
|
||||||
|
_('Sorry, that username is reserved.'),
|
||||||
|
code='evennia_username_reserved',
|
||||||
|
)
|
||||||
|
|
||||||
|
# Check database
|
||||||
|
exists = AccountDB.objects.filter(username__iexact=username).exists()
|
||||||
|
if exists:
|
||||||
|
raise ValidationError(
|
||||||
|
_('Sorry, that username is already 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.
|
||||||
|
|
||||||
|
|
@ -47,5 +86,6 @@ class EvenniaPasswordValidator:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
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"
|
||||||
|
|
@ -813,6 +816,15 @@ AUTH_PASSWORD_VALIDATORS = [
|
||||||
{'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
|
||||||
|
AUTH_USERNAME_VALIDATORS = [
|
||||||
|
{'NAME': 'django.contrib.auth.validators.ASCIIUsernameValidator'},
|
||||||
|
{'NAME': 'django.core.validators.MinLengthValidator',
|
||||||
|
'OPTIONS': {'limit_value': 3}},
|
||||||
|
{'NAME': 'django.core.validators.MaxLengthValidator',
|
||||||
|
'OPTIONS': {'limit_value': 30}},
|
||||||
|
{'NAME': 'evennia.server.validators.EvenniaUsernameAvailabilityValidator'}]
|
||||||
|
|
||||||
# Use a custom test runner that just tests Evennia-specific apps.
|
# Use a custom test runner that just tests Evennia-specific apps.
|
||||||
TEST_RUNNER = 'evennia.server.tests.EvenniaTestSuiteRunner'
|
TEST_RUNNER = 'evennia.server.tests.EvenniaTestSuiteRunner'
|
||||||
|
|
||||||
|
|
@ -823,7 +835,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.
|
||||||
|
|
|
||||||
|
|
@ -668,7 +668,7 @@ class AttributeHandler(object):
|
||||||
|
|
||||||
def all(self, accessing_obj=None, default_access=True):
|
def all(self, accessing_obj=None, default_access=True):
|
||||||
"""
|
"""
|
||||||
Return all Attribute objects on this object.
|
Return all Attribute objects on this object, regardless of category.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
accessing_obj (object, optional): Check the `attrread`
|
accessing_obj (object, optional): Check the `attrread`
|
||||||
|
|
|
||||||
|
|
@ -31,9 +31,12 @@ from django.db.models import signals
|
||||||
|
|
||||||
from django.db.models.base import ModelBase
|
from django.db.models.base import ModelBase
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.contrib.contenttypes.models import ContentType
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.urls import reverse
|
||||||
from django.utils.encoding import smart_str
|
from django.utils.encoding import smart_str
|
||||||
|
from django.utils.text import slugify
|
||||||
|
|
||||||
from evennia.typeclasses.attributes import Attribute, AttributeHandler, NAttributeHandler
|
from evennia.typeclasses.attributes import Attribute, AttributeHandler, NAttributeHandler
|
||||||
from evennia.typeclasses.tags import Tag, TagHandler, AliasHandler, PermissionHandler
|
from evennia.typeclasses.tags import Tag, TagHandler, AliasHandler, PermissionHandler
|
||||||
|
|
@ -733,3 +736,150 @@ class TypedObject(SharedMemoryModel):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
#
|
||||||
|
# Web/Django methods
|
||||||
|
#
|
||||||
|
|
||||||
|
def web_get_admin_url(self):
|
||||||
|
"""
|
||||||
|
Returns the URI path for the Django Admin page for this object.
|
||||||
|
|
||||||
|
ex. Account#1 = '/admin/accounts/accountdb/1/change/'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
path (str): URI path to Django Admin page for object.
|
||||||
|
|
||||||
|
"""
|
||||||
|
content_type = ContentType.objects.get_for_model(self.__class__)
|
||||||
|
return reverse("admin:%s_%s_change" % (content_type.app_label,
|
||||||
|
content_type.model), args=(self.id,))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def web_get_create_url(cls):
|
||||||
|
"""
|
||||||
|
Returns the URI path for a View that allows users to create new
|
||||||
|
instances of this object.
|
||||||
|
|
||||||
|
ex. Chargen = '/characters/create/'
|
||||||
|
|
||||||
|
For this to work, the developer must have defined a named view somewhere
|
||||||
|
in urls.py that follows the format 'modelname-action', so in this case
|
||||||
|
a named view of 'character-create' would be referenced by this method.
|
||||||
|
|
||||||
|
ex.
|
||||||
|
url(r'characters/create/', ChargenView.as_view(), name='character-create')
|
||||||
|
|
||||||
|
If no View has been created and defined in urls.py, returns an
|
||||||
|
HTML anchor.
|
||||||
|
|
||||||
|
This method is naive and simply returns a path. Securing access to
|
||||||
|
the actual view and limiting who can create new objects is the
|
||||||
|
developer's responsibility.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
path (str): URI path to object creation page, if defined.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return reverse('%s-create' % cls._meta.verbose_name.lower())
|
||||||
|
except:
|
||||||
|
return '#'
|
||||||
|
|
||||||
|
def web_get_detail_url(self):
|
||||||
|
"""
|
||||||
|
Returns the URI path for a View that allows users to view details for
|
||||||
|
this object.
|
||||||
|
|
||||||
|
ex. Oscar (Character) = '/characters/oscar/1/'
|
||||||
|
|
||||||
|
For this to work, the developer must have defined a named view somewhere
|
||||||
|
in urls.py that follows the format 'modelname-action', so in this case
|
||||||
|
a named view of 'character-detail' would be referenced by this method.
|
||||||
|
|
||||||
|
ex.
|
||||||
|
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/$',
|
||||||
|
CharDetailView.as_view(), name='character-detail')
|
||||||
|
|
||||||
|
If no View has been created and defined in urls.py, returns an
|
||||||
|
HTML anchor.
|
||||||
|
|
||||||
|
This method is naive and simply returns a path. Securing access to
|
||||||
|
the actual view and limiting who can view this object is the developer's
|
||||||
|
responsibility.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
path (str): URI path to object detail page, if defined.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return reverse('%s-detail' % self._meta.verbose_name.lower(),
|
||||||
|
kwargs={'pk': self.pk, 'slug': slugify(self.name)})
|
||||||
|
except:
|
||||||
|
return '#'
|
||||||
|
|
||||||
|
def web_get_update_url(self):
|
||||||
|
"""
|
||||||
|
Returns the URI path for a View that allows users to update this
|
||||||
|
object.
|
||||||
|
|
||||||
|
ex. Oscar (Character) = '/characters/oscar/1/change/'
|
||||||
|
|
||||||
|
For this to work, the developer must have defined a named view somewhere
|
||||||
|
in urls.py that follows the format 'modelname-action', so in this case
|
||||||
|
a named view of 'character-update' would be referenced by this method.
|
||||||
|
|
||||||
|
ex.
|
||||||
|
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/change/$',
|
||||||
|
CharUpdateView.as_view(), name='character-update')
|
||||||
|
|
||||||
|
If no View has been created and defined in urls.py, returns an
|
||||||
|
HTML anchor.
|
||||||
|
|
||||||
|
This method is naive and simply returns a path. Securing access to
|
||||||
|
the actual view and limiting who can modify objects is the developer's
|
||||||
|
responsibility.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
path (str): URI path to object update page, if defined.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return reverse('%s-update' % self._meta.verbose_name.lower(),
|
||||||
|
kwargs={'pk': self.pk, 'slug': slugify(self.name)})
|
||||||
|
except:
|
||||||
|
return '#'
|
||||||
|
|
||||||
|
def web_get_delete_url(self):
|
||||||
|
"""
|
||||||
|
Returns the URI path for a View that allows users to delete this object.
|
||||||
|
|
||||||
|
ex. Oscar (Character) = '/characters/oscar/1/delete/'
|
||||||
|
|
||||||
|
For this to work, the developer must have defined a named view somewhere
|
||||||
|
in urls.py that follows the format 'modelname-action', so in this case
|
||||||
|
a named view of 'character-detail' would be referenced by this method.
|
||||||
|
|
||||||
|
ex.
|
||||||
|
url(r'characters/(?P<slug>[\w\d\-]+)/(?P<pk>[0-9]+)/delete/$',
|
||||||
|
CharDeleteView.as_view(), name='character-delete')
|
||||||
|
|
||||||
|
If no View has been created and defined in urls.py, returns an
|
||||||
|
HTML anchor.
|
||||||
|
|
||||||
|
This method is naive and simply returns a path. Securing access to
|
||||||
|
the actual view and limiting who can delete this object is the developer's
|
||||||
|
responsibility.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
path (str): URI path to object deletion page, if defined.
|
||||||
|
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
return reverse('%s-delete' % self._meta.verbose_name.lower(),
|
||||||
|
kwargs={'pk': self.pk, 'slug': slugify(self.name)})
|
||||||
|
except:
|
||||||
|
return '#'
|
||||||
|
|
||||||
|
# Used by Django Sites/Admin
|
||||||
|
get_absolute_url = web_get_detail_url
|
||||||
|
|
|
||||||
|
|
@ -345,13 +345,14 @@ class TagHandler(object):
|
||||||
self._catcache = {}
|
self._catcache = {}
|
||||||
self._cache_complete = False
|
self._cache_complete = False
|
||||||
|
|
||||||
def all(self, return_key_and_category=False):
|
def all(self, return_key_and_category=False, return_objs=False):
|
||||||
"""
|
"""
|
||||||
Get all tags in this handler, regardless of category.
|
Get all tags in this handler, regardless of category.
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
return_key_and_category (bool, optional): Return a list of
|
return_key_and_category (bool, optional): Return a list of
|
||||||
tuples `[(key, category), ...]`.
|
tuples `[(key, category), ...]`.
|
||||||
|
return_objs (bool, optional): Return tag objects.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
tags (list): A list of tag keys `[tagkey, tagkey, ...]` or
|
tags (list): A list of tag keys `[tagkey, tagkey, ...]` or
|
||||||
|
|
@ -365,6 +366,8 @@ class TagHandler(object):
|
||||||
if return_key_and_category:
|
if return_key_and_category:
|
||||||
# return tuple (key, category)
|
# return tuple (key, category)
|
||||||
return [(to_str(tag.db_key), to_str(tag.db_category)) for tag in tags]
|
return [(to_str(tag.db_key), to_str(tag.db_category)) for tag in tags]
|
||||||
|
elif return_objs:
|
||||||
|
return tags
|
||||||
else:
|
else:
|
||||||
return [to_str(tag.db_key) for tag in tags]
|
return [to_str(tag.db_key) for tag in tags]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -107,6 +107,17 @@ def uptime():
|
||||||
return time.time() - SERVER_START_TIME
|
return time.time() - SERVER_START_TIME
|
||||||
|
|
||||||
|
|
||||||
|
def portal_uptime():
|
||||||
|
"""
|
||||||
|
Get the current uptime of the portal.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
time (float): The uptime of the portal.
|
||||||
|
"""
|
||||||
|
from evennia.server.sessionhandler import SESSIONS
|
||||||
|
return time.time() - SESSIONS.portal_start_time
|
||||||
|
|
||||||
|
|
||||||
def game_epoch():
|
def game_epoch():
|
||||||
"""
|
"""
|
||||||
Get the game epoch.
|
Get the game epoch.
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ let defaultin_plugin = (function () {
|
||||||
//
|
//
|
||||||
// handle the default <enter> key triggering onSend()
|
// handle the default <enter> key triggering onSend()
|
||||||
var onKeydown = function (event) {
|
var onKeydown = function (event) {
|
||||||
|
$("#inputfield").focus();
|
||||||
if ( (event.which === 13) && (!event.shiftKey) ) { // Enter Key without shift
|
if ( (event.which === 13) && (!event.shiftKey) ) { // Enter Key without shift
|
||||||
var inputfield = $("#inputfield");
|
var inputfield = $("#inputfield");
|
||||||
var outtext = inputfield.val();
|
var outtext = inputfield.val();
|
||||||
|
|
|
||||||
|
|
@ -43,14 +43,6 @@ let history_plugin = (function () {
|
||||||
history_pos = 0;
|
history_pos = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
|
||||||
// Go to the last history line
|
|
||||||
var end = function () {
|
|
||||||
// move to the end of the history stack
|
|
||||||
history_pos = 0;
|
|
||||||
return history[history.length -1];
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
//
|
||||||
// Add input to the scratch line
|
// Add input to the scratch line
|
||||||
var scratch = function (input) {
|
var scratch = function (input) {
|
||||||
|
|
@ -69,27 +61,20 @@ let history_plugin = (function () {
|
||||||
var history_entry = null;
|
var history_entry = null;
|
||||||
var inputfield = $("#inputfield");
|
var inputfield = $("#inputfield");
|
||||||
|
|
||||||
if (inputfield[0].selectionStart == inputfield.val().length) {
|
if (code === 38) { // Arrow up
|
||||||
// Only process up/down arrow if cursor is at the end of the line.
|
history_entry = back();
|
||||||
if (code === 38) { // Arrow up
|
}
|
||||||
history_entry = back();
|
else if (code === 40) { // Arrow down
|
||||||
}
|
history_entry = fwd();
|
||||||
else if (code === 40) { // Arrow down
|
|
||||||
history_entry = fwd();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (history_entry !== null) {
|
if (history_entry !== null) {
|
||||||
// Doing a history navigation; replace the text in the input.
|
// Performing a history navigation
|
||||||
inputfield.val(history_entry);
|
// replace the text in the input and move the cursor to the end of the new value
|
||||||
}
|
inputfield.val('');
|
||||||
else {
|
inputfield.blur().focus().val(history_entry);
|
||||||
// Save the current contents of the input to the history scratch area.
|
event.preventDefault();
|
||||||
setTimeout(function () {
|
return true;
|
||||||
// Need to wait until after the key-up to capture the value.
|
|
||||||
scratch(inputfield.val());
|
|
||||||
end();
|
|
||||||
}, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
|
|
@ -99,6 +84,7 @@ let history_plugin = (function () {
|
||||||
// Listen for onSend lines to add to history
|
// Listen for onSend lines to add to history
|
||||||
var onSend = function (line) {
|
var onSend = function (line) {
|
||||||
add(line);
|
add(line);
|
||||||
|
return null; // we are not returning an altered input line
|
||||||
}
|
}
|
||||||
|
|
||||||
//
|
//
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ pypiwin32
|
||||||
|
|
||||||
django > 1.11, < 2.0
|
django > 1.11, < 2.0
|
||||||
twisted >= 18.0.0, < 19.0.0
|
twisted >= 18.0.0, < 19.0.0
|
||||||
pillow == 2.9.0
|
pillow == 5.2.0
|
||||||
pytz
|
pytz
|
||||||
future >= 0.15.2
|
future >= 0.15.2
|
||||||
django-sekizai
|
django-sekizai
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue