First step with both account+player at the same time, copying player to account.

This commit is contained in:
Griatch 2017-07-05 08:30:06 +02:00
parent 99dbaad7ba
commit ee0e9cc053
9 changed files with 2112 additions and 0 deletions

View file

@ -0,0 +1,6 @@
"""
This sub-package defines the out-of-character entities known as
Accounts. These are equivalent to 'accounts' and can puppet one or
more Objects depending on settings. An Account has no in-game existence.
"""

View file

@ -0,0 +1,971 @@
"""
Typeclass for Account objects
Note that this object is primarily intended to
store OOC information, not game info! This
object represents the actual user (not their
character) and has NO actual precence in the
game world (this is handled by the associated
character object, so you should customize that
instead for most things).
"""
import time
from django.conf import settings
from django.utils import timezone
from evennia.typeclasses.models import TypeclassBase
from evennia.accounts.manager import AccountManager
from evennia.accounts.models import AccountDB
from evennia.objects.models import ObjectDB
from evennia.comms.models import ChannelDB
from evennia.commands import cmdhandler
from evennia.utils import logger
from evennia.utils.utils import (lazy_property,
make_iter, to_unicode, is_iter,
variable_from_module)
from evennia.typeclasses.attributes import NickHandler
from evennia.scripts.scripthandler import ScriptHandler
from evennia.commands.cmdsethandler import CmdSetHandler
from django.utils.translation import ugettext as _
from future.utils import with_metaclass
__all__ = ("DefaultAccount",)
_SESSIONS = None
_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
_MULTISESSION_MODE = settings.MULTISESSION_MODE
_MAX_NR_CHARACTERS = settings.MAX_NR_CHARACTERS
_CMDSET_ACCOUNT = settings.CMDSET_ACCOUNT
_CONNECT_CHANNEL = None
class AccountSessionHandler(object):
"""
Manages the session(s) attached to an account.
"""
def __init__(self, account):
"""
Initializes the handler.
Args:
account (Account): The Account on which this handler is defined.
"""
self.account = account
def get(self, sessid=None):
"""
Get the sessions linked to this object.
Args:
sessid (int, optional): Specify a given session by
session id.
Returns:
sessions (list): A list of Session objects. If `sessid`
is given, this is a list with one (or zero) elements.
"""
global _SESSIONS
if not _SESSIONS:
from evennia.server.sessionhandler import SESSIONS as _SESSIONS
if sessid:
return make_iter(_SESSIONS.session_from_account(self.account, sessid))
else:
return _SESSIONS.sessions_from_account(self.account)
def all(self):
"""
Alias to get(), returning all sessions.
Returns:
sessions (list): All sessions.
"""
return self.get()
def count(self):
"""
Get amount of sessions connected.
Returns:
sesslen (int): Number of sessions handled.
"""
return len(self.get())
class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
"""
This is the base Typeclass for all Accounts. Accounts represent
the person playing the game and tracks account info, password
etc. They are OOC entities without presence in-game. An Account
can connect to a Character Object in order to "enter" the
game.
Account Typeclass API:
* Available properties (only available on initiated typeclass objects)
- key (string) - name of account
- name (string)- wrapper for user.username
- aliases (list of strings) - aliases to the object. Will be saved to
database as AliasDB entries but returned as strings.
- dbref (int, read-only) - unique #id-number. Also "id" can be used.
- date_created (string) - time stamp of object creation
- permissions (list of strings) - list of permission strings
- user (User, read-only) - django User authorization object
- obj (Object) - game object controlled by account. 'character' can also
be used.
- sessions (list of Sessions) - sessions connected to this account
- is_superuser (bool, read-only) - if the connected user is a superuser
* Handlers
- locks - lock-handler: use locks.add() to add new lock strings
- db - attribute-handler: store/retrieve database attributes on this
self.db.myattr=val, val=self.db.myattr
- ndb - non-persistent attribute handler: same as db but does not
create a database entry when storing data
- scripts - script-handler. Add new scripts to object with scripts.add()
- cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object
- nicks - nick-handler. New nicks with nicks.add().
* Helper methods
- msg(text=None, from_obj=None, session=None, options=None, **kwargs)
- execute_cmd(raw_string)
- search(ostring, global_search=False, attribute_name=None,
use_nicks=False, location=None,
ignore_errors=False, account=False)
- is_typeclass(typeclass, exact=False)
- swap_typeclass(new_typeclass, clean_attributes=False, no_default=True)
- access(accessing_obj, access_type='read', default=False, no_superuser_bypass=False)
- check_permstring(permstring)
* Hook methods
basetype_setup()
at_account_creation()
> note that the following hooks are also found on Objects and are
usually handled on the character level:
- at_init()
- at_access()
- at_cmdset_get(**kwargs)
- at_first_login()
- at_post_login(session=None)
- at_disconnect()
- at_message_receive()
- at_message_send()
- at_server_reload()
- at_server_shutdown()
"""
objects = AccountManager()
# properties
@lazy_property
def cmdset(self):
return CmdSetHandler(self, True)
@lazy_property
def scripts(self):
return ScriptHandler(self)
@lazy_property
def nicks(self):
return NickHandler(self)
@lazy_property
def sessions(self):
return AccountSessionHandler(self)
# session-related methods
def disconnect_session_from_account(self, session):
"""
Access method for disconnecting a given session from the
account (connection happens automatically in the
sessionhandler)
Args:
session (Session): Session to disconnect.
"""
global _SESSIONS
if not _SESSIONS:
from evennia.server.sessionhandler import SESSIONS as _SESSIONS
_SESSIONS.disconnect(session)
# puppeting operations
def puppet_object(self, session, obj):
"""
Use the given session to control (puppet) the given object (usually
a Character type).
Args:
session (Session): session to use for puppeting
obj (Object): the object to start puppeting
Raises:
RuntimeError: If puppeting is not possible, the
`exception.msg` will contain the reason.
"""
# safety checks
if not obj:
raise RuntimeError("Object not found")
if not session:
raise RuntimeError("Session not found")
if self.get_puppet(session) == obj:
# already puppeting this object
self.msg("You are already puppeting this object.")
return
if not obj.access(self, 'puppet'):
# no access
self.msg("You don't have permission to puppet '%s'." % obj.key)
return
if obj.account:
# object already puppeted
if obj.account == self:
if obj.sessions.count():
# we may take over another of our sessions
# output messages to the affected sessions
if _MULTISESSION_MODE in (1, 3):
txt1 = "Sharing |c%s|n with another of your sessions."
txt2 = "|c%s|n|G is now shared from another of your sessions.|n"
self.msg(txt1 % obj.name, session=session)
self.msg(txt2 % obj.name, session=obj.sessions.all())
else:
txt1 = "Taking over |c%s|n from another of your sessions."
txt2 = "|c%s|n|R is now acted from another of your sessions.|n"
self.msg(txt1 % obj.name, session=session)
self.msg(txt2 % obj.name, session=obj.sessions.all())
self.unpuppet_object(obj.sessions.get())
elif obj.account.is_connected:
# controlled by another account
self.msg("|c%s|R is already puppeted by another Account." % obj.key)
return
# do the puppeting
if session.puppet:
# cleanly unpuppet eventual previous object puppeted by this session
self.unpuppet_object(session)
# if we get to this point the character is ready to puppet or it
# was left with a lingering account/session reference from an unclean
# server kill or similar
obj.at_pre_puppet(self, session=session)
# do the connection
obj.sessions.add(session)
obj.account = self
session.puid = obj.id
session.puppet = obj
# validate/start persistent scripts on object
obj.scripts.validate()
# re-cache locks to make sure superuser bypass is updated
obj.locks.cache_lock_bypass(obj)
# final hook
obj.at_post_puppet()
def unpuppet_object(self, session):
"""
Disengage control over an object.
Args:
session (Session or list): The session or a list of
sessions to disengage from their puppets.
Raises:
RuntimeError With message about error.
"""
for session in make_iter(session):
obj = session.puppet
if obj:
# do the disconnect, but only if we are the last session to puppet
obj.at_pre_unpuppet()
obj.sessions.remove(session)
if not obj.sessions.count():
del obj.account
obj.at_post_unpuppet(self, session=session)
# Just to be sure we're always clear.
session.puppet = None
session.puid = None
def unpuppet_all(self):
"""
Disconnect all puppets. This is called by server before a
reset/shutdown.
"""
self.unpuppet_object(self.sessions.all())
def get_puppet(self, session):
"""
Get an object puppeted by this session through this account. This is
the main method for retrieving the puppeted object from the
account's end.
Args:
session (Session): Find puppeted object based on this session
Returns:
puppet (Object): The matching puppeted object, if any.
"""
return session.puppet
def get_all_puppets(self):
"""
Get all currently puppeted objects.
Returns:
puppets (list): All puppeted objects currently controlled
by this Account.
"""
return list(set(session.puppet for session in self.sessions.all() if session.puppet))
def __get_single_puppet(self):
"""
This is a legacy convenience link for use with `MULTISESSION_MODE`.
Returns:
puppets (Object or list): Users of `MULTISESSION_MODE` 0 or 1 will
always get the first puppet back. Users of higher `MULTISESSION_MODE`s will
get a list of all puppeted objects.
"""
puppets = self.get_all_puppets()
if _MULTISESSION_MODE in (0, 1):
return puppets and puppets[0] or None
return puppets
character = property(__get_single_puppet)
puppet = property(__get_single_puppet)
# utility methods
def delete(self, *args, **kwargs):
"""
Deletes the account permanently.
Notes:
`*args` and `**kwargs` are passed on to the base delete
mechanism (these are usually not used).
"""
for session in self.sessions.all():
# unpuppeting all objects and disconnecting the user, if any
# sessions remain (should usually be handled from the
# deleting command)
try:
self.unpuppet_object(session)
except RuntimeError:
# no puppet to disconnect from
pass
session.sessionhandler.disconnect(session, reason=_("Account being deleted."))
self.scripts.stop()
self.attributes.clear()
self.nicks.clear()
self.aliases.clear()
super(DefaultAccount, self).delete(*args, **kwargs)
# methods inherited from database model
def msg(self, text=None, from_obj=None, session=None, options=None, **kwargs):
"""
Evennia -> User
This is the main route for sending data back to the user from the
server.
Args:
text (str, optional): text data to send
from_obj (Object or Account, optional): Object sending. If given,
its at_msg_send() hook will be called.
session (Session or list, optional): Session object or a list of
Sessions to receive this send. If given, overrules the
default send behavior for the current
MULTISESSION_MODE.
options (list): Protocol-specific options. Passed on to the protocol.
Kwargs:
any (dict): All other keywords are passed on to the protocol.
"""
if from_obj:
# call hook
try:
from_obj.at_msg_send(text=text, to_obj=self, **kwargs)
except Exception:
# this may not be assigned.
pass
try:
if not self.at_msg_receive(text=text, **kwargs):
# abort message to this account
return
except Exception:
# this may not be assigned.
pass
kwargs["options"] = options
# session relay
sessions = make_iter(session) if session else self.sessions.all()
for session in sessions:
session.data_out(text=text, **kwargs)
def execute_cmd(self, raw_string, session=None, **kwargs):
"""
Do something as this account. This method is never called normally,
but only when the account object itself is supposed to execute the
command. It takes account nicks into account, but not nicks of
eventual puppets.
Args:
raw_string (str): Raw command input coming from the command line.
session (Session, optional): The session to be responsible
for the command-send
Kwargs:
kwargs (any): Other keyword arguments will be added to the
found command object instance as variables before it
executes. This is unused by default Evennia but may be
used to set flags and change operating paramaters for
commands at run-time.
"""
raw_string = to_unicode(raw_string)
raw_string = self.nicks.nickreplace(raw_string, categories=("inputline", "channel"), include_account=False)
if not session and _MULTISESSION_MODE in (0, 1):
# for these modes we use the first/only session
sessions = self.sessions.get()
session = sessions[0] if sessions else None
return cmdhandler.cmdhandler(self, raw_string,
callertype="account", session=session, **kwargs)
def search(self, searchdata, return_puppet=False, search_object=False,
typeclass=None, nofound_string=None, multimatch_string=None, **kwargs):
"""
This is similar to `DefaultObject.search` but defaults to searching
for Accounts only.
Args:
searchdata (str or int): Search criterion, the Account's
key or dbref to search for.
return_puppet (bool, optional): Instructs the method to
return matches as the object the Account controls rather
than the Account itself (or None) if nothing is puppeted).
search_object (bool, optional): Search for Objects instead of
Accounts. This is used by e.g. the @examine command when
wanting to examine Objects while OOC.
typeclass (Account typeclass, optional): Limit the search
only to this particular typeclass. This can be used to
limit to specific account typeclasses or to limit the search
to a particular Object typeclass if `search_object` is True.
nofound_string (str, optional): A one-time error message
to echo if `searchdata` leads to no matches. If not given,
will fall back to the default handler.
multimatch_string (str, optional): A one-time error
message to echo if `searchdata` leads to multiple matches.
If not given, will fall back to the default handler.
Return:
match (Account, Object or None): A single Account or Object match.
Notes:
Extra keywords are ignored, but are allowed in call in
order to make API more consistent with
objects.objects.DefaultObject.search.
"""
# handle me, self and *me, *self
if isinstance(searchdata, basestring):
# handle wrapping of common terms
if searchdata.lower() in ("me", "*me", "self", "*self",):
return self
if search_object:
matches = ObjectDB.objects.object_search(searchdata, typeclass=typeclass)
else:
matches = AccountDB.objects.account_search(searchdata, typeclass=typeclass)
matches = _AT_SEARCH_RESULT(matches, self, query=searchdata,
nofound_string=nofound_string,
multimatch_string=multimatch_string)
if matches and return_puppet:
try:
return matches.puppet
except AttributeError:
return None
return matches
def access(self, accessing_obj, access_type='read', default=False, no_superuser_bypass=False, **kwargs):
"""
Determines if another object has permission to access this
object in whatever way.
Args:
accessing_obj (Object): Object trying to access this one.
access_type (str, optional): Type of access sought.
default (bool, optional): What to return if no lock of
access_type was found
no_superuser_bypass (bool, optional): Turn off superuser
lock bypassing. Be careful with this one.
Kwargs:
kwargs (any): Passed to the at_access hook along with the result.
Returns:
result (bool): Result of access check.
"""
result = super(DefaultAccount, self).access(accessing_obj, access_type=access_type,
default=default, no_superuser_bypass=no_superuser_bypass)
self.at_access(result, accessing_obj, access_type, **kwargs)
return result
@property
def idle_time(self):
"""
Returns the idle time of the least idle session in seconds. If
no sessions are connected it returns nothing.
"""
idle = [session.cmd_last_visible for session in self.sessions.all()]
if idle:
return time.time() - float(max(idle))
return None
@property
def connection_time(self):
"""
Returns the maximum connection time of all connected sessions
in seconds. Returns nothing if there are no sessions.
"""
conn = [session.conn_time for session in self.sessions.all()]
if conn:
return time.time() - float(min(conn))
return None
# account hooks
def basetype_setup(self):
"""
This sets up the basic properties for an account. Overload this
with at_account_creation rather than changing this method.
"""
# A basic security setup
lockstring = "examine:perm(Admin);edit:perm(Admin);" \
"delete:perm(Admin);boot:perm(Admin);msg:all()"
self.locks.add(lockstring)
# The ooc account cmdset
self.cmdset.add_default(_CMDSET_ACCOUNT, permanent=True)
def at_account_creation(self):
"""
This is called once, the very first time the account is created
(i.e. first time they register with the game). It's a good
place to store attributes all accounts should have, like
configuration values etc.
"""
# set an (empty) attribute holding the characters this account has
lockstring = "attrread:perm(Admins);attredit:perm(Admins);" \
"attrcreate:perm(Admins)"
self.attributes.add("_playable_characters", [], lockstring=lockstring)
self.attributes.add("_saved_protocol_flags", {}, lockstring=lockstring)
def at_init(self):
"""
This is always called whenever this object is initiated --
that is, whenever it its typeclass is cached from memory. This
happens on-demand first time the object is used or activated
in some way after being created but also after each server
restart or reload. In the case of account objects, this usually
happens the moment the account logs in or reconnects after a
reload.
"""
pass
# Note that the hooks below also exist in the character object's
# typeclass. You can often ignore these and rely on the character
# ones instead, unless you are implementing a multi-character game
# and have some things that should be done regardless of which
# character is currently connected to this account.
def at_first_save(self):
"""
This is a generic hook called by Evennia when this object is
saved to the database the very first time. You generally
don't override this method but the hooks called by it.
"""
self.basetype_setup()
self.at_account_creation()
permissions = settings.PERMISSION_ACCOUNT_DEFAULT
if hasattr(self, "_createdict"):
# this will only be set if the utils.create_account
# function was used to create the object.
cdict = self._createdict
if cdict.get("locks"):
self.locks.add(cdict["locks"])
if cdict.get("permissions"):
permissions = cdict["permissions"]
del self._createdict
self.permissions.batch_add(*permissions)
def at_access(self, result, accessing_obj, access_type, **kwargs):
"""
This is triggered after an access-call on this Account has
completed.
Args:
result (bool): The result of the access check.
accessing_obj (any): The object requesting the access
check.
access_type (str): The type of access checked.
Kwargs:
kwargs (any): These are passed on from the access check
and can be used to relay custom instructions from the
check mechanism.
Notes:
This method cannot affect the result of the lock check and
its return value is not used in any way. It can be used
e.g. to customize error messages in a central location or
create other effects based on the access result.
"""
pass
def at_cmdset_get(self, **kwargs):
"""
Called just *before* cmdsets on this account are requested by
the command handler. The cmdsets are available as
`self.cmdset`. If changes need to be done on the fly to the
cmdset before passing them on to the cmdhandler, this is the
place to do it. This is called also if the account currently
have no cmdsets. kwargs are usually not used unless the
cmdset is generated dynamically.
"""
pass
def at_first_login(self, **kwargs):
"""
Called the very first time this account logs into the game.
Note that this is called *before* at_pre_login, so no session
is established and usually no character is yet assigned at
this point. This hook is intended for account-specific setup
like configurations.
Args:
**kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default).
"""
pass
def at_pre_login(self, **kwargs):
"""
Called every time the user logs in, just before the actual
login-state is set.
Args:
**kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default).
"""
pass
def _send_to_connect_channel(self, message):
"""
Helper method for loading and sending to the comm channel
dedicated to connection messages.
Args:
message (str): A message to send to the connect channel.
"""
global _CONNECT_CHANNEL
if not _CONNECT_CHANNEL:
try:
_CONNECT_CHANNEL = ChannelDB.objects.filter(db_key=settings.DEFAULT_CHANNELS[1]["key"])[0]
except Exception:
logger.log_trace()
now = timezone.now()
now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month,
now.day, now.hour, now.minute)
if _CONNECT_CHANNEL:
_CONNECT_CHANNEL.tempmsg("[%s, %s]: %s" % (_CONNECT_CHANNEL.key, now, message))
else:
logger.log_info("[%s]: %s" % (now, message))
def at_post_login(self, session=None, **kwargs):
"""
Called at the end of the login process, just before letting
the account loose.
Args:
session (Session, optional): Session logging in, if any.
**kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default).
Notes:
This is called *before* an eventual Character's
`at_post_login` hook. By default it is used to set up
auto-puppeting based on `MULTISESSION_MODE`.
"""
# if we have saved protocol flags on ourselves, load them here.
protocol_flags = self.attributes.get("_saved_protocol_flags", None)
if session and protocol_flags:
session.update_flags(**protocol_flags)
# inform the client that we logged in through an OOB message
if session:
session.msg(logged_in={})
self._send_to_connect_channel("|G%s connected|n" % self.key)
if _MULTISESSION_MODE == 0:
# in this mode we should have only one character available. We
# try to auto-connect to our last conneted object, if any
try:
self.puppet_object(session, self.db._last_puppet)
except RuntimeError:
self.msg("The Character does not exist.")
return
elif _MULTISESSION_MODE == 1:
# in this mode all sessions connect to the same puppet.
try:
self.puppet_object(session, self.db._last_puppet)
except RuntimeError:
self.msg("The Character does not exist.")
return
elif _MULTISESSION_MODE in (2, 3):
# In this mode we by default end up at a character selection
# screen. We execute look on the account.
# we make sure to clean up the _playable_characers list in case
# any was deleted in the interim.
self.db._playable_characters = [char for char in self.db._playable_characters if char]
self.msg(self.at_look(target=self.db._playable_characters,
session=session))
def at_failed_login(self, session, **kwargs):
"""
Called by the login process if a user account is targeted correctly
but provided with an invalid password. By default it does nothing,
but exists to be overriden.
Args:
session (session): Session logging in.
**kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default).
"""
pass
def at_disconnect(self, reason=None, **kwargs):
"""
Called just before user is disconnected.
Args:
reason (str, optional): The reason given for the disconnect,
(echoed to the connection channel by default).
**kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default).
"""
reason = reason and "(%s)" % reason or ""
self._send_to_connect_channel("|R%s disconnected %s|n" % (self.key, reason))
def at_post_disconnect(self, **kwargs):
"""
This is called *after* disconnection is complete. No messages
can be relayed to the account from here. After this call, the
account should not be accessed any more, making this a good
spot for deleting it (in the case of a guest account account,
for example).
Args:
**kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default).
"""
pass
def at_message_receive(self, message, from_obj=None, **kwargs):
"""
This is currently unused.
Args:
**kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default).
"""
return True
def at_message_send(self, message, to_object, **kwargs):
"""
This is currently unused.
Args:
**kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default).
"""
pass
def at_server_reload(self):
"""
This hook is called whenever the server is shutting down for
restart/reboot. If you want to, for example, save
non-persistent properties across a restart, this is the place
to do it.
"""
pass
def at_server_shutdown(self):
"""
This hook is called whenever the server is shutting down fully
(i.e. not for a restart).
"""
pass
def at_look(self, target=None, session=None, **kwargs):
"""
Called when this object executes a look. It allows to customize
just what this means.
Args:
target (Object or list, optional): An object or a list
objects to inspect.
session (Session, optional): The session doing this look.
**kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default).
Returns:
look_string (str): A prepared look string, ready to send
off to any recipient (usually to ourselves)
"""
if target and not is_iter(target):
# single target - just show it
return target.return_appearance(self)
else:
# list of targets - make list to disconnect from db
characters = list(tar for tar in target if tar) if target else []
sessions = self.sessions.all()
is_su = self.is_superuser
# text shown when looking in the ooc area
result = ["Account |g%s|n (you are Out-of-Character)" % self.key]
nsess = len(sessions)
result.append(nsess == 1 and "\n\n|wConnected session:|n" or "\n\n|wConnected sessions (%i):|n" % nsess)
for isess, sess in enumerate(sessions):
csessid = sess.sessid
addr = "%s (%s)" % (sess.protocol_key, isinstance(sess.address, tuple)
and str(sess.address[0]) or str(sess.address))
result.append("\n %s %s" % (session.sessid == csessid and "|w* %s|n" % (isess + 1)
or " %s" % (isess + 1), addr))
result.append("\n\n |whelp|n - more commands")
result.append("\n |wooc <Text>|n - talk on public channel")
charmax = _MAX_NR_CHARACTERS if _MULTISESSION_MODE > 1 else 1
if is_su or len(characters) < charmax:
if not characters:
result.append("\n\n You don't have any characters yet. See |whelp @charcreate|n for creating one.")
else:
result.append("\n |w@charcreate <name> [=description]|n - create new character")
result.append("\n |w@chardelete <name>|n - delete a character (cannot be undone!)")
if characters:
string_s_ending = len(characters) > 1 and "s" or ""
result.append("\n |w@ic <character>|n - enter the game (|w@ooc|n to get back here)")
if is_su:
result.append("\n\nAvailable character%s (%i/unlimited):" % (string_s_ending, len(characters)))
else:
result.append("\n\nAvailable character%s%s:"
% (string_s_ending, charmax > 1 and " (%i/%i)" % (len(characters), charmax) or ""))
for char in characters:
csessions = char.sessions.all()
if csessions:
for sess in csessions:
# character is already puppeted
sid = sess in sessions and sessions.index(sess) + 1
if sess and sid:
result.append("\n - |G%s|n [%s] (played by you in session %i)"
% (char.key, ", ".join(char.permissions.all()), sid))
else:
result.append("\n - |R%s|n [%s] (played by someone else)"
% (char.key, ", ".join(char.permissions.all())))
else:
# character is "free to puppet"
result.append("\n - %s [%s]" % (char.key, ", ".join(char.permissions.all())))
look_string = ("-" * 68) + "\n" + "".join(result) + "\n" + ("-" * 68)
return look_string
class DefaultGuest(DefaultAccount):
"""
This class is used for guest logins. Unlike Accounts, Guests and
their characters are deleted after disconnection.
"""
def at_post_login(self, session=None, **kwargs):
"""
In theory, guests only have one character regardless of which
MULTISESSION_MODE we're in. They don't get a choice.
Args:
session (Session, optional): Session connecting.
**kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default).
"""
self._send_to_connect_channel("|G%s connected|n" % self.key)
self.puppet_object(session, self.db._last_puppet)
def at_server_shutdown(self):
"""
We repeat the functionality of `at_disconnect()` here just to
be on the safe side.
"""
super(DefaultGuest, self).at_server_shutdown()
characters = self.db._playable_characters
for character in characters:
if character:
print "deleting Character:", character
character.delete()
def at_post_disconnect(self, **kwargs):
"""
Once having disconnected, destroy the guest's characters and
Args:
**kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default).
"""
super(DefaultGuest, self).at_post_disconnect()
characters = self.db._playable_characters
for character in characters:
if character:
character.delete()
self.delete()

255
evennia/accounts/admin.py Normal file
View file

@ -0,0 +1,255 @@
#
# This sets up how models are displayed
# in the web admin interface.
#
from builtins import object
from django import forms
from django.conf import settings
from django.contrib import admin
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
from evennia.accounts.models import AccountDB
from evennia.typeclasses.admin import AttributeInline, TagInline
from evennia.utils import create
# handle the custom User editor
class AccountDBChangeForm(UserChangeForm):
"""
Modify the accountdb class.
"""
class Meta(object):
model = AccountDB
fields = '__all__'
username = forms.RegexField(
label="Username",
max_length=30,
regex=r'^[\w. @+-]+$',
widget=forms.TextInput(
attrs={'size': '30'}),
error_messages={
'invalid': "This value may contain only letters, spaces, numbers "
"and @/./+/-/_ characters."},
help_text="30 characters or fewer. Letters, spaces, digits and "
"@/./+/-/_ only.")
def clean_username(self):
"""
Clean the username and check its existence.
"""
username = self.cleaned_data['username']
if username.upper() == self.instance.username.upper():
return username
elif AccountDB.objects.filter(username__iexact=username):
raise forms.ValidationError('An account with that name '
'already exists.')
return self.cleaned_data['username']
class AccountDBCreationForm(UserCreationForm):
"""
Create a new AccountDB instance.
"""
class Meta(object):
model = AccountDB
fields = '__all__'
username = forms.RegexField(
label="Username",
max_length=30,
regex=r'^[\w. @+-]+$',
widget=forms.TextInput(
attrs={'size': '30'}),
error_messages={
'invalid': "This value may contain only letters, spaces, numbers "
"and @/./+/-/_ characters."},
help_text="30 characters or fewer. Letters, spaces, digits and "
"@/./+/-/_ only.")
def clean_username(self):
"""
Cleanup username.
"""
username = self.cleaned_data['username']
if AccountDB.objects.filter(username__iexact=username):
raise forms.ValidationError('An account with that name already '
'exists.')
return username
class AccountForm(forms.ModelForm):
"""
Defines how to display Accounts
"""
class Meta(object):
model = AccountDB
fields = '__all__'
db_key = forms.RegexField(
label="Username",
initial="AccountDummy",
max_length=30,
regex=r'^[\w. @+-]+$',
required=False,
widget=forms.TextInput(attrs={'size': '30'}),
error_messages={
'invalid': "This value may contain only letters, spaces, numbers"
" and @/./+/-/_ characters."},
help_text="This should be the same as the connected Account's key "
"name. 30 characters or fewer. Letters, spaces, digits and "
"@/./+/-/_ only.")
db_typeclass_path = forms.CharField(
label="Typeclass",
initial=settings.BASE_PLAYER_TYPECLASS,
widget=forms.TextInput(
attrs={'size': '78'}),
help_text="Required. Defines what 'type' of entity this is. This "
"variable holds a Python path to a module with a valid "
"Evennia Typeclass. Defaults to "
"settings.BASE_ACCOUNT_TYPECLASS.")
db_permissions = forms.CharField(
label="Permissions",
initial=settings.PERMISSION_PLAYER_DEFAULT,
required=False,
widget=forms.TextInput(
attrs={'size': '78'}),
help_text="In-game permissions. A comma-separated list of text "
"strings checked by certain locks. They are often used for "
"hierarchies, such as letting an Account have permission "
"'Admin', 'Builder' etc. An Account permission can be "
"overloaded by the permissions of a controlled Character. "
"Normal accounts use 'Accounts' by default.")
db_lock_storage = forms.CharField(
label="Locks",
widget=forms.Textarea(attrs={'cols': '100', 'rows': '2'}),
required=False,
help_text="In-game lock definition string. If not given, defaults "
"will be used. This string should be on the form "
"<i>type:lockfunction(args);type2:lockfunction2(args);...")
db_cmdset_storage = forms.CharField(
label="cmdset",
initial=settings.CMDSET_PLAYER,
widget=forms.TextInput(attrs={'size': '78'}),
required=False,
help_text="python path to account cmdset class (set in "
"settings.CMDSET_ACCOUNT by default)")
class AccountInline(admin.StackedInline):
"""
Inline creation of Account
"""
model = AccountDB
template = "admin/accounts/stacked.html"
form = AccountForm
fieldsets = (
("In-game Permissions and Locks",
{'fields': ('db_lock_storage',),
#{'fields': ('db_permissions', 'db_lock_storage'),
'description': "<i>These are permissions/locks for in-game use. "
"They are unrelated to website access rights.</i>"}),
("In-game Account data",
{'fields': ('db_typeclass_path', 'db_cmdset_storage'),
'description': "<i>These fields define in-game-specific properties "
"for the Account object in-game.</i>"}))
extra = 1
max_num = 1
class AccountTagInline(TagInline):
"""
Inline Account Tags.
"""
model = AccountDB.db_tags.through
related_field = "accountdb"
class AccountAttributeInline(AttributeInline):
"""
Inline Account Attributes.
"""
model = AccountDB.db_attributes.through
related_field = "accountdb"
class AccountDBAdmin(BaseUserAdmin):
"""
This is the main creation screen for Users/accounts
"""
list_display = ('username', 'email', 'is_staff', 'is_superuser')
form = AccountDBChangeForm
add_form = AccountDBCreationForm
inlines = [AccountTagInline, AccountAttributeInline]
fieldsets = (
(None, {'fields': ('username', 'password', 'email')}),
('Website profile', {
'fields': ('first_name', 'last_name'),
'description': "<i>These are not used "
"in the default system.</i>"}),
('Website dates', {
'fields': ('last_login', 'date_joined'),
'description': '<i>Relevant only to the website.</i>'}),
('Website Permissions', {
'fields': ('is_active', 'is_staff', 'is_superuser',
'user_permissions', 'groups'),
'description': "<i>These are permissions/permission groups for "
"accessing the admin site. They are unrelated to "
"in-game access rights.</i>"}),
('Game Options', {
'fields': ('db_typeclass_path', 'db_cmdset_storage',
'db_lock_storage'),
'description': '<i>These are attributes that are more relevant '
'to gameplay.</i>'}))
# ('Game Options', {'fields': (
# 'db_typeclass_path', 'db_cmdset_storage',
# 'db_permissions', 'db_lock_storage'),
# 'description': '<i>These are attributes that are '
# 'more relevant to gameplay.</i>'}))
add_fieldsets = (
(None,
{'fields': ('username', 'password1', 'password2', 'email'),
'description': "<i>These account details are shared by the admin "
"system and the game.</i>"},),)
def save_model(self, request, obj, form, change):
"""
Custom save actions.
Args:
request (Request): Incoming request.
obj (Object): Object to save.
form (Form): Related form instance.
change (bool): False if this is a new save and not an update.
"""
obj.save()
if not change:
#calling hooks for new account
obj.set_class_from_typeclass(typeclass_path=settings.BASE_PLAYER_TYPECLASS)
obj.basetype_setup()
obj.at_account_creation()
def response_add(self, request, obj, post_url_continue=None):
from django.http import HttpResponseRedirect
from django.core.urlresolvers import reverse
if '_continue' in request.POST:
return HttpResponseRedirect(reverse("admin:accounts_accountdb_change", args=[obj.id]))
return HttpResponseRedirect(reverse("admin:accounts_accountdb_change", args=[obj.id]))
admin.site.register(AccountDB, AccountDBAdmin)

419
evennia/accounts/bots.py Normal file
View file

@ -0,0 +1,419 @@
"""
Bots are a special child typeclasses of
Account that are controlled by the server.
"""
from __future__ import print_function
import time
from django.conf import settings
from evennia.accounts.accounts import DefaultAccount
from evennia.scripts.scripts import DefaultScript
from evennia.utils import search
from evennia.utils import utils
_IDLE_TIMEOUT = settings.IDLE_TIMEOUT
_IRC_ENABLED = settings.IRC_ENABLED
_RSS_ENABLED = settings.RSS_ENABLED
_SESSIONS = None
# Bot helper utilities
class BotStarter(DefaultScript):
"""
This non-repeating script has the
sole purpose of kicking its bot
into gear when it is initialized.
"""
def at_script_creation(self):
"""
Called once, when script is created.
"""
self.key = "botstarter"
self.desc = "bot start/keepalive"
self.persistent = True
self.db.started = False
if _IDLE_TIMEOUT > 0:
# call before idle_timeout triggers
self.interval = int(max(60, _IDLE_TIMEOUT * 0.90))
self.start_delay = True
def at_start(self):
"""
Kick bot into gear.
"""
if not self.db.started:
self.account.start()
self.db.started = True
def at_repeat(self):
"""
Called self.interval seconds to keep connection. We cannot use
the IDLE command from inside the game since the system will
not catch it (commands executed from the server side usually
has no sessions). So we update the idle counter manually here
instead. This keeps the bot getting hit by IDLE_TIMEOUT.
"""
global _SESSIONS
if not _SESSIONS:
from evennia.server.sessionhandler import SESSIONS as _SESSIONS
for session in _SESSIONS.sessions_from_account(self.account):
session.update_session_counters(idle=True)
def at_server_reload(self):
"""
If server reloads we don't need to reconnect the protocol
again, this is handled by the portal reconnect mechanism.
"""
self.db.started = True
def at_server_shutdown(self):
"""
Make sure we are shutdown.
"""
self.db.started = False
#
# Bot base class
class Bot(DefaultAccount):
"""
A Bot will start itself when the server starts (it will generally
not do so on a reload - that will be handled by the normal Portal
session resync)
"""
def basetype_setup(self):
"""
This sets up the basic properties for the bot.
"""
# the text encoding to use.
self.db.encoding = "utf-8"
# A basic security setup
lockstring = "examine:perm(Admin);edit:perm(Admin);delete:perm(Admin);boot:perm(Admin);msg:false()"
self.locks.add(lockstring)
# set the basics of being a bot
script_key = "%s" % self.key
self.scripts.add(BotStarter, key=script_key)
self.is_bot = True
def start(self, **kwargs):
"""
This starts the bot, whatever that may mean.
"""
pass
def msg(self, text=None, from_obj=None, session=None, options=None, **kwargs):
"""
Evennia -> outgoing protocol
"""
super(Bot, self).msg(text=text, from_obj=from_obj, session=session, options=options, **kwargs)
def execute_cmd(self, raw_string, session=None):
"""
Incoming protocol -> Evennia
"""
super(Bot, self).msg(raw_string, session=session)
def at_server_shutdown(self):
"""
We need to handle this case manually since the shutdown may be
a reset.
"""
for session in self.sessions.all():
session.sessionhandler.disconnect(session)
# Bot implementations
# IRC
class IRCBot(Bot):
"""
Bot for handling IRC connections.
"""
def start(self, ev_channel=None, irc_botname=None, irc_channel=None, irc_network=None, irc_port=None, irc_ssl=None):
"""
Start by telling the portal to start a new session.
Args:
ev_channel (str): Key of the Evennia channel to connect to.
irc_botname (str): Name of bot to connect to irc channel. If
not set, use `self.key`.
irc_channel (str): Name of channel on the form `#channelname`.
irc_network (str): URL of the IRC network, like `irc.freenode.net`.
irc_port (str): Port number of the irc network, like `6667`.
irc_ssl (bool): Indicates whether to use SSL connection.
"""
if not _IRC_ENABLED:
# the bot was created, then IRC was turned off. We delete
# ourselves (this will also kill the start script)
self.delete()
return
global _SESSIONS
if not _SESSIONS:
from evennia.server.sessionhandler import SESSIONS as _SESSIONS
# if keywords are given, store (the BotStarter script
# will not give any keywords, so this should normally only
# happen at initialization)
if irc_botname:
self.db.irc_botname = irc_botname
elif not self.db.irc_botname:
self.db.irc_botname = self.key
if ev_channel:
# connect to Evennia channel
channel = search.channel_search(ev_channel)
if not channel:
raise RuntimeError("Evennia Channel '%s' not found." % ev_channel)
channel = channel[0]
channel.connect(self)
self.db.ev_channel = channel
if irc_channel:
self.db.irc_channel = irc_channel
if irc_network:
self.db.irc_network = irc_network
if irc_port:
self.db.irc_port = irc_port
if irc_ssl:
self.db.irc_ssl = irc_ssl
# instruct the server and portal to create a new session with
# the stored configuration
configdict = {"uid": self.dbid,
"botname": self.db.irc_botname,
"channel": self.db.irc_channel,
"network": self.db.irc_network,
"port": self.db.irc_port,
"ssl": self.db.irc_ssl}
_SESSIONS.start_bot_session("evennia.server.portal.irc.IRCBotFactory", configdict)
def get_nicklist(self, caller):
"""
Retrive the nick list from the connected channel.
Args:
caller (Object or Account): The requester of the list. This will
be stored and echoed to when the irc network replies with the
requested info.
Notes: Since the return is asynchronous, the caller is stored internally
in a list; all callers in this list will get the nick info once it
returns (it is a custom OOB inputfunc option). The callback will not
survive a reload (which should be fine, it's very quick).
"""
if not hasattr(self, "_nicklist_callers"):
self._nicklist_callers = []
self._nicklist_callers.append(caller)
super(IRCBot, self).msg(request_nicklist="")
return
def ping(self, caller):
"""
Fire a ping to the IRC server.
Args:
caller (Object or Account): The requester of the ping.
"""
if not hasattr(self, "_ping_callers"):
self._ping_callers = []
self._ping_callers.append(caller)
super(IRCBot, self).msg(ping="")
def reconnect(self):
"""
Force a protocol-side reconnect of the client without
having to destroy/recreate the bot "account".
"""
super(IRCBot, self).msg(reconnect="")
def msg(self, text=None, **kwargs):
"""
Takes text from connected channel (only).
Args:
text (str, optional): Incoming text from channel.
Kwargs:
options (dict): Options dict with the following allowed keys:
- from_channel (str): dbid of a channel this text originated from.
- from_obj (list): list of objects this text.
"""
from_obj = kwargs.get("from_obj", None)
options = kwargs.get("options", None) or {}
if not self.ndb.ev_channel and self.db.ev_channel:
# cache channel lookup
self.ndb.ev_channel = self.db.ev_channel
if "from_channel" in options and text and self.ndb.ev_channel.dbid == options["from_channel"]:
if not from_obj or from_obj != [self.id]:
super(IRCBot, self).msg(channel=text)
def execute_cmd(self, session=None, txt=None, **kwargs):
"""
Take incoming data and send it to connected channel. This is
triggered by the bot_data_in Inputfunc.
Args:
session (Session, optional): Session responsible for this
command. Note that this is the bot.
txt (str, optional): Command string.
Kwargs:
user (str): The name of the user who sent the message.
channel (str): The name of channel the message was sent to.
type (str): Nature of message. Either 'msg', 'action', 'nicklist' or 'ping'.
nicklist (list, optional): Set if `type='nicklist'`. This is a list of nicks returned by calling
the `self.get_nicklist`. It must look for a list `self._nicklist_callers`
which will contain all callers waiting for the nicklist.
timings (float, optional): Set if `type='ping'`. This is the return (in seconds) of a
ping request triggered with `self.ping`. The return must look for a list
`self._ping_callers` which will contain all callers waiting for the ping return.
"""
if kwargs["type"] == "nicklist":
# the return of a nicklist request
if hasattr(self, "_nicklist_callers") and self._nicklist_callers:
chstr = "%s (%s:%s)" % (self.db.irc_channel, self.db.irc_network, self.db.irc_port)
nicklist = ", ".join(sorted(kwargs["nicklist"], key=lambda n: n.lower()))
for obj in self._nicklist_callers:
obj.msg("Nicks at %s:\n %s" % (chstr, nicklist))
self._nicklist_callers = []
return
elif kwargs["type"] == "ping":
# the return of a ping
if hasattr(self, "_ping_callers") and self._ping_callers:
chstr = "%s (%s:%s)" % (self.db.irc_channel, self.db.irc_network, self.db.irc_port)
for obj in self._ping_callers:
obj.msg("IRC ping return from %s took %ss." % (chstr, kwargs["timing"]))
self._ping_callers = []
return
elif kwargs["type"] == "privmsg":
# A private message to the bot - a command.
user = kwargs["user"]
if txt.lower().startswith("who"):
# return server WHO list (abbreviated for IRC)
global _SESSIONS
if not _SESSIONS:
from evennia.server.sessionhandler import SESSIONS as _SESSIONS
whos = []
t0 = time.time()
for sess in _SESSIONS.get_sessions():
delta_cmd = t0 - sess.cmd_last_visible
delta_conn = t0 - session.conn_time
account = sess.get_account()
whos.append("%s (%s/%s)" % (utils.crop("|w%s|n" % account.name, width=25),
utils.time_format(delta_conn, 0),
utils.time_format(delta_cmd, 1)))
text = "Who list (online/idle): %s" % ", ".join(sorted(whos, key=lambda w: w.lower()))
elif txt.lower().startswith("about"):
# some bot info
text = "This is an Evennia IRC bot connecting from '%s'." % settings.SERVERNAME
else:
text = "I understand 'who' and 'about'."
super(IRCBot, self).msg(privmsg=((text,), {"user": user}))
else:
# something to send to the main channel
if kwargs["type"] == "action":
# An action (irc pose)
text = "%s@%s %s" % (kwargs["user"], kwargs["channel"], txt)
else:
# msg - A normal channel message
text = "%s@%s: %s" % (kwargs["user"], kwargs["channel"], txt)
if not self.ndb.ev_channel and self.db.ev_channel:
# cache channel lookup
self.ndb.ev_channel = self.db.ev_channel
if self.ndb.ev_channel:
self.ndb.ev_channel.msg(text, senders=self.id)
#
# RSS
class RSSBot(Bot):
"""
An RSS relayer. The RSS protocol itself runs a ticker to update
its feed at regular intervals.
"""
def start(self, ev_channel=None, rss_url=None, rss_rate=None):
"""
Start by telling the portal to start a new RSS session
Args:
ev_channel (str): Key of the Evennia channel to connect to.
rss_url (str): Full URL to the RSS feed to subscribe to.
rss_rate (int): How often for the feedreader to update.
Raises:
RuntimeError: If `ev_channel` does not exist.
"""
if not _RSS_ENABLED:
# The bot was created, then RSS was turned off. Delete ourselves.
self.delete()
return
global _SESSIONS
if not _SESSIONS:
from evennia.server.sessionhandler import SESSIONS as _SESSIONS
if ev_channel:
# connect to Evennia channel
channel = search.channel_search(ev_channel)
if not channel:
raise RuntimeError("Evennia Channel '%s' not found." % ev_channel)
channel = channel[0]
self.db.ev_channel = channel
if rss_url:
self.db.rss_url = rss_url
if rss_rate:
self.db.rss_rate = rss_rate
# instruct the server and portal to create a new session with
# the stored configuration
configdict = {"uid": self.dbid,
"url": self.db.rss_url,
"rate": self.db.rss_rate}
_SESSIONS.start_bot_session("evennia.server.portal.rss.RSSBotFactory", configdict)
def execute_cmd(self, txt=None, session=None, **kwargs):
"""
Take incoming data and send it to connected channel. This is
triggered by the bot_data_in Inputfunc.
Args:
session (Session, optional): Session responsible for this
command.
txt (str, optional): Command string.
kwargs (dict, optional): Additional Information passed from bot.
Not used by the RSSbot by default.
"""
if not self.ndb.ev_channel and self.db.ev_channel:
# cache channel lookup
self.ndb.ev_channel = self.db.ev_channel
if self.ndb.ev_channel:
self.ndb.ev_channel.msg(txt, senders=self.id)

180
evennia/accounts/manager.py Normal file
View file

@ -0,0 +1,180 @@
"""
The managers for the custom Account object and permissions.
"""
import datetime
from django.utils import timezone
from django.contrib.auth.models import UserManager
from evennia.typeclasses.managers import (TypedObjectManager, TypeclassManager)
__all__ = ("AccountManager",)
#
# Account Manager
#
class AccountDBManager(TypedObjectManager, UserManager):
"""
This AccountManager implements methods for searching
and manipulating Accounts directly from the database.
Evennia-specific search methods (will return Characters if
possible or a Typeclass/list of Typeclassed objects, whereas
Django-general methods will return Querysets or database objects):
dbref (converter)
dbref_search
get_dbref_range
object_totals
typeclass_search
num_total_accounts
get_connected_accounts
get_recently_created_accounts
get_recently_connected_accounts
get_account_from_email
get_account_from_uid
get_account_from_name
account_search (equivalent to evennia.search_account)
#swap_character
"""
def num_total_accounts(self):
"""
Get total number of accounts.
Returns:
count (int): The total number of registered accounts.
"""
return self.count()
def get_connected_accounts(self):
"""
Get all currently connected accounts.
Returns:
count (list): Account objects with currently
connected sessions.
"""
return self.filter(db_is_connected=True)
def get_recently_created_accounts(self, days=7):
"""
Get accounts recently created.
Args:
days (int, optional): How many days in the past "recently" means.
Returns:
accounts (list): The Accounts created the last `days` interval.
"""
end_date = timezone.now()
tdelta = datetime.timedelta(days)
start_date = end_date - tdelta
return self.filter(date_joined__range=(start_date, end_date))
def get_recently_connected_accounts(self, days=7):
"""
Get accounts recently connected to the game.
Args:
days (int, optional): Number of days backwards to check
Returns:
accounts (list): The Accounts connected to the game in the
last `days` interval.
"""
end_date = timezone.now()
tdelta = datetime.timedelta(days)
start_date = end_date - tdelta
return self.filter(last_login__range=(
start_date, end_date)).order_by('-last_login')
def get_account_from_email(self, uemail):
"""
Search account by
Returns an account object based on email address.
Args:
uemail (str): An email address to search for.
Returns:
account (Account): A found account, if found.
"""
return self.filter(email__iexact=uemail)
def get_account_from_uid(self, uid):
"""
Get an account by id.
Args:
uid (int): Account database id.
Returns:
account (Account): The result.
"""
try:
return self.get(id=uid)
except self.model.DoesNotExist:
return None
def get_account_from_name(self, uname):
"""
Get account object based on name.
Args:
uname (str): The Account name to search for.
Returns:
account (Account): The found account.
"""
try:
return self.get(username__iexact=uname)
except self.model.DoesNotExist:
return None
def search_account(self, ostring, exact=True, typeclass=None):
"""
Searches for a particular account by name or
database id.
Args:
ostring (str or int): A key string or database id.
exact (bool, optional): Only valid for string matches. If
`True`, requires exact (non-case-sensitive) match,
otherwise also match also keys containing the `ostring`
(non-case-sensitive fuzzy match).
typeclass (str or Typeclass, optional): Limit the search only to
accounts of this typeclass.
"""
dbref = self.dbref(ostring)
if dbref or dbref == 0:
# bref search is always exact
matches = self.filter(id=dbref)
if matches:
return matches
query = {"username__iexact" if exact else "username__icontains": ostring}
if typeclass:
# we accept both strings and actual typeclasses
if callable(typeclass):
typeclass = u"%s.%s" % (typeclass.__module__, typeclass.__name__)
else:
typeclass = u"%s" % typeclass
query["db_typeclass_path"] = typeclass
if exact:
return self.filter(**query)
else:
return self.filter(**query)
# back-compatibility alias
account_search = search_account
class AccountManager(AccountDBManager, TypeclassManager):
pass

View file

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-03 19:13
from __future__ import unicode_literals
import django.contrib.auth.validators
from django.db import migrations, models
import django.utils.timezone
import evennia.accounts.manager
class Migration(migrations.Migration):
initial = True
dependencies = [
('auth', '0008_alter_user_username_max_length'),
('typeclasses', '0008_lock_and_perm_rename'),
]
operations = [
migrations.CreateModel(
name='AccountDB',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, null=True, verbose_name='last login')),
('is_superuser', models.BooleanField(default=False, help_text='Designates that this user has all permissions without explicitly assigning them.', verbose_name='superuser status')),
('username', models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.ASCIIUsernameValidator()], verbose_name='username')),
('first_name', models.CharField(blank=True, max_length=30, verbose_name='first name')),
('last_name', models.CharField(blank=True, max_length=30, verbose_name='last name')),
('email', models.EmailField(blank=True, max_length=254, verbose_name='email address')),
('is_staff', models.BooleanField(default=False, help_text='Designates whether the user can log into this admin site.', verbose_name='staff status')),
('is_active', models.BooleanField(default=True, help_text='Designates whether this user should be treated as active. Unselect this instead of deleting accounts.', verbose_name='active')),
('date_joined', models.DateTimeField(default=django.utils.timezone.now, verbose_name='date joined')),
('db_key', models.CharField(db_index=True, max_length=255, verbose_name=b'key')),
('db_typeclass_path', models.CharField(help_text=b"this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.", max_length=255, null=True, verbose_name=b'typeclass')),
('db_date_created', models.DateTimeField(auto_now_add=True, verbose_name=b'creation date')),
('db_lock_storage', models.TextField(blank=True, help_text=b"locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.", verbose_name=b'locks')),
('db_is_connected', models.BooleanField(default=False, help_text=b'If player is connected to game or not', verbose_name=b'is_connected')),
('db_cmdset_storage', models.CharField(help_text=b'optional python path to a cmdset class. If creating a Character, this will default to settings.CMDSET_CHARACTER.', max_length=255, null=True, verbose_name=b'cmdset')),
('db_is_bot', models.BooleanField(default=False, help_text=b'Used to identify irc/rss bots', verbose_name=b'is_bot')),
('db_attributes', models.ManyToManyField(help_text=b'attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute')),
('db_tags', models.ManyToManyField(help_text=b'tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag')),
('groups', models.ManyToManyField(blank=True, help_text='The groups this user belongs to. A user will get all permissions granted to each of their groups.', related_name='user_set', related_query_name='user', to='auth.Group', verbose_name='groups')),
('user_permissions', models.ManyToManyField(blank=True, help_text='Specific permissions for this user.', related_name='user_set', related_query_name='user', to='auth.Permission', verbose_name='user permissions')),
],
options={
'verbose_name': 'Account',
},
managers=[
('objects', evennia.accounts.manager.AccountDBManager()),
],
),
]

View file

@ -0,0 +1,58 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.11.2 on 2017-07-03 19:17
from __future__ import unicode_literals
from django.apps import apps as global_apps
from django.db import migrations
def forwards(apps, schema_editor):
try:
PlayerDB = apps.get_model('players', 'PlayerDB')
except LookupError:
# playerdb not available. Skip.
return
AccountDB = apps.get_model('accounts', 'AccountDB')
for player in PlayerDB.objects.all():
account = AccountDB(id=player.id,
password=player.password,
is_superuser=player.is_superuser,
last_login=player.last_login,
username=player.username,
first_name=player.first_name,
last_name=player.last_name,
email=player.email,
is_staff=player.is_staff,
is_active=player.is_active,
date_joined=player.date_joined,
db_key=player.db_key,
db_typeclass_path=player.db_typeclass_path,
db_date_created=player.db_date_created,
db_lock_storage=player.db_lock_storage,
db_is_connected=player.db_is_connected,
db_cmdset_storage=player.db_cmdset_storage,
db_is_bot=player.db_is_bot)
account.save()
for group in player.groups.all():
account.groups.add(group)
for user_permission in player.user_permissions.all():
account.user_permissions.add(user_permission)
for attr in player.db_attributes.all():
account.db_attributes.add(attr)
for tag in player.db_tags.all():
account.db_tags.add(tag)
class Migration(migrations.Migration):
dependencies = [
('accounts', '0001_initial'),
]
operations = [
migrations.RunPython(forwards, migrations.RunPython.noop)
]
if global_apps.is_installed('players'):
dependencies.append(('players', '0006_auto_20170606_1731'))

View file

169
evennia/accounts/models.py Normal file
View file

@ -0,0 +1,169 @@
"""
Account
The account class is an extension of the default Django user class,
and is customized for the needs of Evennia.
We use the Account to store a more mud-friendly style of permission
system as well as to allow the admin more flexibility by storing
attributes on the Account. Within the game we should normally use the
Account manager's methods to create users so that permissions are set
correctly.
To make the Account model more flexible for your own game, it can also
persistently store attributes of its own. This is ideal for extra
account info and OOC account configuration variables etc.
"""
from builtins import object
from django.conf import settings
from django.db import models
from django.contrib.auth.models import AbstractUser
from django.utils.encoding import smart_str
from evennia.accounts.manager import AccountDBManager
from evennia.typeclasses.models import TypedObject
from evennia.utils.utils import make_iter
__all__ = ("AccountDB",)
#_ME = _("me")
#_SELF = _("self")
_MULTISESSION_MODE = settings.MULTISESSION_MODE
_GA = object.__getattribute__
_SA = object.__setattr__
_DA = object.__delattr__
_TYPECLASS = None
#------------------------------------------------------------
#
# AccountDB
#
#------------------------------------------------------------
class AccountDB(TypedObject, AbstractUser):
"""
This is a special model using Django's 'profile' functionality
and extends the default Django User model. It is defined as such
by use of the variable AUTH_PROFILE_MODULE in the settings.
One accesses the fields/methods. We try use this model as much
as possible rather than User, since we can customize this to
our liking.
The TypedObject supplies the following (inherited) properties:
- key - main name
- typeclass_path - the path to the decorating typeclass
- typeclass - auto-linked typeclass
- date_created - time stamp of object creation
- permissions - perm strings
- dbref - #id of object
- db - persistent attribute storage
- ndb - non-persistent attribute storage
The AccountDB adds the following properties:
- is_connected - If any Session is currently connected to this Account
- name - alias for user.username
- sessions - sessions connected to this account
- is_superuser - bool if this account is a superuser
- is_bot - bool if this account is a bot and not a real account
"""
#
# AccountDB Database model setup
#
# inherited fields (from TypedObject):
# db_key, db_typeclass_path, db_date_created, db_permissions
# store a connected flag here too, not just in sessionhandler.
# This makes it easier to track from various out-of-process locations
db_is_connected = models.BooleanField(default=False,
verbose_name="is_connected",
help_text="If player is connected to game or not")
# database storage of persistant cmdsets.
db_cmdset_storage = models.CharField('cmdset', max_length=255, null=True,
help_text="optional python path to a cmdset class. If creating a Character, this will default to settings.CMDSET_CHARACTER.")
# marks if this is a "virtual" bot account object
db_is_bot = models.BooleanField(default=False, verbose_name="is_bot", help_text="Used to identify irc/rss bots")
# Database manager
objects = AccountDBManager()
# defaults
__settingsclasspath__ = settings.BASE_SCRIPT_TYPECLASS
__defaultclasspath__ = "evennia.accounts.accounts.DefaultAccount"
__applabel__ = "accounts"
class Meta(object):
verbose_name = 'Account'
# cmdset_storage property
# This seems very sensitive to caching, so leaving it be for now /Griatch
#@property
def __cmdset_storage_get(self):
"""
Getter. Allows for value = self.name. Returns a list of cmdset_storage.
"""
storage = self.db_cmdset_storage
# we need to check so storage is not None
return [path.strip() for path in storage.split(',')] if storage else []
#@cmdset_storage.setter
def __cmdset_storage_set(self, value):
"""
Setter. Allows for self.name = value. Stores as a comma-separated
string.
"""
_SA(self, "db_cmdset_storage", ",".join(str(val).strip() for val in make_iter(value)))
_GA(self, "save")()
#@cmdset_storage.deleter
def __cmdset_storage_del(self):
"Deleter. Allows for del self.name"
_SA(self, "db_cmdset_storage", None)
_GA(self, "save")()
cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set, __cmdset_storage_del)
#
# property/field access
#
def __str__(self):
return smart_str("%s(account %s)" % (self.name, self.dbid))
def __unicode__(self):
return u"%s(account#%s)" % (self.name, self.dbid)
#@property
def __username_get(self):
return self.username
def __username_set(self, value):
self.username = value
self.save(update_fields=["username"])
def __username_del(self):
del self.username
# aliases
name = property(__username_get, __username_set, __username_del)
key = property(__username_get, __username_set, __username_del)
#@property
def __uid_get(self):
"Getter. Retrieves the user id"
return self.id
def __uid_set(self, value):
raise Exception("User id cannot be set!")
def __uid_del(self):
raise Exception("User id cannot be deleted!")
uid = property(__uid_get, __uid_set, __uid_del)