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 import uvicorn
if __name__ == "__main__": if __name__ == "__main__":
uvicorn.run("customiwmserver.main:app", host="0.0.0.0", port=8001, reload=True) 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 Type: int = 0
class Map(BaseModel): class Map(BaseModel):
"""Pydantic model for maps. """Pydantic model for maps.
It looks like a lot of variables are also included in the user It looks like a lot of variables are also included in the user
data. data.
""" """
ID: int = 0 ID: int = 0
CreatorId: int = 0 CreatorId: int = 0
@ -189,12 +191,10 @@ class Map(BaseModel):
NumThumbsUp: int = 0 NumThumbsUp: int = 0
NumThumbsDown: int = 0 NumThumbsDown: int = 0
TagIDs: str = "" TagIDs: str = ""
TagNames: str = "" TagNames: str = ""
TagFreqs: str = "" TagFreqs: str = ""
PlayCount: int = 0 PlayCount: int = 0
ClearCount: int = 0 ClearCount: int = 0
@ -218,7 +218,9 @@ class Map(BaseModel):
# It's still useful for requests though. # It's still useful for requests though.
FirstClearUsername: Union[str, None] = "uwu" FirstClearUsername: Union[str, None] = "uwu"
MapData: str = "" 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 Played: bool = False
Clear: bool = False Clear: bool = False
FullClear: bool = False FullClear: bool = False
@ -229,6 +231,7 @@ class Map(BaseModel):
class MapLeaderboard(BaseModel): class MapLeaderboard(BaseModel):
"""Pydantic model for Map record leaderboards.""" """Pydantic model for Map record leaderboards."""
BestPlaytime: int BestPlaytime: int
BestPlaytimeTime: str BestPlaytimeTime: str
BestReplay: str BestReplay: str
@ -271,11 +274,11 @@ ids_to_names = {
4: "Auto", 4: "Auto",
} }
def convertTagsToNames(tags): def convertTagsToNames(tags):
"""Converts tag IDs to names""" """Converts tag IDs to names"""
global ids_to_names global ids_to_names
tagNames = [] tagNames = []
for i in tags.split(','): for i in tags.split(","):
tagNames.append(ids_to_names[int(i)]) tagNames.append(ids_to_names[int(i)])
return tagNames return tagNames

View file

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