diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index 4e4d21031c..129479dfaa 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -29,6 +29,8 @@ from evennia.utils import class_from_module, create, logger from evennia.utils.utils import (lazy_property, to_str, make_iter, is_iter, variable_from_module) +from evennia.server.signals import (SIGNAL_ACCOUNT_POST_CREATE, SIGNAL_OBJECT_POST_PUPPET, + SIGNAL_OBJECT_POST_UNPUPPET) from evennia.typeclasses.attributes import NickHandler from evennia.scripts.scripthandler import ScriptHandler from evennia.commands.cmdsethandler import CmdSetHandler @@ -52,6 +54,7 @@ _CONNECT_CHANNEL = None CREATION_THROTTLE = Throttle(limit=2, timeout=10 * 60) LOGIN_THROTTLE = Throttle(limit=5, timeout=5 * 60) + class AccountSessionHandler(object): """ Manages the session(s) attached to an account. @@ -312,6 +315,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): obj.locks.cache_lock_bypass(obj) # final hook obj.at_post_puppet() + SIGNAL_OBJECT_POST_PUPPET.send(sender=obj, account=self, session=session) def unpuppet_object(self, session): """ @@ -334,6 +338,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): if not obj.sessions.count(): del obj.account obj.at_post_unpuppet(self, session=session) + SIGNAL_OBJECT_POST_UNPUPPET.send(sender=obj, session=session, account=self) # Just to be sure we're always clear. session.puppet = None session.puid = None @@ -746,7 +751,9 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)): logger.log_trace() # Update the throttle to indicate a new account was created from this IP - if ip and not guest: CREATION_THROTTLE.update(ip, 'Too many accounts being created.') + if ip and not guest: + CREATION_THROTTLE.update(ip, 'Too many accounts being created.') + SIGNAL_ACCOUNT_POST_CREATE.send(sender=account, ip=ip) return account, errors def delete(self, *args, **kwargs): diff --git a/evennia/accounts/models.py b/evennia/accounts/models.py index 412740c875..a0283e2aef 100644 --- a/evennia/accounts/models.py +++ b/evennia/accounts/models.py @@ -25,6 +25,7 @@ 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 +from evennia.server.signals import SIGNAL_ACCOUNT_POST_RENAME __all__ = ("AccountDB",) @@ -146,8 +147,10 @@ class AccountDB(TypedObject, AbstractUser): return self.username def __username_set(self, value): + old_name = self.username self.username = value self.save(update_fields=["username"]) + SIGNAL_ACCOUNT_POST_RENAME.send(self, old_name=old_name, new_name=value) def __username_del(self): del self.username diff --git a/evennia/server/sessionhandler.py b/evennia/server/sessionhandler.py index 0a519a225f..0a0e7def9a 100644 --- a/evennia/server/sessionhandler.py +++ b/evennia/server/sessionhandler.py @@ -20,12 +20,12 @@ from django.conf import settings from evennia.commands.cmdhandler import CMD_LOGINSTART from evennia.utils.logger import log_trace from evennia.utils.utils import (variable_from_module, is_iter, - to_str, make_iter, delay, callables_from_module) + make_iter, delay, callables_from_module) +from evennia.server.signals import SIGNAL_ACCOUNT_POST_LOGIN, SIGNAL_ACCOUNT_POST_LOGOUT +from evennia.server.signals import SIGNAL_ACCOUNT_POST_CONNECT, SIGNAL_ACCOUNT_POST_DISCONNECT from evennia.utils.inlinefuncs import parse_inlinefunc from codecs import decode as codecs_decode -import pickle - _INLINEFUNC_ENABLED = settings.INLINEFUNC_ENABLED # delayed imports @@ -483,7 +483,6 @@ class ServerSessionHandler(SessionHandler): faking login without any AMP being actually active. """ - if session.logged_in and not force: # don't log in a session that is already logged in. return @@ -519,6 +518,9 @@ class ServerSessionHandler(SessionHandler): sessiondata={"logged_in": True, "uid": session.uid}) account.at_post_login(session=session) + if nsess < 2: + SIGNAL_ACCOUNT_POST_LOGIN.send(sender=account, session=session) + SIGNAL_ACCOUNT_POST_CONNECT.send(sender=account, session=session) def disconnect(self, session, reason="", sync_portal=True): """ @@ -545,7 +547,11 @@ class ServerSessionHandler(SessionHandler): string = string.format(reason=sreason, account=session.account, address=session.address, nsessions=nsess) session.log(string) + if nsess == 0: + SIGNAL_ACCOUNT_POST_LOGOUT.send(sender=session.account, session=session) + session.at_disconnect(reason) + SIGNAL_ACCOUNT_POST_DISCONNECT.send(sender=session.account, session=session) sessid = session.sessid if sessid in self and not hasattr(self, "_disconnect_all"): del self[sessid] diff --git a/evennia/server/signals.py b/evennia/server/signals.py new file mode 100644 index 0000000000..d7d0d1fa56 --- /dev/null +++ b/evennia/server/signals.py @@ -0,0 +1,60 @@ +""" +This module brings Django Signals into Evennia. These are events that +can be subscribed to by importing a given Signal and using the +following code. + +THIS_SIGNAL.connect(callback, sender_object) + +When other code calls THIS_SIGNAL.send(sender, **kwargs), the callback +will be triggered. + +Callbacks must be in the following format: + +def my_callback(sender, **kwargs): + ... + +This is used on top of hooks to make certain features easier to +add to contribs without necessitating a full takeover of hooks +that may be in high demand. + +""" +from django.dispatch import Signal + +# The sender is the created Account. This is triggered at the very end of Account.create() +# after the Account is created. +SIGNAL_ACCOUNT_POST_CREATE = Signal(providing_args=['ip', ]) + +# The Sender is the renamed Account. This is triggered by the username setter in AccountDB. +SIGNAL_ACCOUNT_POST_RENAME = Signal(providing_args=['old_name', 'new_name']) + +# The Sender is the connecting Account. This is triggered when an Account connects cold; +# that is, it had no other sessions connected. +SIGNAL_ACCOUNT_POST_LOGIN = Signal(providing_args=['session', ]) + +# The Sender is the Account attempting to authenticate. This is triggered whenever a +# session tries to login to an Account but fails. +SIGNAL_ACCOUNT_POST_LOGIN_FAIL = Signal(providing_args=['session', ]) + +# The sender is the connecting Account. This is triggered whenever a session authenticates +# to an Account regardless of existing sessions. +SIGNAL_ACCOUNT_POST_CONNECT = Signal(providing_args=['session', ]) + +# The sender is the Account. This is triggered when an Account's final session disconnects. +SIGNAL_ACCOUNT_POST_LOGOUT = Signal(providing_args=['session', ]) + +# The sender is the disconnecting Account. This is triggered whenever a session disconnects +# from the account, regardless of how many it started with or remain. +SIGNAL_ACCOUNT_POST_DISCONNECT = Signal(providing_args=['session', ]) + +# The sender is the Object being puppeted. This is triggered after all puppeting hooks have +# been called. The Object has already been puppeted by this point. +SIGNAL_OBJECT_POST_PUPPET = Signal(providing_args=['session', 'account']) + +# The sender is the Object being released. This is triggered after all hooks are called. +# The Object is no longer puppeted by this point. +SIGNAL_OBJECT_POST_UNPUPPET = Signal(providing_args=['session', 'account']) + +# The sender is the Typed Object being released. This isn't necessarily an Object; +# it could be a script. It fires whenever the value of the Typed object's 'key' +# changes. Will need to use isinstance() or other filtering on things that use this. +SIGNAL_TYPED_OBJECT_POST_RENAME = Signal(providing_args=['old_key', 'new_key']) diff --git a/evennia/typeclasses/models.py b/evennia/typeclasses/models.py index cd8d9a03ed..25f3da4a37 100644 --- a/evennia/typeclasses/models.py +++ b/evennia/typeclasses/models.py @@ -42,6 +42,7 @@ from evennia.typeclasses.attributes import Attribute, AttributeHandler, NAttribu from evennia.typeclasses.tags import Tag, TagHandler, AliasHandler, PermissionHandler from evennia.utils.idmapper.models import SharedMemoryModel, SharedMemoryModelBase +from evennia.server.signals import SIGNAL_TYPED_OBJECT_POST_RENAME from evennia.typeclasses import managers from evennia.locks.lockhandler import LockHandler @@ -326,6 +327,7 @@ class TypedObject(SharedMemoryModel): self.db_key = value self.save(update_fields=["db_key"]) self.at_rename(oldname, value) + SIGNAL_TYPED_OBJECT_POST_RENAME.send(sender=self, old_key=oldname, new_key=value) # #