mirror of
https://github.com/evennia/evennia.git
synced 2026-03-29 20:17:16 +02:00
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