First step with both account+player at the same time, copying player to account.
This commit is contained in:
parent
99dbaad7ba
commit
ee0e9cc053
9 changed files with 2112 additions and 0 deletions
6
evennia/accounts/__init__.py
Normal file
6
evennia/accounts/__init__.py
Normal 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.
|
||||||
|
|
||||||
|
"""
|
||||||
971
evennia/accounts/accounts.py
Normal file
971
evennia/accounts/accounts.py
Normal 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
255
evennia/accounts/admin.py
Normal 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
419
evennia/accounts/bots.py
Normal 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
180
evennia/accounts/manager.py
Normal 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
|
||||||
54
evennia/accounts/migrations/0001_initial.py
Normal file
54
evennia/accounts/migrations/0001_initial.py
Normal 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()),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
]
|
||||||
58
evennia/accounts/migrations/0002_copy_player_to_account.py
Normal file
58
evennia/accounts/migrations/0002_copy_player_to_account.py
Normal 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'))
|
||||||
0
evennia/accounts/migrations/__init__.py
Normal file
0
evennia/accounts/migrations/__init__.py
Normal file
169
evennia/accounts/models.py
Normal file
169
evennia/accounts/models.py
Normal 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)
|
||||||
Loading…
Add table
Add a link
Reference in a new issue