Add hook system

+ Formatting
This commit is contained in:
magmaus3 2022-11-19 13:51:28 +01:00
parent 36dff3d99a
commit 5360b128e0
Signed by: magmaus3
GPG key ID: 966755D3F4A9B251
7 changed files with 350 additions and 206 deletions

View file

@ -1,3 +1,4 @@
import uvicorn
if __name__ == "__main__":
uvicorn.run("customiwmserver.main:app", host="0.0.0.0", port=8001, reload=True)

View file

@ -133,11 +133,13 @@ class Notification(BaseModel):
}
Type: int = 0
class Map(BaseModel):
"""Pydantic model for maps.
It looks like a lot of variables are also included in the user
data.
"""
ID: int = 0
CreatorId: int = 0
@ -189,12 +191,10 @@ class Map(BaseModel):
NumThumbsUp: int = 0
NumThumbsDown: int = 0
TagIDs: str = ""
TagNames: str = ""
TagFreqs: str = ""
PlayCount: int = 0
ClearCount: int = 0
@ -218,7 +218,9 @@ class Map(BaseModel):
# It's still useful for requests though.
FirstClearUsername: Union[str, None] = "uwu"
MapData: str = ""
MapReplay: str = "" # Might be also stored in DB as a dict of user IDs and the replays
MapReplay: str = (
"" # Might be also stored in DB as a dict of user IDs and the replays
)
Played: bool = False
Clear: bool = False
FullClear: bool = False
@ -229,6 +231,7 @@ class Map(BaseModel):
class MapLeaderboard(BaseModel):
"""Pydantic model for Map record leaderboards."""
BestPlaytime: int
BestPlaytimeTime: str
BestReplay: str
@ -271,11 +274,11 @@ ids_to_names = {
4: "Auto",
}
def convertTagsToNames(tags):
"""Converts tag IDs to names"""
global ids_to_names
tagNames = []
for i in tags.split(','):
for i in tags.split(","):
tagNames.append(ids_to_names[int(i)])
return tagNames

View file

@ -14,13 +14,19 @@ reports_collection = db.reports
general_collection = db.general
admin_log_collection = db.admin_log
def LogAdminAction(action_type: str, action_data: dict, UserID: int = None, success: bool = True):
def LogAdminAction(
action_type: str, action_data: dict, UserID: int = None, success: bool = True
):
"""Log administrator action."""
admin_log_collection.insert_one({
admin_log_collection.insert_one(
{
"date": datetime.utcnow(),
"action_type": action_type,
"action_data": action_data
})
"action_data": action_data,
}
)
def auth_check(Authorization):
"""Checks credentials.
@ -37,7 +43,7 @@ def auth_check(Authorization):
if not query:
return False, "nouser"
if query['Password'] != password:
if query["Password"] != password:
return False, "wrongpass"
return True, query

View file

@ -0,0 +1,46 @@
from functools import reduce
class HookException(Exception):
pass
class HookMan:
hooks = {}
def __new__(cls):
return reduce(lambda x, y: y(x), cls.hooks, super(HookMan, cls).__new__(cls))
def execute_hooks(self, hook_type, *args, **kwargs):
if hook_type not in self.hooks:
return
for hook in self.hooks[hook_type]:
print(f"Executing hook {hook.__name__}")
try:
hook()(*args, **kwargs)
except Exception as exc:
raise HookException(exc)
@classmethod
def add_hook(cls, func, *args, **kwargs):
"""Adds a hook."""
hook_type = func.hook_type
if hook_type in cls.hooks:
cls.hooks[hook_type] = cls.hooks[hook_type] + [func]
else:
cls.hooks[hook_type] = [func]
hook = HookMan()
# @hook.add_hook
# class example_hook:
# """Example hook."""
# hook_type = "player_login"
#
# def __call__(self, *args, **kwargs):
# print("Hook player_login,", kwargs)
#
#
hook.execute_hooks("player_login", hello="john")

View file

@ -0,0 +1,9 @@
# from . import *
import pkgutil
__path__ = pkgutil.extend_path(__path__, __name__)
for imp, module, ispackage in pkgutil.walk_packages(
path=__path__, prefix=__name__ + "."
):
__import__(module)

View file

@ -0,0 +1,18 @@
#
# Example module.
#
from .. import hook_system
hook = hook_system.hook
@hook.add_hook
class Login:
# Different hook types are executed at different places.
# In this case, the hook is executed when a user logs in.
# Check the documentation for more hook types.
hook_type = "player_login"
def __call__(self, *args, **kwargs):
print("LOGIN HOOK:", args, kwargs)

View file

@ -7,59 +7,75 @@ from typing import Union, Optional, Any
import uvicorn
from . import data_types as types
from . import database as db
from . import hook_system
from . import hooks
import pymongo
app = FastAPI()
hook = hook_system.hook
@app.exception_handler(RequestValidationError)
async def http_exception_handler(request, exc):
return PlainTextResponse(f"{str(exc)}", status_code=422)
@app.exception_handler(StarletteHTTPException)
async def http_exception_handler(request, exc):
return PlainTextResponse(f"[{exc.status_code}] {str(exc.detail)}", status_code=exc.status_code)
return PlainTextResponse(
f"[{exc.status_code}] {str(exc.detail)}", status_code=exc.status_code
)
## Users
@app.post("/api/v1/login")
async def login(username: str = Form(), password: str = Form(), version: str = Form()):
"""User login"""
# FIXME Add login
hook.execute_hooks("player_login", username=username)
return {"token": username + ":" + password, "userId": 1}
@app.put("/api/v1/user")
async def create_user(username: str = Form(), password: str = Form(), email: str = Form(), version: str = Form()):
async def create_user(
username: str = Form(),
password: str = Form(),
email: str = Form(),
version: str = Form(),
):
"""Create a user with specified credentials"""
token = username + ":" + password
UserID = len(list(db.user_collection.find({})))
if UserID == 0: UserID = 1
if UserID == 0:
UserID = 1
insert_data = {
**types.User(
Username=username,
Email=email,
ID=UserID
).dict(),
**types.User(Username=username, Email=email, ID=UserID).dict(),
"Password": password,
}
db.user_collection.insert_one(insert_data)
return {"token": token, "userId": UserID}
@app.get("/api/v1/notifunread")
async def notifunread():
"""Returns the number of unread notifications."""
# FIXME Add notifications
return 0
@app.get("/api/v1/refresh")
async def refresh_login(Authorization: Union[str, None] = Header(default=None)):
"""Intended for refreshing user token."""
return {"token": Authorization}
@app.get("/api/v1/user/{user_id}")
async def get_user(user_id: int):
"""Returns specified user's profile."""
@ -68,16 +84,21 @@ async def get_user(user_id: int):
del query["Password"]
return types.User(**query)
## Maps
@app.get("/api/v1/mapcount")
async def mapcount():
return len(list(db.maps_collection.find({})))
@app.get("/api/v1/useruploadcooldown")
async def useruploadcooldown():
"""Limits the amount of levels that user can upload."""
return {"success": True}
@app.get("/api/v1/map")
async def search_for_maps(
start: int = 0,
@ -88,7 +109,7 @@ async def search_for_maps(
name: str = "",
author: str = "",
author_id: Optional[int] = None,
last_x_hours: Optional[int] = None
last_x_hours: Optional[int] = None,
):
"""Search for maps."""
@ -96,21 +117,23 @@ async def search_for_maps(
query = db.maps_collection.find({}).limit(limit)
entries = []
for i in query:
del i['_id']
del i['MapData']
del i["_id"]
del i["MapData"]
entries.append(i)
return entries
@app.get("/api/v1/map/{mapID}")
async def getMap(mapID: int):
query = db.maps_collection.find_one({"ID": mapID})
del query['_id']
del query["_id"]
return query
@app.post("/api/v1/map/{mapID}/start")
async def getMap(mapID: int):
query = db.maps_collection.find_one({"ID": mapID})
del query['_id']
del query["_id"]
returned_resp = {
"BestDeaths": 0,
@ -122,10 +145,11 @@ async def getMap(mapID: int):
"Played": True,
"Rating": 5,
"TagIDs": "1,8,9",
"TagNames": "Boss/Avoidance,Music,Art"
"TagNames": "Boss/Avoidance,Music,Art",
}
return returned_resp
@app.post("/api/v1/map/{mapID}/stop")
async def stopMapPlay(
mapID: int,
@ -149,29 +173,32 @@ async def stopMapPlay(
userData = types.User(**authcheck[1])
query = db.maps_collection.find_one({"ID": mapID, "Leaderboard.UserID": userData.ID})
if query is not None: del query['_id']
query = db.maps_collection.find_one(
{"ID": mapID, "Leaderboard.UserID": userData.ID}
)
if query is not None:
del query["_id"]
print(__import__("json").dumps(query, indent=4))
BestUserTime = None
BestTime = None
if query is not None and 'Leaderboard' in query:
if query is not None and "Leaderboard" in query:
print("-" * 3)
for i in query['Leaderboard']:
if i['UserID'] == userData.ID:
if BestUserTime is None or BestUserTime > i['BestPlaytime'] :
BestUserTime = i['BestPlaytime']
if BestTime is None or BestTime > i['BestPlaytime']:
for i in query["Leaderboard"]:
if i["UserID"] == userData.ID:
if BestUserTime is None or BestUserTime > i["BestPlaytime"]:
BestUserTime = i["BestPlaytime"]
if BestTime is None or BestTime > i["BestPlaytime"]:
BestTime = i["BestPlaytime"]
if len(query['Leaderboard']) <= 1 and i['UserID'] != userData.ID:
if len(query["Leaderboard"]) <= 1 and i["UserID"] != userData.ID:
FirstClear = True
print(BestUserTime, BestTime)
if BestUserTime is None or playtime < BestUserTime:
print(BestUserTime)
updateQuery = db.maps_collection.update_one({"ID": mapID},
updateQuery = db.maps_collection.update_one(
{"ID": mapID},
{
"$push": {
"Leaderboard": types.MapLeaderboard(
ShoesColor=userData.ShoesColor,
@ -195,26 +222,38 @@ async def stopMapPlay(
FollowerColor=userData.FollowerColor,
SaveEffect=userData.SaveEffect,
TextSnd=userData.TextSnd,
BestPlaytime=playtime,
BestPlaytimeTime="2020-02-13T15:19:33Z",
BestReplay=replayData,
CreatorName=userData.Username,
UserID=userData.ID
UserID=userData.ID,
).dict()
},
})
},
)
if BestTime is None or playtime < BestTime:
print(BestTime, playtime)
NewMapRecord = True
hook.execute_hooks(
"map_finished",
user=userData,
mapID=mapID,
clear=clear,
deaths=deaths,
playtime=playtime,
FirstClear=FirstClear,
NewMapRecord=NewMapRecord,
)
return {"FirstClear": FirstClear, "NewMapRecord": NewMapRecord}
@app.put("/api/v1/map")
async def upload_map(
Authorization: Union[str, None] = Header(default=None),
mapName: str = Form(),
mapDescription: str = Form(default=''),
mapDescription: str = Form(default=""),
mapVersion: int = Form(),
mapData: str = Form(),
file: Optional[UploadFile] = None,
@ -239,11 +278,11 @@ async def upload_map(
print(authcheck)
userData = types.User(**authcheck[1])
MapCode = "SUSYBAKA" # FIXME Add mapcodes correctly
db.maps_collection.insert_one({
db.maps_collection.insert_one(
{
**types.Map(
CreatorName=userData.Username,
CreatorId=userData.ID,
ID=len(list(db.maps_collection.find({}))) + 1,
Name=mapName,
Description=mapDescription,
@ -254,7 +293,6 @@ async def upload_map(
HiddenInChallenges=hideInChallenges,
TagIDs=tags,
TagNames=",".join(types.convertTagsToNames(tags)),
ShoesColor=userData.ShoesColor,
PantsColor=userData.PantsColor,
ShirtColor=userData.ShirtColor,
@ -276,40 +314,42 @@ async def upload_map(
FollowerColor=userData.FollowerColor,
SaveEffect=userData.SaveEffect,
TextSnd=userData.TextSnd,
Leaderboard=[
types.MapLeaderboard(
BestPlaytime=playtime,
BestPlaytimeTime="2020-02-13T15:19:33Z",
BestReplay=mapReplay,
CreatorName=userData.Username,
UserID=userData.ID
UserID=userData.ID,
).dict()
]
],
).dict()
})
}
)
return {"MapCode": MapCode}
# raise HTTPException(501)
@app.get("/api/v1/map/{mapID}/besttimes/{maxEntries}")
async def getMapLeaderboard(mapID, maxEntries):
"""Returns maxEntries records for the specified level"""
query = db.maps_collection.find_one({"ID": int(mapID)})
if not query:
raise HTTPException(404, detail="Map not found")
del query['_id']
leaderboard = query['Leaderboard']
del query["_id"]
leaderboard = query["Leaderboard"]
return leaderboard
@app.get("/api/v1/map/{mapID}/userbesttime/{userID}")
async def getPlayerRecord(mapID, userID):
"""Returns specific replay"""
query = db.maps_collection.find_one({"ID": int(mapID)})
if not query:
raise HTTPException(404, detail="Map not found")
del query['_id']
leaderboard = query['Leaderboard']
del query["_id"]
leaderboard = query["Leaderboard"]
# Find user
replayIndex = None
@ -320,21 +360,25 @@ async def getPlayerRecord(mapID, userID):
break
print(leaderboard[replayIndex])
if replayIndex is not None: return {"BestMapTime": leaderboard[replayIndex], "Exists": True}
else: return {"BestMapTime": None, "Exists": False}
if replayIndex is not None:
return {"BestMapTime": leaderboard[replayIndex], "Exists": True}
else:
return {"BestMapTime": None, "Exists": False}
@app.get("/api/v1/map/{mapID}/besttime")
async def getBestRecord(mapID: int):
query = db.maps_collection.find_one({"ID": int(mapID)})
BestTime = None
BestPlay = None
for i in query['Leaderboard']:
if BestTime is None or BestTime > i['BestPlaytime']:
for i in query["Leaderboard"]:
if BestTime is None or BestTime > i["BestPlaytime"]:
BestTime = i["BestPlaytime"]
BestPlay = i
return {"BestMapTime": BestPlay, "Exists": True}
@app.post("/api/v1/map/{mapID}/invalidatealltimes")
async def invalidateAllTimes(
mapID: int,
@ -352,38 +396,45 @@ async def invalidateAllTimes(
elif authcheck[0]:
userData = types.User(**authcheck[1])
if userData.Admin:
query = db.maps_collection.update_one({"ID": mapID}, {"$push": {
"Leaderboard": {
"$each": [],
"$slice": 0
}
}
})
db.LogAdminAction(action_type="invalidateAllTimes",
query = db.maps_collection.update_one(
{"ID": mapID}, {"$push": {"Leaderboard": {"$each": [], "$slice": 0}}}
)
db.LogAdminAction(
action_type="invalidateAllTimes",
action_data={
"Reason": Reason,
"CustomReason": CustomReason,
"mapID": mapID
"mapID": mapID,
},
UserID=userData.ID
UserID=userData.ID,
)
else:
db.LogAdminAction(action_type="invalidateAllTimes", action_data={
db.LogAdminAction(
action_type="invalidateAllTimes",
action_data={
"Reason": Reason,
"CustomReason": CustomReason,
"mapID": mapID,
"unauthorized": True
}, success=False)
raise HTTPException(403, detail="Attempted to perform an administrator action without permission. This will be reported.")
"unauthorized": True,
},
success=False,
)
raise HTTPException(
403,
detail="Attempted to perform an administrator action without permission. This will be reported.",
)
## General
@app.post("/api/v1/reports")
async def reportContent(
Authorization: Union[str, None] = Header(default=None),
user_id: int = Form(),
map_id: Optional[int] = Form(default=None),
report_type: int = Form(),
content: str = Form()
content: str = Form(),
):
authcheck = db.auth_check(Authorization)
@ -392,20 +443,30 @@ async def reportContent(
elif not authcheck[0] and authcheck[1] == "wrongpass":
raise HTTPException(403, detail="Wrong password")
elif authcheck[0]:
db.reports_collection.insert_one({"user_id": user_id, "map_id": map_id, "report_type": report_type, "content": content})
db.reports_collection.insert_one(
{
"user_id": user_id,
"map_id": map_id,
"report_type": report_type,
"content": content,
}
)
return True
@app.get("/api/v1/featuredlist")
async def featuredlist():
"""Returns the list id of the weekly levels list."""
# FIXME Add playlists
return
@app.get("/api/v1/followcheck")
async def followcheck():
# FIXME Find the purpouse of this endpoint
return 1
def start():
"""Launched with `poetry run start` at root level"""
uvicorn.run("customiwmserver.main:app", host="0.0.0.0", port=8001, reload=True)