diff --git a/evennia/accounts/accounts.py b/evennia/accounts/accounts.py index e890724d1c..0edc9d5ce8 100644 --- a/evennia/accounts/accounts.py +++ b/evennia/accounts/accounts.py @@ -276,6 +276,12 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): # Used by account.create_character() to choose default typeclass for characters. default_character_typeclass = settings.BASE_CHARACTER_TYPECLASS + lockstring = ( + "examine:perm(Admin);edit:perm(Admin);" + "delete:perm(Admin);boot:perm(Admin);msg:all();" + "noidletimeout:perm(Builder) or perm(noidletimeout)" + ) + # properties @lazy_property def cmdset(self): @@ -1411,12 +1417,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase): """ # A basic security setup - lockstring = ( - "examine:perm(Admin);edit:perm(Admin);" - "delete:perm(Admin);boot:perm(Admin);msg:all();" - "noidletimeout:perm(Builder) or perm(noidletimeout)" - ) - self.locks.add(lockstring) + self.locks.add(self.lockstring) # The ooc account cmdset self.cmdset.add_default(_CMDSET_ACCOUNT, persistent=True) diff --git a/evennia/locks/lockhandler.py b/evennia/locks/lockhandler.py index 29cce23cde..eba0e64509 100644 --- a/evennia/locks/lockhandler.py +++ b/evennia/locks/lockhandler.py @@ -338,9 +338,16 @@ class LockHandler: """ if isinstance(lockstring, str): - lockdefs = lockstring.split(";") + lockdefs = [ + stripped for lockdef in lockstring.split(";") if (stripped := lockdef.strip()) + ] else: - lockdefs = [lockdef for locks in lockstring for lockdef in locks.split(";")] + lockdefs = [ + stripped + for locks in lockstring + for lockdef in locks.split(";") + if (stripped := lockdef.strip()) + ] lockstring = ";".join(lockdefs) err = "" diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 2188960723..48041382d4 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -213,10 +213,6 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # Used for sorting / filtering in inventories / room contents. _content_types = ("object",) - # lockstring of newly created objects, for easy overloading. - # Will be formatted with the appropriate attributes. - lockstring = "control:id({account_id}) or perm(Admin);delete:id({account_id}) or perm(Admin)" - objects = ObjectManager() # populated by `return_appearance` @@ -1032,6 +1028,27 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): obj.msg(_(string)) obj.move_to(home, move_type="teleport") + @classmethod + def get_default_lockstring( + cls, account: "DefaultAccount" = None, caller: "DefaultObject" = None, **kwargs + ): + """ + Classmethod called during .create() to determine default locks for the object. + + Args: + account (Account): Account to attribute this object to. + caller (DefaultObject): The object which is creating this one. + **kwargs: Arbitrary input. + + Returns: + lockstring (str): A lockstring to use for this object. + """ + pid = f"pid({account.id})" if account else None + cid = f"id({caller.id})" if caller else None + admin = "perm(Admin)" + trio = " or ".join([x for x in [pid, cid, admin] if x]) + return ";".join([f"{x}:{trio}" for x in ["control", "delete", "edit"]]) + @classmethod def create( cls, @@ -1080,8 +1097,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): # Create a sane lockstring if one wasn't supplied lockstring = kwargs.get("locks") - if account and not lockstring: - lockstring = cls.lockstring.format(account_id=account.id) + if (account or caller) and not lockstring: + lockstring = cls.get_default_lockstring(account=account, caller=caller, **kwargs) kwargs["locks"] = lockstring # Create object @@ -1100,7 +1117,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase): obj.db.desc = desc except Exception as e: - errors.append("An error occurred while creating this '%s' object." % key) + errors.append(f"An error occurred while creating this '{key}' object: {e}") logger.log_err(e) return obj, errors @@ -2543,6 +2560,33 @@ class DefaultCharacter(DefaultObject): "edit:pid({account_id}) or perm(Admin)" ) + @classmethod + def get_default_lockstring( + cls, account: "DefaultAccount" = None, caller: "DefaultObject" = None, **kwargs + ): + """ + Classmethod called during .create() to determine default locks for the object. + + Args: + account (Account): Account to attribute this object to. + caller (DefaultObject): The object which is creating this one. + **kwargs: Arbitrary input. + + Returns: + lockstring (str): A lockstring to use for this object. + """ + pid = f"pid({account.id})" if account else None + character = kwargs.get("character", None) + cid = f"id({character})" if character else None + + puppet = "puppet:" + " or ".join( + [x for x in [pid, cid, "perm(Developer)", "pperm(Developer)"] if x] + ) + delete = "delete:" + " or ".join([x for x in [pid, "perm(Admin)"] if x]) + edit = "edit:" + " or ".join([x for x in [pid, "perm(Admin)"] if x]) + + return ";".join([puppet, delete, edit]) + @classmethod def create(cls, key, account=None, **kwargs): """ @@ -2613,21 +2657,20 @@ class DefaultCharacter(DefaultObject): account.characters.add(obj) # Add locks - if not locks and account: + if not locks: # Allow only the character itself and the creator account to puppet this character # (and Developers). - locks = cls.lockstring.format(character_id=obj.id, account_id=account.id) - elif not locks and not account: - locks = cls.lockstring.format(character_id=obj.id, account_id=-1) + locks = cls.get_default_lockstring(account=account, character=obj) - obj.locks.add(locks) + if locks: + obj.locks.add(locks) # If no description is set, set a default description if description or not obj.db.desc: obj.db.desc = description if description else _("This is a character.") except Exception as e: - errors.append(f"An error occurred while creating object '{key} object.") + errors.append(f"An error occurred while creating object '{key} object: {e}") logger.log_err(e) return obj, errors @@ -2825,14 +2868,6 @@ class DefaultRoom(DefaultObject): # Generally, a room isn't expected to HAVE a location, but maybe in some games? _content_types = ("room",) - # lockstring of newly created rooms, for easy overloading. - # Will be formatted with the {id} of the creating object. - lockstring = ( - "control:id({id}) or perm(Admin); " - "delete:id({id}) or perm(Admin); " - "edit:id({id}) or perm(Admin)" - ) - @classmethod def create( cls, @@ -2891,12 +2926,10 @@ class DefaultRoom(DefaultObject): obj = create.create_object(**kwargs) # Add locks - if not locks and account: - locks = cls.lockstring.format(id=account.id) - elif not locks and not account: - locks = cls.lockstring.format(id=obj.id) - - obj.locks.add(locks) + if not locks: + locks = cls.get_default_lockstring(account=account, caller=caller, room=obj) + if locks: + obj.locks.add(locks) # Record creator id and creation IP if ip: @@ -2909,7 +2942,7 @@ class DefaultRoom(DefaultObject): obj.db.desc = description if description else _("This is a room.") except Exception as e: - errors.append("An error occurred while creating this '%s' object." % key) + errors.append(f"An error occurred while creating this '{key}' object: {e}") logger.log_err(e) return obj, errors @@ -3000,14 +3033,6 @@ class DefaultExit(DefaultObject): exit_command = ExitCommand priority = 101 - # lockstring of newly created exits, for easy overloading. - # Will be formatted with the {id} of the creating object. - lockstring = ( - "control:id({id}) or perm(Admin); " - "delete:id({id}) or perm(Admin); " - "edit:id({id}) or perm(Admin)" - ) - # Helper classes and methods to implement the Exit. These need not # be overloaded unless one want to change the foundation for how # Exits work. See the end of the class for hook methods to overload. @@ -3110,11 +3135,10 @@ class DefaultExit(DefaultObject): obj = create.create_object(**kwargs) # Set appropriate locks - if not locks and account: - locks = cls.lockstring.format(id=account.id) - elif not locks and not account: - locks = cls.lockstring.format(id=obj.id) - obj.locks.add(locks) + if not locks: + locks = cls.get_default_lockstring(account=account, caller=caller, exit=obj) + if locks: + obj.locks.add(locks) # Record creator id and creation IP if ip: @@ -3127,7 +3151,7 @@ class DefaultExit(DefaultObject): obj.db.desc = description if description else _("This is an exit.") except Exception as e: - errors.append("An error occurred while creating this '%s' object." % key) + errors.append(f"An error occurred while creating this '{key}' object: {e}") logger.log_err(e) return obj, errors diff --git a/evennia/objects/tests.py b/evennia/objects/tests.py index ea1ac4bfc3..34f3c589bc 100644 --- a/evennia/objects/tests.py +++ b/evennia/objects/tests.py @@ -114,6 +114,31 @@ class DefaultObjectTest(BaseEvenniaTest): # partial match to 'colon' - multimatch error since stack is not homogenous self.assertEqual(self.char1.search("co", stacked=2), None) + def test_get_default_lockstring_base(self): + pattern = f"control:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);delete:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);edit:pid({self.account.id}) or id({self.char1.id}) or perm(Admin)" + self.assertEqual( + DefaultObject.get_default_lockstring(account=self.account, caller=self.char1), pattern + ) + + def test_get_default_lockstring_room(self): + pattern = f"control:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);delete:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);edit:pid({self.account.id}) or id({self.char1.id}) or perm(Admin)" + self.assertEqual( + DefaultRoom.get_default_lockstring(account=self.account, caller=self.char1), pattern + ) + + def test_get_default_lockstring_exit(self): + pattern = f"control:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);delete:pid({self.account.id}) or id({self.char1.id}) or perm(Admin);edit:pid({self.account.id}) or id({self.char1.id}) or perm(Admin)" + self.assertEqual( + DefaultExit.get_default_lockstring(account=self.account, caller=self.char1), pattern + ) + + def test_get_default_lockstring_character(self): + pattern = f"puppet:pid({self.account.id}) or perm(Developer) or pperm(Developer);delete:pid({self.account.id}) or perm(Admin);edit:pid({self.account.id}) or perm(Admin)" + self.assertEqual( + DefaultCharacter.get_default_lockstring(account=self.account, caller=self.char1), + pattern, + ) + class TestObjectManager(BaseEvenniaTest): "Test object manager methods"