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
@ -156,7 +158,7 @@ class Map(BaseModel):
DragonCoins: bool = False
# Might be duplicates from the User data
ShoesColor: int = 0
PantsColor: int = 0
ShirtColor: int = 10897172
@ -189,17 +191,15 @@ class Map(BaseModel):
NumThumbsUp: int = 0
NumThumbsDown: int = 0
TagIDs: str = ""
TagNames: str = ""
TagFreqs: str = ""
PlayCount: int = 0
ClearCount: int = 0
# I'm not sure how ClearRate is handled
# in the official server, but I assume it's
# in the official server, but I assume it's
# calculated as ClearCount / PlayCount.
ClearRate: float = 0.0
FavoriteCount: int = 0
@ -216,9 +216,11 @@ class Map(BaseModel):
# Propably not needed, because you can simply read the
# FirstClearUserID, and get the name from there
# It's still useful for requests though.
FirstClearUsername: Union[str, None] = "uwu"
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
@ -256,7 +259,7 @@ class MapLeaderboard(BaseModel):
FollowerColor: int = 0
SaveEffect: int = 0
TextSnd: int = 0
ids_to_names = {
0: "Adventure/Variety",
@ -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({
"date": datetime.utcnow(),
"action_type": action_type,
"action_data": action_data
})
admin_log_collection.insert_one(
{
"date": datetime.utcnow(),
"action_type": action_type,
"action_data": action_data,
}
)
def auth_check(Authorization):
"""Checks credentials.
@ -33,11 +39,11 @@ def auth_check(Authorization):
"""
username, password = Authorization.split(":")
query = user_collection.find_one({"Username": username})
query = user_collection.find_one({"Username": username})
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,136 +7,160 @@ 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
# 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
# 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."""
#FIXME use database
# FIXME use database
query = db.user_collection.find_one({"ID": user_id})
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,
limit: int = 5,
min_diff: float = 0.0,
max_diff: float = 5.0,
order: str = '[{ "Dir": "desc", "Name": "created_at" }]',
name: str = "",
author: str = "",
author_id: Optional[int] = None,
last_x_hours: Optional[int] = None
):
start: int = 0,
limit: int = 5,
min_diff: float = 0.0,
max_diff: float = 5.0,
order: str = '[{ "Dir": "desc", "Name": "created_at" }]',
name: str = "",
author: str = "",
author_id: Optional[int] = None,
last_x_hours: Optional[int] = None,
):
"""Search for maps."""
# query = db.maps_collection.find({ "CreatorId": author_id }).limit(limit)
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']
query = db.maps_collection.find_one({"ID": mapID})
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']
query = db.maps_collection.find_one({"ID": mapID})
del query["_id"]
returned_resp = {
"BestDeaths": 0,
"BestPlaytime": 0,
"Clear": False,
"CurMap": query,
"Difficulty": 0,
"Followed": False,
"Played": True,
"Rating": 5,
"TagIDs": "1,8,9",
"TagNames": "Boss/Avoidance,Music,Art"
}
"BestDeaths": 0,
"BestPlaytime": 0,
"Clear": False,
"CurMap": query,
"Difficulty": 0,
"Followed": False,
"Played": True,
"Rating": 5,
"TagIDs": "1,8,9",
"TagNames": "Boss/Avoidance,Music,Art",
}
return returned_resp
@app.post("/api/v1/map/{mapID}/stop")
async def stopMapPlay(
mapID: int,
clear: int = Form(),
deaths: int = Form(),
playtime: int = Form(),
totalDeaths: int = Form(),
totalTime: int = Form(),
replayData: str = Form(),
Authorization: Union[str, None] = Header(default=None),
):
mapID: int,
clear: int = Form(),
deaths: int = Form(),
playtime: int = Form(),
totalDeaths: int = Form(),
totalTime: int = Form(),
replayData: str = Form(),
Authorization: Union[str, None] = Header(default=None),
):
"""Saves the map replay, and informs the user if their play is a record"""
authcheck = db.auth_check(Authorization)
if not authcheck[0] and authcheck[1] == "nouser":
@ -146,32 +170,35 @@ async def stopMapPlay(
elif authcheck[0]:
NewMapRecord = False
FirstClear = False
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']
print(__import__("json").dumps(query,indent=4))
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:
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']:
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"]:
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,41 +222,53 @@ 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
).dict()
},
})
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=''),
mapVersion: int = Form(),
mapData: str = Form(),
file: Optional[UploadFile] = None,
mapReplay: str = Form(),
deaths: int = Form(),
playtime: int = Form(),
totalDeaths: int = Form(),
totalTime: int = Form(),
listed: int = Form(),
requiresCancels: int = Form(),
hideInChallenges: int = Form(),
tags: str = Form(),
rng: int = Form(),
clientVersion: float = Form(),
):
Authorization: Union[str, None] = Header(default=None),
mapName: str = Form(),
mapDescription: str = Form(default=""),
mapVersion: int = Form(),
mapData: str = Form(),
file: Optional[UploadFile] = None,
mapReplay: str = Form(),
deaths: int = Form(),
playtime: int = Form(),
totalDeaths: int = Form(),
totalTime: int = Form(),
listed: int = Form(),
requiresCancels: int = Form(),
hideInChallenges: int = Form(),
tags: str = Form(),
rng: int = Form(),
clientVersion: float = Form(),
):
authcheck = db.auth_check(Authorization)
if not authcheck[0] and authcheck[1] == "nouser":
raise HTTPException(404, detail="User not found")
@ -238,78 +277,79 @@ async def upload_map(
elif authcheck[0]:
print(authcheck)
userData = types.User(**authcheck[1])
MapCode = "SUSYBAKA" #FIXME Add mapcodes correctly
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,
Version=mapVersion,
MapCode=MapCode,
MapData=mapData,
Listed=bool(listed),
HiddenInChallenges=hideInChallenges,
TagIDs=tags,
TagNames=",".join(types.convertTagsToNames(tags)),
ShoesColor=userData.ShoesColor,
PantsColor=userData.PantsColor,
ShirtColor=userData.ShirtColor,
CapeColor=userData.CapeColor,
SkinColor=userData.SkinColor,
HairColor=userData.HairColor,
HatSpr=userData.HatSpr,
Country=userData.Country,
HairSpr=userData.HairSpr,
HatColor=userData.HatColor,
HatColorInv=userData.HatColorInv,
FacialExpression=userData.FacialExpression,
DeathEffect=userData.DeathEffect,
GunSpr=userData.GunSpr,
BulletSpr=userData.BulletSpr,
SwordSpr=userData.SwordSpr,
Costume=userData.Costume,
FollowerSpr=userData.FollowerSpr,
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
MapCode = "SUSYBAKA" # FIXME Add mapcodes correctly
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,
Version=mapVersion,
MapCode=MapCode,
MapData=mapData,
Listed=bool(listed),
HiddenInChallenges=hideInChallenges,
TagIDs=tags,
TagNames=",".join(types.convertTagsToNames(tags)),
ShoesColor=userData.ShoesColor,
PantsColor=userData.PantsColor,
ShirtColor=userData.ShirtColor,
CapeColor=userData.CapeColor,
SkinColor=userData.SkinColor,
HairColor=userData.HairColor,
HatSpr=userData.HatSpr,
Country=userData.Country,
HairSpr=userData.HairSpr,
HatColor=userData.HatColor,
HatColorInv=userData.HatColorInv,
FacialExpression=userData.FacialExpression,
DeathEffect=userData.DeathEffect,
GunSpr=userData.GunSpr,
BulletSpr=userData.BulletSpr,
SwordSpr=userData.SwordSpr,
Costume=userData.Costume,
FollowerSpr=userData.FollowerSpr,
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,
).dict()
]
).dict()
})
],
).dict()
}
)
return {"MapCode": MapCode}
#raise HTTPException(501)
# 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) })
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']
raise HTTPException(404, detail="Map not found")
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) })
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']
raise HTTPException(404, detail="Map not found")
del query["_id"]
leaderboard = query["Leaderboard"]
# Find user
replayIndex = None
@ -320,28 +360,32 @@ 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) })
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,
Reason: int = Body(),
CustomReason: str = Body(default=""),
Authorization: Union[str, None] = Header(default=None),
):
mapID: int,
Reason: int = Body(),
CustomReason: str = Body(default=""),
Authorization: Union[str, None] = Header(default=None),
):
"""Removes ALL records from a specific map"""
authcheck = db.auth_check(Authorization)
@ -352,39 +396,46 @@ 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",
action_data={
"Reason": Reason,
"CustomReason": CustomReason,
"mapID": mapID
},
UserID=userData.ID
)
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,
},
UserID=userData.ID,
)
else:
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.")
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.",
)
## 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()
):
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(),
):
authcheck = db.auth_check(Authorization)
if not authcheck[0] and authcheck[1] == "nouser":
@ -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
# FIXME Add playlists
return
@app.get("/api/v1/followcheck")
async def followcheck():
#FIXME Find the purpouse of this endpoint
# 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)