From f3c6eca0315bcfab3901c88d745180663226cc40 Mon Sep 17 00:00:00 2001 From: TheCommCraft Date: Sun, 30 Jun 2024 14:34:41 +0200 Subject: [PATCH] 0.6.12 --- dungeonmaker/dm_backend/backend.py | 49 ++++++---- dungeonmaker/dm_backend/modules/dm/dmtypes.py | 14 ++- dungeonmaker/dm_backend/modules/dm/room.py | 20 +++- dungeonmaker/dm_backend/modules/dm/session.py | 95 ++++++++++++++----- dungeonmaker/dm_backend/modules/dm/utils.py | 2 +- setup.py | 6 +- 6 files changed, 134 insertions(+), 52 deletions(-) diff --git a/dungeonmaker/dm_backend/backend.py b/dungeonmaker/dm_backend/backend.py index 5d53718..3e6e5b6 100644 --- a/dungeonmaker/dm_backend/backend.py +++ b/dungeonmaker/dm_backend/backend.py @@ -124,7 +124,7 @@ def load_private_profile() -> json.dumps: def load_profile(username : str = None, *, user_id : str = None) -> json.dumps: user : User try: - user = self.dm_session.find(USER, id=user_id, name=username) + user = self.dm_session.find(USER, __id=user_id, name=username) except KeyError: raise ErrorMessage(json.dumps({"success": False, "result": None, "reason": "That profile doesn't seem to exist."})) user_data = s_vars(user) @@ -181,13 +181,13 @@ def reset_password(username : str, password : str = None, linked_user : str = No if code is None: code = secrets.randbits(24) self.current_client_data["password_reset_code"] = code - return f"Your password reset code is \"{code}\". Try commenting \"Password reset code: {code}\" using your linked account." + return f"Your password reset code is \"{code}\". Comment \"Password reset code: {code}\" using your linked account." if code != self.current_client_data["password_reset_code"]: raise ErrorMessage("Wrong code.") user = self.dm_session.find(USER, name=username) + if user.linked_user is None: + raise ErrorMessage("You do not have a user linked, so you cannot reset your password like this. Try commenting on the project for help.") if linked_user != user.linked_user: - if user.linked_user is None: - raise ErrorMessage("You do not have a user linked, so you cannot reset your password like this. Try commenting on the project for help.") raise ErrorMessage("That is not your linked user.") project = self.project comment = f"Password reset code: {code}" @@ -207,7 +207,7 @@ def logout() -> str: return "OK" @self.request_handler.request(name="save_dungeon", allow_python_syntax=True, auto_convert=True) - def save_dungeon(start_room : RoomId, start_x : int, start_y : int, name : str, dungeon_id=None): + def save_dungeon(start_room : RoomId, start_x : int, start_y : int, name : str = None, dungeon_id : DungeonId = None): dungeon : Dungeon self.ensure_login() try: @@ -221,10 +221,21 @@ def save_dungeon(start_room : RoomId, start_x : int, start_y : int, name : str, "owner_name": self.current_client_data["username"], "start": () }) + if not name: + raise ErrorMessage("You need to pick a name.") + if not find_comment(self.project, content=f"Set name of {dungeon.dungeon_id} to {name}"): + raise ErrorMessage(f"Could not confirm name. Comment \"Set name of {dungeon.dungeon_id} to {name}\" and try again.") + user = self.find_current_client_user() + user.remaining_dungeons -= 1 + user.owned_dungeons.append(dungeon_id) + user.permitted_dungeons.append(dungeon_id) + user.write() if not dungeon.get_user(self.current_client_data["username"]).permitions.get("edit_infos"): raise ErrorMessage("Not Authorized") - #if not find_comment(self.pr) - dungeon.name = name + if name: + if not find_comment(self.project, content=f"Set name of {dungeon.dungeon_id} to {name}"): + raise ErrorMessage(f"Could not confirm name. Comment \"Set name of {dungeon.dungeon_id} to {name}\" and try again.") + dungeon.name = name dungeon.start = (start_room, start_x, start_y) dungeon.write() @@ -240,13 +251,16 @@ def save_room(room_id : RoomId, content : str, bound_dungeon : DungeonId) -> str try: room = self.dm_session.find(ROOM, room_id) except KeyError: - dungeon = self.dm_session.find(DUNGEON, bound_dungeon) - if not ( - (permitions := dungeon.get_user(user_id=self.current_client_data["user_id"]).permitions).get("edit_room") == room_id or - permitions.get("edit_rooms") - ): + try: + dungeon = self.dm_session.find(DUNGEON, bound_dungeon) + except KeyError: + raise ErrorMessage("Dungeon does not exist.") + if not dungeon.permissions[self.current_client_data["user_id"]].can_edit_room(room_id=room_id): raise ErrorMessage("Not authorized") room = dungeon.new_room(room_id=room_id) + user = self.find_current_client_user() + user.remaining_rooms -= 1 + user.write() else: if not room.dungeon_id == bound_dungeon: raise ErrorMessage("Wrong dungeon bound.") @@ -254,10 +268,7 @@ def save_room(room_id : RoomId, content : str, bound_dungeon : DungeonId) -> str dungeon = self.dm_session.find(DUNGEON, bound_dungeon) except KeyError: raise ErrorMessage("Dungeon does not exist.") - if not ( - (permitions := dungeon.get_user(user_id=self.current_client_data["user_id"]).permitions).get("edit_room") == room_id or - permitions.get("edit_rooms") - ): + if not dungeon.permissions[self.current_client_data["user_id"]].can_edit_room(room_id=room_id): raise ErrorMessage("Not authorized") room.content = content room.write() @@ -293,6 +304,7 @@ def unlike_dungeon(dungeon_id : DungeonId) -> str: @self.request_handler.request(name="load_tab", allow_python_syntax=True, auto_convert=True) def load_tab(tab : str) -> json.dumps: + data = [] if tab == "popular": data = self.dm_session.get_popular_tab() elif tab == "random": @@ -340,8 +352,6 @@ def find_current_client_user(self) -> User: - - def gen_passdata(*, username : str, password : str) -> bytes: """ Generate passdata for an account @@ -395,6 +405,3 @@ def include_data(data : dict, *, include : list[str] = ()) -> dict: - - - diff --git a/dungeonmaker/dm_backend/modules/dm/dmtypes.py b/dungeonmaker/dm_backend/modules/dm/dmtypes.py index 26f7100..e2c2b6a 100644 --- a/dungeonmaker/dm_backend/modules/dm/dmtypes.py +++ b/dungeonmaker/dm_backend/modules/dm/dmtypes.py @@ -3,6 +3,7 @@ """ from __future__ import annotations from typing import Literal, Any, Union, Sequence, Mapping +from weakref import WeakValueDictionary from dataclasses import dataclass, field import secrets, time @@ -121,8 +122,15 @@ def get(self, __type : Any) -> Permission: return ([i for i in self if i.type == __type] + [Permission(type=__type, value=None)])[0] except IndexError: pass - - + + def get_all(self, __type : Any) -> list[Permission]: + return [i for i in self if i.type == __type] + + def can_edit_room(self, *, room_id : RoomId = None, room : BaseRoom = None): + room_id = room_id or room.room_id + if self.get("edit_rooms"): + return True + return any(i.value == room_id for i in self.get_all("edit_room")) class Stats(dict): """ @@ -177,6 +185,8 @@ class BaseUser: session : BaseDMSession = field(kw_only=True) passdata : bytes = field(kw_only=True) linked_user : Union[str, None] = field(kw_only=True, default=None) + remaining_dungeons : int = field(kw_only=True, default=16) + remaining_rooms : int = field(kw_only=True, default=128) stats : Stats = field(kw_only=True, default_factory=Stats) diff --git a/dungeonmaker/dm_backend/modules/dm/room.py b/dungeonmaker/dm_backend/modules/dm/room.py index 0c36720..1515d9a 100644 --- a/dungeonmaker/dm_backend/modules/dm/room.py +++ b/dungeonmaker/dm_backend/modules/dm/room.py @@ -3,10 +3,11 @@ """ from __future__ import annotations from typing import Self -from .dmtypes import RoomId, BaseDungeon, BaseRoom -from . import dungeon +from .dmtypes import RoomId, BaseDungeon, BaseRoom, UserId +from . import dungeon, user from . import session as _session from .utils import s_vars +from .selectors import DUNGEON class Room(BaseRoom): """ @@ -35,7 +36,20 @@ def write(self): self.session.database_abstraction.insert_room(data=s_vars(self)) return self.session.database_abstraction.update_room(room_id=self.room_id, updator={"$set": s_vars(self)}) - + + def get_dungeon(self): + """ + Get the corresponding dungeon. + """ + return self.session.find(DUNGEON, self.dungeon_id) + + def can_be_edited(self, *, user : user.User = None, user_id : UserId) -> bool: + """ + Return whether a certain user can edit a room. + """ + user_id = user_id or user.user_id + __dungeon = self.get_dungeon() + return __dungeon.permissions[user_id].can_edit_room(self.room_id) diff --git a/dungeonmaker/dm_backend/modules/dm/session.py b/dungeonmaker/dm_backend/modules/dm/session.py index a72e8b2..8cb2804 100644 --- a/dungeonmaker/dm_backend/modules/dm/session.py +++ b/dungeonmaker/dm_backend/modules/dm/session.py @@ -3,6 +3,7 @@ """ from __future__ import annotations import random +from weakref import WeakValueDictionary from typing import Literal, Union, assert_never, Sequence, Mapping from dataclasses import dataclass, field from . import dungeon, user, room @@ -12,10 +13,21 @@ @dataclass class DMSession: + database_abstraction : _dba.DatabaseAbstractionSelector + database_abstractions : list[BaseDatabaseAbstraction] + _cached : dict[WeakValueDictionary[str, Union[dungeon.Dungeon, user.User, room.Room]]] def __init__(self, *, database_abstractions : list = None): self.database_abstractions = list(database_abstractions or ()) self.database_abstraction = _dba.DatabaseAbstractionSelector(self.database_abstractions) + self.setup_cache() + + def setup_cache(self): + self._cached = {} + self._cached["dungeons"] = WeakValueDictionary() + self._cached["users"] = WeakValueDictionary() + self._cached["rooms"] = WeakValueDictionary() + def add_database_abstraction(self, dba : BaseDatabaseAbstraction): """ @@ -74,11 +86,11 @@ def search_for_term(self, term : str, *, amount : int = 10) -> list[dungeon.Dung "$addFields": { "score": { "$add": [ - {"$multiply": + {"$multiply": [ 20, {"$size": "$likers" } - ] - }, + ] + }, "$views" ] } @@ -87,31 +99,70 @@ def search_for_term(self, term : str, *, amount : int = 10) -> list[dungeon.Dung ] return [dungeon.Dungeon(**dungeon_data) for dungeon_data in self.database_abstraction.sorted_dungeons(amount=amount, field="score", aggregation=aggregator)] - def find(self, type : Literal["dungeon", "room", "user"], id : Union[DungeonId, RoomId, UserId] = None, *, name : str = None) -> Union[dungeon.Dungeon, room.Room, user.User]: + def lookup_cache(self, cache_type : str, __id : Union[DungeonId, RoomId, UserId]) -> Union[None, dungeon.Dungeon, room.Room, user.User]: """ - Finds something. + Don't use. + """ + if __id in (cache := self._cached[cache_type]): + return cache[__id] + + def save_cache(self, cache_type : str, __id : Union[DungeonId, RoomId, UserId], value : Union[dungeon.Dungeon, room.Room, user.User]): + """ + Don't use. """ - if type == DUNGEON: - return dungeon.Dungeon.read(dungeon_id=id, session=self) - if type == ROOM: - return room.Room.read(id, session=self) - if type == USER: - return user.User.lookup_user(user_id=id, username=name, session=self) - assert_never(type) + self._cached[cache_type][__id] = value - def create(self, type : Literal["dungeon", "room", "user"], *, args : Sequence = (), kwargs : Mapping = {}) -> Union[dungeon.Dungeon, room.Room, user.User]: + def find(self, __type : Literal["dungeon", "room", "user"], __id : Union[DungeonId, RoomId, UserId] = None, *, name : str = None) -> Union[dungeon.Dungeon, room.Room, user.User]: + """ + Finds something. + """ + if __type == DUNGEON: + if (value := self.lookup_cache("dungeons", __id)): + return value + __dungeon = dungeon.Dungeon.read(dungeon_id=__id, session=self) + self.save_cache("dungeons", __id, __dungeon) + return __dungeon + if __type == ROOM: + if (value := self.lookup_cache("rooms", __id)): + return value + __room = room.Room.read(__id, session=self) + self.save_cache("rooms", __id, __room) + return __room + if __type == USER: + if (value := self.lookup_cache("users", __id)): + return value + __user = user.User.lookup_user(user_id=__id, username=name, session=self) + self.save_cache("users", __id, __user) + return __user + assert_never(__type) + + def create(self, __type : Literal["dungeon", "room", "user"], *, args : Sequence = (), kwargs : Mapping = None) -> Union[dungeon.Dungeon, room.Room, user.User]: """ Creates something. """ - args = list(args) - kwargs = dict(kwargs) - if type == DUNGEON: - return dungeon.Dungeon(*args, session=self, **kwargs) - if type == ROOM: - return room.Room(*args, session=self, **kwargs) - if type == USER: - return user.User(*args, session=self, **kwargs) - assert_never(type) + kwargs = kwargs or {} + if __type == DUNGEON: + __dungeon = dungeon.Dungeon(*args, session=self, **kwargs) + __id = __dungeon.dungeon_id + if self.lookup_cache("dungeons", __id): + raise ValueError("Dungeon already exists (in cache!!)") + self.save_cache("dungeons", __id, __dungeon) + return __dungeon + if __type == ROOM: + __room = room.Room(*args, session=self, **kwargs) + __id = __room.room_id + if self.lookup_cache("rooms", __id): + raise ValueError("Room already exists (in cache!!)") + self.save_cache("rooms", __id, __room) + return __room + if __type == USER: + __user = user.User(*args, session=self, **kwargs) + __id = __user.user_id + if self.lookup_cache("users", __id): + raise ValueError("User already exists (in cache!!)") + self.save_cache("users", __id, __user) + return __user + assert_never(__type) diff --git a/dungeonmaker/dm_backend/modules/dm/utils.py b/dungeonmaker/dm_backend/modules/dm/utils.py index 068459d..75d2c3c 100644 --- a/dungeonmaker/dm_backend/modules/dm/utils.py +++ b/dungeonmaker/dm_backend/modules/dm/utils.py @@ -2,4 +2,4 @@ def s_vars(__obj) -> dict: """ Use like vars() but for objects with __slots__. """ - return {slot: getattr(__obj, slot) for slot in __obj.__slots__ if not slot in ["_id", "session"]} \ No newline at end of file + return {slot: getattr(__obj, slot) for slot in __obj.__slots__ if not slot in ["_id", "session", "_cached"]} \ No newline at end of file diff --git a/setup.py b/setup.py index b2fcfe0..841ad68 100644 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name='dungeonmaker', - version='0.6.11', + version='0.6.12', author='Simon Gilde', author_email='simon.c.gilde@gmail.com', description='Dungeon maker backend and other things', @@ -17,7 +17,7 @@ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', ], keywords=['scratch'], install_requires=[ @@ -25,5 +25,5 @@ 'pymongo', 'scratchattach', ], - python_requires='>=3.10', + python_requires='>=3.11', ) \ No newline at end of file